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

相关推荐
用户90443816324601 天前
90%前端都踩过的JS内存黑洞:从《你不知道的JavaScript》解锁底层逻辑与避坑指南
前端·javascript·面试
无敌最俊朗@1 天前
STL-vector面试剖析(面试复习4)
java·面试·职场和发展
Benmao⁢1 天前
C语言期末复习笔记
c语言·开发语言·笔记·leetcode·面试·蓝桥杯
测试人社区-千羽1 天前
大语言模型在软件测试中的应用与挑战
人工智能·测试工具·语言模型·自然语言处理·面试·职场和发展·aigc
jiayong231 天前
Redis面试深度解析
数据库·redis·面试
鱼鱼块1 天前
从后端拼模板到 Vue 响应式:前端界面的三次进化
前端·vue.js·面试
愤怒的代码1 天前
第 3 篇:ArrayList / LinkedList / fail-fast 深度解析(5 题)
面试
T___T1 天前
class 出现前,JS 是怎么继承的
前端·javascript·面试
天天扭码1 天前
京东前端开发实习生 一面
前端·网络协议·面试
诗和远方14939562327341 天前
动态库和静态库的区别
面试