HashMap的table长度必须是2的幂次方,以确保hash&(length-1)等价于hash%length,用位运算替代取模提升性能;非2的幂会导致哈希分布不均、冲突加剧、查找退化为O(n)。

为什么HashMap的table长度必须是2的幂次方
因为只有这样,hash & (length - 1) 才等价于 hash % length,且能用位运算替代取模,避免除法开销。
Java 8 的 HashMap 在计算数组下标时,不调用 % 运算符,而是用 hash & (tab.length - 1)。这个技巧成立的前提是 tab.length 是 2 的幂次方——此时 length - 1 的二进制全是 1(比如 16 → 15 → 1111),按位与就自然截取出 hash 值的低几位,效果和取模一致。
如果不是 2 的幂(比如 length=15),14 的二进制是 1110,按位与会“丢掉”某些 bit 位,导致分布不均、大量哈希冲突。
扩容时如何保证长度始终是2的幂
HashMap 不允许用户直接指定任意初始容量,所有传入的 initialCapacity 都会被 tableSizeFor() 函数“向上取整到最近的 2 的幂”。
立即学习“Java免费学习笔记(深入)”;
- 传
new HashMap(10)→ 实际初始化容量是 16 - 传
new HashMap(1000)→ 实际是 1024 - 如果传 0 或负数,会触发默认容量 16
这个逻辑藏在 tableSizeFor() 里:通过连续右移 + 或运算,把最高位之后全变成 1,再 +1 就得到下一个 2 的幂。它不依赖循环或 Math.log,纯位运算,快且确定。
不是2的幂会发生什么(实测现象)
你不能直接让 table.length 变成非 2 的幂,但可以强行绕过构造逻辑测试后果:
- 手动反射修改
table数组长度为 15,再 put 数据 → 下标计算错乱,get()找不到已存的 key - 即使插入成功,
hash & 14会让所有偶数 hash 全落在偶数索引,奇数 hash 全落在奇数索引,桶分布严重倾斜 - 极端情况下,某个桶链表/红黑树长度暴涨,
get()时间退化为 O(n)
这不是“偶尔不准”,而是设计契约被破坏后,整个散列逻辑失效。JDK 没做运行时校验,靠的是构造阶段的强制对齐。
为什么不用 Math.pow(2, ceil(log2(n))) 这种写法
因为要避免浮点运算误差和性能损耗:Math.log() 是 native 调用,慢;浮点转整可能因精度问题向下取整出错(比如本该是 32,算出来 31.9999999 → (int) 后变 31)。
tableSizeFor() 完全基于整数位操作,无分支、无浮点、无溢出风险:
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 >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个函数只对 int 有效,且隐含一个关键点:它假设输入 cap ≤ 2³¹,一旦超过,右移后符号位扩展可能让结果全为 1,+1 后变 0 ——所以最大容量被硬编码为 1 << 30(即 2³⁰),不是 2³¹。
真正容易被忽略的,是 tableSizeFor() 对负数和 0 的处理方式,以及它和扩容阈值 threshold 的联动关系:扩容不是看 size ≥ capacity,而是看 size ≥ threshold,而 threshold = capacity × loadFactor,capacity 又永远是 2 的幂 —— 整个链条里只要一环不是 2 的幂,哈希映射就崩了。










