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

相关推荐
Chan1621 小时前
微服务 - Higress网关
java·spring boot·微服务·云原生·面试·架构·intellij-idea
Cx330❀1 天前
【优选算法必刷100题】第43题(模拟):数青蛙
c++·算法·leetcode·面试
释怀°Believe1 天前
Daily算法刷题【面试经典150题-7️⃣位运算/数学/】
算法·面试·职场和发展
CCPC不拿奖不改名1 天前
网络与API:HTTP基础+面试习题
网络·python·网络协议·学习·http·面试·职场和发展
无限码力1 天前
华为OD技术面真题 - 计算机网络 - 3
计算机网络·华为od·面试·华为od技术面真题·华为od面试八股文·华为od技术面计算机网络相关
Bigbig.1 天前
驱动工程师面试题 - 操作系统1
linux·开发语言·面试·硬件架构
码农丁丁1 天前
谈谈面试的本质
面试·职场和发展·技术管理·ai时代的技术管理
a程序小傲1 天前
【Node】单线程的Node.js为什么可以实现多线程?
java·数据库·后端·面试·node.js
独自归家的兔1 天前
Spring Cloud核心架构组件深度解析(原理+实战+面试高频)
spring cloud·面试·架构
程序员小寒2 天前
从一道前端面试题,谈 JS 对象存储特点和运算符执行顺序
开发语言·前端·javascript·面试