键值之道:深入学习Java中强大的HashMap(二)

简介: 键值之道:深入学习Java中强大的HashMap

键值之道:深入学习Java中强大的HashMap(一)https://developer.aliyun.com/article/1480891


接下来我们再来看看核心的方法 putVal:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  if ((tab = table) == null || (n = tab.length) == 0)
    // 第一次新增元素的时候进行扩容到默认 16
    n = (tab = resize()).length;
  // 计算索引下标查询对应下标下的数据
  if ((p = tab[i = (n - 1) & hash]) == null)
    // 未发生碰撞那么直接创建新节点放入指定下标位置
    tab[i] = newNode(hash, key, value, null);
  else {
    Node<K,V> e; K k;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      // 存在相同的 key:内存地址相等或者 equals 方法为true
      e = p;
    else if (p instanceof TreeNode)
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
          p.next = newNode(hash, key, value, null);
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    if (e != null) { // existing mapping for key
      // hash值一样,key也一样那么直接进行替换
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}

大概逻辑如下:

  1. 先通过 hash 值计算出 key 映射到哪个桶
  2. 如果桶上没有碰撞冲突,则直接插入
  3. 如果出现碰撞冲突了,则需要处理冲突
  1. 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据
  2. 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树
  1. 如果桶中存在重复的键,则为该键替换新值 value
  2. 如果 size 大于阈值 threshold,则进行扩容

扩容 resize

  • capacity 即容量,默认16。
  • loadFactor 加载因子,默认是0.75
  • threshold 阈值。阈值=容量*加载因子。默认12。当元素数量超过阈值时便会触发扩容。

一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。HashMap的容量是有上限的,必须小于 1<<30。

final Node<K,V>[] resize() {
  Node<K,V>[] oldTab = table;
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  int oldThr = threshold;
  int newCap, newThr = 0;
  if (oldCap > 0) {
    if (oldCap >= MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
      return oldTab;
    }
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
      // 新容量等于旧容量的两倍
      newThr = oldThr << 1; // double threshold
  }
  else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
  else {               // zero initial threshold signifies using defaults
    newCap = DEFAULT_INITIAL_CAPACITY;
    // 计算新的临界值
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  }
  if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
  }
  threshold = newThr;
  @SuppressWarnings({"rawtypes","unchecked"})
  // 创建新数组
  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  table = newTab;
  if (oldTab != null) {
    // ...复制老数组中的元素到新数组,遍历所有的元素进行 rehash,非常耗时间,所以尽量少的出现扩容操作
  }
  return newTab;
}

JDK8 因为巧妙的设计,性能有了大大的提升,由于数组的容量是以 2 的幂次方扩容的,那么在扩容时,新的位置要么在原位置,要么在原长度+原位置的位置。

数组长度变为原来的 2 倍,表现在二进制上就是多了一个高位参与数组下标确定。此时,一个元素通过 hash 转换坐标的方法计算后,恰好出现一个现象:最高位是 0 则坐标不变,最高位是 1 则坐标变为“10000+原坐标”,即“原长度+原坐标”。因此,在扩容时,不需要重新计算元素的 hash 了,只需要判断最高位是 1 还是 0 就好了。

如果我们确切的知道我们有多少键值对需要存储,那么我们在初始化 HashMap 的时候就应该指定它的容量,以防止 HashMap 自动扩容,影响使用效率。

当我们明确知道 HashMap 中元素的个数的时候,把默认容量设置成 initialCapacity/ 0.75F + 1.0F 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。

比如如果我们要存入100个数据,那么将容量可以设置成 100/0.75+1 约等 135。

删除 remove

public V remove(Object key) {
  Node<K,V> e;
  return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}

remove 方式实际将删除逻辑转交 removeNode 方法处理。

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
  Node<K,V>[] tab; Node<K,V> p; int n, index;
  if ((tab = table) != null && (n = tab.length) > 0 &&
      (p = tab[index = (n - 1) & hash]) != null) {
    // 找到目标节点
    Node<K,V> node = null, e; K k; V v;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      // 直接根据 key 匹配到目标节点
      node = p;
    else if ((e = p.next) != null) {
      // 发生碰撞
      if (p instanceof TreeNode)
        // 从红黑树中找到节点
        node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
      else {
        // 从链表中找到节点
        do {
          if (e.hash == hash &&
              ((k = e.key) == key ||
               (key != null && key.equals(k)))) {
            node = e;
            break;
          }
          p = e;
        } while ((e = e.next) != null);
      }
    }
    if (node != null && (!matchValue || (v = node.value) == value ||
                         (value != null && value.equals(v)))) {
      if (node instanceof TreeNode)
        // 调用树的删除
        ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
      else if (node == p)
        // 从链表中删除
        tab[index] = node.next;
      else
        p.next = node.next;
      ++modCount;
      --size;
      afterNodeRemoval(node);
      return node;
    }
  }
  return null;
}

删除方法就是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于 6 的时候要转回链表。

查找 get

public V get(Object key) {
  Node<K,V> e;
  return (e = getNode(hash(key), key)) == null ? null : e.value;
}

调用 get 查找方法,通过元素的 key 找到 value,get 方法实际上将查找逻辑转交 getNode 方法处理。

final Node<K,V> getNode(int hash, Object key) {
  Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  // 如果哈希表不为空并且 key 对应的桶上不为空
  if ((tab = table) != null && (n = tab.length) > 0 &&
      (first = tab[(n - 1) & hash]) != null) {
    // 根据索引的位置检查第一个元素
    if (first.hash == hash && // always check first node
        ((k = first.key) == key || (key != null && key.equals(k))))
      return first;
    // 索引位置的第一个元素不是目标
    if ((e = first.next) != null) {
      if (first instanceof TreeNode)
        // 调用红黑树的查找(递归折半)
        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
      do {
        // 使用链表的方式查找
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          return e;
      } while ((e = e.next) != null);
    }
  }
  return null;
}

调用红黑树的查找使用的是 getTreeNode 的 find 方法。

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
  TreeNode<K,V> p = this;
  do {
    int ph, dir; K pk;
    TreeNode<K,V> pl = p.left, pr = p.right, q;
    if ((ph = p.hash) > h)
      p = pl;
    else if (ph < h)
      p = pr;
    else if ((pk = p.key) == k || (k != null && k.equals(pk)))
      return p;
    else if (pl == null)
      p = pr;
    else if (pr == null)
      p = pl;
    else if ((kc != null ||
              (kc = comparableClassFor(k)) != null) &&
             (dir = compareComparables(kc, k, pk)) != 0)
      p = (dir < 0) ? pl : pr;
    else if ((q = pr.find(h, k, kc)) != null)
      return q;
    else
      p = pl;
  } while (p != null);
  return null;
}
  • 查找红黑树,由于之前添加时已经保证这个树是有序的了,因此查找时基本就是折半查找,效率更高
  • 这里和插入时一样,如果对比结点的哈希值和要查找的哈希值相等,就会判断key是否相等,相等就直接返回。不相等就从子树中递归查找

遍历 HashMap

分别遍历 Key 和 Value

for (String key : map.keySet()) {
  System.out.println(key);
}
for (Object vlaue : map.values() {
  System.out.println(value);
}

使用 Iterator 迭代器迭代(推荐)

Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
  Map.Entry<String, Object> entry = iterator.next();
  System.out.println(entry.getKey());
  System.out.println(entry.getValue());
}

通过 get 方式

Set<String> keySet = map.keySet();
for (String str : keySet) {
  System.out.println(str);
  System.out.println(map.get(str));
}

笔记大部分摘录自《Java核心技术卷I》,含有少数本人修改补充痕迹。

参考文章:http://985.so/b7m3vhttp://985.so/b023nhttp://985.so/b0xnv

相关文章
|
12天前
|
算法 Java 关系型数据库
校招 Java 面试基础题目解析及学习指南含新技术实操要点
本指南聚焦校招Java面试,涵盖Java 8+新特性、多线程与并发、集合与泛型改进及实操项目。内容包括Lambda表达式、Stream API、Optional类、CompletableFuture异步编程、ReentrantLock与Condition、局部变量类型推断(var)、文本块、模块化系统等。通过在线书店系统项目,实践Java核心技术,如书籍管理、用户管理和订单管理,结合Lambda、Stream、CompletableFuture等特性。附带资源链接,助你掌握最新技术,应对面试挑战。
32 2
|
12天前
|
设计模式 算法 Java
2025 春季校招 Java 研发笔试题详细解析及高效学习指南
本指南专为2025春季校招Java研发岗位笔试设计,涵盖Java 17+新特性(如模式匹配、文本块、记录类和密封类)、现代技术栈(Spring Boot 3、响应式编程、Stream API增强)以及算法与数据结构实战。同时深入解析Spring Data JPA、事务管理、性能优化等内容,并结合实际案例讲解常见算法题解与设计模式应用。资源包含核心知识点、面试题及笔试技巧,助力高效备考。下载地址:[链接](https://pan.quark.cn/s/14fcf913bae6)。
28 1
|
16天前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
73 3
|
4月前
|
存储 缓存 安全
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
187 17
Java HashMap详解及实现原理
|
5月前
|
Java 调度 开发者
Java线程池ExecutorService学习和使用
通过学习和使用Java中的 `ExecutorService`,可以显著提升并发编程的效率和代码的可维护性。合理配置线程池参数,结合实际应用场景,可以实现高效、可靠的并发处理。希望本文提供的示例和思路能够帮助开发者深入理解并应用 `ExecutorService`,实现更高效的并发程序。
106 10
|
5月前
|
Java 数据库连接 数据库
【潜意识Java】深度分析黑马项目《苍穹外卖》在Java学习中的重要性
《苍穹外卖》项目对Java学习至关重要。它涵盖了用户管理、商品查询、订单处理等模块,涉及Spring Boot、MyBatis、Redis等技术栈。
480 4
|
5月前
|
前端开发 Java 数据库连接
【潜意识Java】深度解读JavaWeb开发在Java学习中的重要性
深度解读JavaWeb开发在Java学习中的重要性
99 4
|
20天前
|
算法 Java 调度
Java多线程基础
本文主要讲解多线程相关知识,分为两部分。第一部分涵盖多线程概念(并发与并行、进程与线程)、Java程序运行原理(JVM启动多线程特性)、实现多线程的两种方式(继承Thread类与实现Runnable接口)及其区别。第二部分涉及线程同步(同步锁的应用场景与代码示例)及线程间通信(wait()与notify()方法的使用)。通过多个Demo代码实例,深入浅出地解析多线程的核心知识点,帮助读者掌握其实现与应用技巧。
|
4月前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
271 60
【Java并发】【线程池】带你从0-1入门线程池
|
2月前
|
Java 中间件 调度
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
103 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
下一篇
oss创建bucket