1) 扰动(spread):为什么要“再搅一遍”?
HashMap 取桶位只看 低位(见 §2),若某些 hashCode() 高位离散、低位很烂,就会集中落某些桶。
JDK 8 的做法是把 高 16 位混到低 16 位,最简且够用:
// HashMap.hash(Object key)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
h >>> 16:把高位右移到低位;
-
^:异或混合,让高位信息参与低位决策;
-
结果叫“spread hash”。
直观好处:即使只取低位做索引,也“看到了”原 hash 的高位信息,让分布更均衡。
注:null key 的 spread 固定为 0(所以它总在第 0 桶)。
2) 索引:
index = spread & (n - 1)
数组下标需要 spread % n。如果把 n 设为 2 的幂,有个重要等式:
x % n == x & (n - 1) (仅当 n 为 2 的幂)
所以 索引 = spread & (n-1) ,好处是:
- 更快:按位与替代取模;
- 更均衡:配合扰动,低位不均也能缓解;
- 更简化的扩容再分布(见 §4)。
3) 为什么“容量始终是 2 的幂”
HashMap 会把容量(table 长度)始终保持为 2^k:
-
计算索引可用位与(上节);
-
扩容从 N 到 2N 时,每个节点只可能去“原桶”或“原桶 + oldCap” ,重分布非常轻(见 §4);
-
配合简单扰动就能获得足够均衡的落桶效果(工程折中:性能 + 简洁)。
初始容量会被“上取整到最近的 2^k”。对应代码(JDK 8):
// HashMap.tableSizeFor(int cap)
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1; n |= n >>> 2; n |= n >>> 4;
n |= n >>> 8; n |= n >>> 16;
return (n < 0) ? 1 : (n >= 1<<30) ? 1<<30 : n + 1;
}
4) 扩容再分布:只看一个比特位就够了
设扩容从 oldCap = N 到 newCap = 2N。新索引:
newIndex = spread & (2N - 1) = spread & ((N - 1) | N)
这意味着:
newIndex = (spread & (N - 1)) (如果 (spread & N) == 0)
或 (spread & (N - 1)) + N (如果 (spread & N) != 0)
结论:每个元素不是留在原索引,就是去 原索引 + oldCap。
因此扩容时:
- 无需重新计算完整 hash;
- 只检查 一个新比特位(oldCap 位) 就能决定去向;
- 链表/树内元素可按此规则分成两条链搬运,复杂度与桶内元素数线性相关,成本小。
5) 这套设计带来的综合效果
- 性能:位运算取代 %,扩容再分布也只看一位;整体更快。
- 分布:轻量扰动 + 2^k 容量让低位参与更多高位信息,改善低位差的 hashCode()。
- 实现简洁:扩容逻辑极简(“留在原地或 +oldCap”),也便于树化链退化/重建时的搬运。
6) 小例子(按位直觉)
-
假设 oldCap = 16 (0b1_0000),n-1 = 15 (0b0_1111)
-
某元素 spread = 0b1011_0010:
- 旧索引:0b1011_0010 & 0b0_1111 = 0b0_0010 = 2
- 看 oldCap 位:(spread & 0b1_0000) == 0 → 留下(还是 2)
-
若 spread = 0b1011_1010:
- 旧索引:仍是 2
- spread & oldCap = 0b1_0000 ≠ 0 → 去 2 + 16 = 18
7) 相关细节与易错点
-
扰动不是“加强版 hash” :只是把高位摊到低位,复杂度低、够用即可。真正的分布主要还是靠 key.hashCode() 质量。
-
非 2^k 长度会怎样?
- 索引需要 %(慢);
- 再分布也不能用“原地或 +oldCap”的简单规则,扩容成本高。
-
null key 总在第 0 桶:spread=0 → 索引= 0 & (n-1) = 0。
-
JDK8 树化阈值:当一个桶元素数 ≥ 8 且表长 ≥ 64 时树化,帮助把最坏 O(n) 降到 O(log n) ;扩容后若该桶元素 ≤ 6 会退化回链表。
-
请保证 equals/hashCode 正确且 key 不可变:否则再好的扰动与位运算都救不了冲突与查找失败。
8) 面试快速回答模板(30 秒版)
HashMap 用 spread = h ^ (h >>> 16) 把高位混到低位;索引用 spread & (n-1),所以容量保持 2 的幂,这样按位与就等价取模。扩容从 N 到 2N 时,每个元素只可能留在原桶或去“原桶+oldCap”,只看一个比特位决定,无需重新算 hash,既快又均衡。