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

相关推荐
程序员Sunday1 小时前
TypeScript 的 any、unknown、never 有什么区别?
前端·面试·typescript
小 黄 鸡2 小时前
HashMap 面试必会知识点详解(JDK 8)
java·面试·职场和发展
程序员Sunday6 小时前
ES Module 和 CommonJS 有什么区别?循环依赖时会发生什么?
前端·javascript·面试
溯原的技术笔记7 小时前
JavaScript 闭包
面试
无敌贵点大王7 小时前
嵌入式软件面试经(自用)
c语言·stm32·单片机·学习·mongodb·面试·职场和发展
救救孩子把7 小时前
AI面试八股文-07_6-向量数据库索引详解
数据库·人工智能·面试·向量数据库·hnsw
程序 代码狂人7 小时前
数据开发面试【问答完整版|带深挖追问 + 扩展回答】
面试·职场和发展
江华森8 小时前
Web漏洞与中间件框架漏洞实战详解
面试
美味蛋炒饭.11 小时前
java八股面试(3)
java·开发语言·面试
考研保研资料分享11 小时前
2022食品跨申农业工程保研:中农、海大与华工考核复盘
面试