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 + 位运算索引"的组合设计。

相关推荐
Lee川11 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川15 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i17 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有17 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有17 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫18 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫18 小时前
Handler基本概念
面试
Wect19 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼19 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼19 小时前
Next.js 企业级落地
前端·javascript·面试