hash & (capacity - 1) 能代替 hash % capacity,因为当capacity为2的幂时,capacity-1二进制全为1,位与操作等价于取模且更快;非2^n时结果不等、分布倾斜、性能暴跌。

为什么 hash & (capacity - 1) 能代替 hash % capacity
因为当 capacity 是 2 的幂时,capacity - 1 的二进制全是 1(比如 16−1=15 → 1111),此时用位与操作等价于只保留 hash 的低几位——这和取模运算数学上完全一致,但 CPU 执行位运算比除法取模快一个数量级。
- 错误现象:
capacity = 15时,hash & 14≠hash % 15,索引会越界或分布严重倾斜 - 实操建议:永远不要手动传非 2^n 的初始容量,哪怕你写
new HashMap(12),构造器内部也会自动上调到 16 - 验证方式:调用
capacity()(需反射)或看扩容日志,你会发现所有实际容量只能是 16、32、64…
扩容时只看“高位比特”是怎么省事的
扩容从 16→32 时,数组长度翻倍,二进制从 10000 变成 100000,关键变化只在新增的那一位(第 5 位)。所以每个元素重哈希时,只需判断原 hash 的这一位是 0 还是 1:是 0 就留在原位置,是 1 就挪到 原下标 + 旧容量 的位置。
- 不用重新算整个
hash % newCapacity,更不用调用key.hashCode()再次计算 - 链表迁移时,JDK 8 会按顺序把节点拆成两个子链,靠的就是这个“单比特判断”逻辑
- 如果容量不是 2^n(比如 17→34),就无法做这种位级快速分流,必须全量重散列,性能断崖下跌
为什么不用质数长度(像 Hashtable 那样)
质数长度确实能在数学上让 % 结果更均匀,但 HashMap 放弃这条路,纯粹为性能让步:现代 CPU 对位运算的优化远超整数除法,而哈希扰动函数 (h = key.hashCode()) ^ (h >>> 16) 已经把高 16 位信息揉进低 16 位,大幅缓解了低位重复导致的聚集问题。
- Hashtable 用 11 作初始容量,是因为它依赖
%,且当时 CPU 指令集对除法优化弱 - HashMap 的扰动函数本质是“用异或把高位信息混入低位”,让
& (capacity - 1)不再只吃低位,而是吃到了更丰富的 hash 信息 - 如果你自己实现类似结构,别盲目模仿质数,先测
&vs%在你的目标平台上的真实耗时
实际编码中容易忽略的三个细节
很多人知道“要是 2 的幂”,但不知道哪些环节会悄悄破坏它。
-
HashMap(int initialCapacity)构造时传 100,实际容量是 128,不是 100——这是 JDK 内部tableSizeFor()方法强制规整的结果 - 使用
putAll()或clone()时,如果源 map 容量不规范(比如被反射篡改过),目标 map 可能继承异常容量,引发后续扩容异常 - 自定义 key 类时,若
hashCode()返回值长期集中在某几个低位(如总是返回偶数),即使容量是 2^n,也会大量碰撞——这时得检查 hash 实现,而不是怪容量
位运算替代取模这事,看着是底层细节,但一旦容量破功,性能退化不是线性的,是成倍放大。真正出问题的时候,往往不是报错,而是压测时 QPS 突然掉一半,查半天才发现某个初始化参数被改成了 30。









