HashMap扩容:翻倍 + 低/高位链分裂(O(n))

1) 什么时候扩容?

  • 记 capacity 为表长(始终 2 的幂 ),size 为键值对数,loadFactor 默认 0.75
  • 阈值:threshold = capacity * loadFactor。
  • 触发条件:size > threshold 时扩容。首次 put 也会在分配表时设置阈值(见你前文)。

2) 扩多大?

  • 翻倍:newCap = oldCap << 1,直到 MAXIMUM_CAPACITY = 1<<30。
  • 新阈值:newThr = newCap * loadFactor(溢出/顶到最大时做保护)。

3) 核心:低/高位链分裂(lo/hi split)

索引计算是:

ini 复制代码
index = (spreadHash) & (capacity - 1)

扩容 N → 2N 后,新的掩码多出一位(oldCap 这一位)。对每个旧桶里的节点,只需看这一位就能决定新位置:

scss 复制代码
newIndex =
  oldIndex                       若 (hash & oldCap) == 0   // 低位链 lo
  oldIndex + oldCap              若 (hash & oldCap) != 0   // 高位链 hi

关键点不需要重新计算 hash 或做取模 。每个元素只看 1 个比特位 ,要么留在原位置、要么移动到"原位置 + oldCap"。这就是所谓 低/高位链分裂


4) 链表桶怎么搬(保持顺序,O(n))

旧桶是链表时,JDK 8 的 resize 会一次遍历把它拆成两条链尾插(保持相对顺序):

伪码:

ini 复制代码
Node<K,V> loHead=null, loTail=null, hiHead=null, hiTail=null;
for (Node<K,V> e = oldHead; e != null; e = next) {
    next = e.next;
    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;
    }
}
if (loTail != null) { loTail.next = null; newTab[oldIndex] = loHead; }
if (hiTail != null) { hiTail.next = null; newTab[oldIndex + oldCap] = hiHead; }
  • 保持顺序 :JDK 7 时代是头插,可能反转链表;JDK 8 改为尾插,稳定遍历顺序
  • 只遍历一次 :每个节点只看一位、挂到两条新链之一,总成本 O(n) (n 为元素总数)。

5) 红黑树桶怎么搬(TreeNode.split)

当旧桶是红黑树时,调用 TreeNode.split(...):

  • 也按 (hash & oldCap) 拆成 lo/hi 两组;
  • 分别决定目标桶:若某组的节点数 ≤ 6 (UNTREEIFY_THRESHOLD),就退化回链表;否则仍保持红黑树;
  • 树与链之间转换时,节点相对次序也被稳定维护。

6) 为什么要"容量 2 的幂 + 扰动 hash"?

  • 2 的幂让 x % capacity == x & (capacity-1),按位与代替取模更快;
  • 扩容时新索引只与 oldCap 这一位相关,才能做到 "留原位或 +oldCap"
  • JDK 8 的 扰动函数 h ^ (h >>> 16) 把高位信息混到低位,避免仅看低位导致的集中碰撞;两者一起保证扩容与分布的效率和均衡。

7) 复杂度 & 影响

  • 时间 :一次扩容遍历每个元素恰好一次,O(n) 。没有"重新哈希每个键"的额外开销。
  • 空间 :新表分配为原表两倍;扩容瞬间会有峰值内存(旧表 + 新表 + 节点引用调整)。
  • 停顿 :扩容在 put 内同步完成,对大表会有明显停顿 → 预估容量可避免频繁扩容。

8) 实战建议(避免扩容抖动)

  1. 预估初始容量:传入 nextPowerOfTwo(ceil(expected / loadFactor)),一次到位。
  2. 键的 hashCode/equals 要靠谱且 key 不可变:减少碰撞与树化概率(碰撞高会更慢)。
  3. 关注树化门槛 :当桶内元素数 ≥ 8 且容量 ≥ 64 才树化;扩容后若该桶元素 ≤ 6 会退化为链表。
  4. 大规模构建(例如批量导入):先合并数据再一次性放入,或先 ensureCapacity 的思想(自行计算初值)。

9) 小例子(直观看"加 oldCap")

  • 扩容:oldCap = 16 (0b1_0000) → newCap = 32

  • 某节点 hash = 0b1011_0010,旧索引 = hash & 0b0_1111 = 0b0010 = 2

    • 若 hash & oldCap = 0 → 仍在 2****
    • 若 hash = 0b1011_1010(仅 oldCap 位变 1)→ 新索引 2 + 16 = 18

一句话总结

扩容=翻倍 + lo/hi 分裂 :当 size > threshold,表长翻倍;每个旧桶按 (hash & oldCap) 一分为二,元素要么留原位,要么搬到"原位 + oldCap" 。整个过程只看一个比特保持相对顺序总复杂度 O(n) ,这得益于"容量 2 的幂 + 扰动 hash + 位运算索引"的组合设计。

相关推荐
笔尖的记忆7 小时前
【前端架构和框架】react中Scheduler调度原理
前端·面试
南北是北北9 小时前
类型推断、重载与桥方法
面试
程序员清风9 小时前
滴滴二面:MySQL执行计划中,Key有值,还是很慢怎么办?
java·后端·面试
南北是北北9 小时前
泛型变体与通配符(PECS)+ 集合
面试
shepherd1119 小时前
JDK 8钉子户进阶指南:十年坚守,终迎Java 21升级盛宴!
java·后端·面试
南北是北北9 小时前
界类型参数、递归边界与交叉类型
面试
南北是北北9 小时前
java&kotlin泛型语法详解
面试
前端缘梦10 小时前
Webpack 5 核心升级指南:从配置优化到性能提升的完整实践
前端·面试·webpack
LL_break11 小时前
线程1——javaEE 附面题
java·开发语言·面试·java-ee