扩容源码再看一下扩容逻辑的具体实现:
resize 详细流程图是是否否是否是否否是单节点红黑树链表是否resize() 入口oldCap table.length\noldThr thresholdoldCap 0?oldCap MAXIMUM_CAPACITY?threshold MAX_VALUE\n返回旧数组newCap oldCap 1\nnewThr oldThr 1oldThr 0?newCap oldThr\n有参构造首次扩容newCap 16, newThr 12\n无参构造首次扩容newThr 0?newThr newCap * loadFactorthreshold newThrnewTab new Node[newCap]oldTab ! null?返回 newTab遍历旧数组 0 ~ oldCap节点数量?newTab[e.hash (newCap-1)] esplit() 拆分红黑树hash oldCap 0?\n拆分低位/高位链表插入低位 newTab[j]插入高位 newTab[j oldCap]继续遍历扩容源码详解/** * 扩容同时负责首次初始化 */ final NodeK, V[] resize() { NodeK, V[] oldTab table; int oldCap (oldTab null) ? 0 : oldTab.length; int oldThr threshold; int newCap, newThr 0; // 计算扩容后容量大小 // 1. 如果原来容量大于0说明不是第一次扩容直接扩容为原来的2倍 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; } } else if (oldThr 0) { // 2. 把原来的阈值当成新的容量大小有参构造首次put时走这个分支 newCap oldThr; } else { // 3. 如果是第一次初始化无参构造则容量和阈值都用默认值 newCap DEFAULT_INITIAL_CAPACITY; newThr (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 4. 如果新的阈值未设置则重新计算扩容后阈值 if (newThr 0) { float ft (float) newCap * loadFactor; newThr (newCap MAXIMUM_CAPACITY ft (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE); } threshold newThr; // 5. 创建一个新数组容量使用上面计算的大小 NodeK, V[] newTab (NodeK, V[]) new Node[newCap]; table newTab; // 6. 遍历原来的数组将元素插入到新数组 if (oldTab ! null) { for (int j 0; j oldCap; j) { NodeK, V e; if ((e oldTab[j]) ! null) { oldTab[j] null; // 帮助 GC 回收 // 7. 如果下标位置只有一个元素则直接插入新数组即可 if (e.next null) { newTab[e.hash (newCap - 1)] e; } else if (e instanceof TreeNode) { // 8. 如果下标位置元素类型是红黑树则执行红黑树的拆分逻辑 ((TreeNodeK, V) e).split(this, newTab, j, oldCap); } else { // 9. 否则执行链表的拆分逻辑使用 do-while 循环 // loHead、loTail表示低位链表的头尾节点 // hiHead、hiTail表示高位链表的头尾节点 NodeK, V loHead null, loTail null; NodeK, V hiHead null, hiTail null; NodeK, V next; do { next e.next; // 10. 判断当前元素 hash oldCap 是否为0 // 为0 → 位置不变插入低位链表 // 不为0 → 位置 原下标 oldCap插入高位链表 if ((e.hash oldCap) 0) { if (loTail null) { loHead e; } else { loTail.next e; } loTail e; } else { if (hiTail null) { hiHead e; } else { hiTail.next e; } hiTail e; } } while ((e next) ! null); // 11. 将低位链表插入到新数组中位置不变 if (loTail ! null) { loTail.next null; newTab[j] loHead; } // 12. 将高位链表插入到新数组中位置 j oldCap if (hiTail ! null) { hiTail.next null; newTab[j oldCap] hiHead; } } } } } return newTab; }重点关注第 9 步的链表拆分逻辑。这个方法非常巧妙不是逐个计算元素在新数组中的下标而是将原链表拆成两个链表整体迁移。假设原数组容量是 16某个下标位置的链表元素分别是1 - 17 - 33 - 49这些元素的共同特点是对 16 求余等于 1。扩容后新数组容量是 32这些元素会拆分成两条链表下标 1 的链表1 - 33hash 16 0位置不变下标 17 的链表17 - 49hash 16 16新位置 1 16 17为什么hash oldCap能判断元素是否需要移动因为容量是 2 的幂扩容后多了一位。如果元素的 hash 值在这一位上是 0下标不变如果是 1新下标 原下标 oldCap。这样就不需要重新计算每个元素的下标大幅提升了扩容效率。 核心提示负载因子为什么默认是 0.75这是空间利用率和时间效率之间的最佳折中。根据泊松分布负载因子为 0.75 时链表长度达到 8 的概率极低约千万分之六。如果负载因子设为 1.0虽然空间利用率高了但哈希冲突会显著增加如果设为 0.5虽然冲突减少了但空间浪费了一半。0.75 是经过大量实验验证的最优值。哈希冲突解决从链表到红黑树当多个 key 的 hash 值计算出相同的数组下标时就会发生哈希冲突。HashMap 采用拉链法解决冲突将冲突的 key 串成链表挂在同一个桶位。但在极端情况下恶意构造 hashCode 或大量冲突链表会很长查询退化为 O(n)。Java 8 引入红黑树来解决这个问题。下面是链表到红黑树的转换流程图否是是否TreeNodeNode否是否是是否发生哈希冲突计算桶位 i (n-1) hashtab[i] 是否已有节点?直接插入, 无冲突key 是否匹配?覆盖旧值当前节点类型?红黑树插入 putTreeVal链表尾插链表长度 8?继续等待数组容量 64?resize() 扩容, 不树化treeifyBin() 链表转红黑树每个 Node 转为 TreeNodetreeify() 构建红黑树红黑树就绪, 查询 O(log n)扩容后该桶位链表长度 6?untreeify() 红黑树退化链表 核心提示红黑树的树化过程不是直接将链表中的节点逐个 insert 到空树中而是先将所有节点转为 TreeNode 并用 prev/next 连成双向链表再通过treeify()方法自底向上构建红黑树。这种方式比逐个插入更高效且构建后会自动平衡。退化过程同理untreeify()会遍历树节点逐个转回普通 Node 并重新连成单向链表。get 源码再看一下 get 方法源码实现/** * get 方法入口 */ public V get(Object key) { NodeK, V e; return (e getNode(hash(key), key)) null ? null : e.value; } /** * 查询节点方法 */ final NodeK, V getNode(int hash, Object key) { NodeK, V[] tab; NodeK, V first, e; int n; K k; // 1. 获取下标位置节点元素命名为 first if ((tab table) ! null (n tab.length) 0 (first tab[(n - 1) hash]) ! null) { // 2. 比较 first 节点哈希值与 key 值 if (first.hash hash ((k first.key) key || (key ! null key.equals(k)))) { return first; } if ((e first.next) ! null) { // 3. 如果 first 节点类型是红黑树就执行红黑树的查找逻辑 if (first instanceof TreeNode) { return ((TreeNodeK, V) first).getTreeNode(hash, key); } // 4. 否则就执行链表的查找逻辑 do { if (e.hash hash ((k e.key) key || (key ! null key.equals(k)))) { return e; } } while ((e e.next) ! null); } } // 5. 都没找到就返回 null return null; }get 方法的逻辑与 put 类似先计算下标 → 检查头节点 → 根据节点类型选择查找方式。头尾节点的比较都优先用判断引用是否相同再用equals判断值是否相等这是一种短路优化。remove 源码再看一下 remove 方法源码/** * 删除方法入口 */ public V remove(Object key) { NodeK, V e; return (e removeNode(hash(key), key, null, false, true)) null ? null : e.value; } /** * 删除节点方法 */ final NodeK, V removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { NodeK, V[] tab; NodeK, V p; int n, index; // 1. 判断数组是否为空下标位置节点是否为空 if ((tab table) ! null (n tab.length) 0 (p tab[index (n - 1) hash]) ! null) { NodeK, V node null, e; K k; V v; // 2. 判断下标节点 key 是否与传入的 key 相等 if (p.hash hash ((k p.key) key || (key ! null key.equals(k)))) { node p; } else if ((e p.next) ! null) { // 3. 如果节点类型是红黑树就执行红黑树的查找逻辑 if (p instanceof TreeNode) { node ((TreeNodeK, V) p).getTreeNode(hash, key); } else { // 4. 如果节点类型是链表就执行链表的查找逻辑 do { if (e.hash hash ((k e.key) key || (key ! null key.equals(k)))) { node e; break; } p e; } while ((e e.next) ! null); } } // 5. 当找到节点时执行删除 if (node ! null (!matchValue || (v node.value) value || (value ! null value.equals(v)))) { if (node instanceof TreeNode) { ((TreeNodeK, V) node).removeTreeNode(this, tab, movable)