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) 实战建议(避免扩容抖动)
- 预估初始容量:传入 nextPowerOfTwo(ceil(expected / loadFactor)),一次到位。
- 键的 hashCode/equals 要靠谱且 key 不可变:减少碰撞与树化概率(碰撞高会更慢)。
- 关注树化门槛 :当桶内元素数 ≥ 8 且容量 ≥ 64 才树化;扩容后若该桶元素 ≤ 6 会退化为链表。
- 大规模构建(例如批量导入):先合并数据再一次性放入,或先 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 + 位运算索引"的组合设计。