一句话总结
容量为 2 的幂是为了用位运算 (n-1) & hash 代替取模(位运算比 % 快一个数量级),且 (n-1) 二进制全 1 保证分布均匀。扩容时容量翻倍仍是 2 的幂,元素新位置只有两种可能(原位或原位+旧容量),无需重新 hash。tableSizeFor() 自动将任意初始容量调整为最近的 2 的幂。
初级理解
HashMap 的容量设计为 2 的幂(16、32、64...),核心原因是用位运算代替取模运算,大幅提升性能。
取模运算:hash % n → 计算桶下标
位运算优化:当 n 是 2 的幂时,hash % n == hash & (n - 1)
位运算 & 比取模 % 快得多(CPU 级别的差异),这是 HashMap 高性能的关键设计之一。
// 假设 n = 16(2^4),n - 1 = 15(二进制 1111)
int hash = 18; // 二进制 10010
int index = hash & (16 - 1); // 10010 & 01111 = 00010 = 2
// 等价于 18 % 16 = 2,但位运算更快
为什么 n - 1 的二进制全是 1?因为 n 是 2 的幂,n - 1 的二进制低位全是 1(如 16-1=15=1111),这样 & 运算的结果只取决于 hash 的低位,等价于取模。
中级深入
如果容量不是 2 的幂会怎样?假设 n = 10,n - 1 = 9(二进制 1001),有些位永远是 0,导致某些桶永远用不到,hash 碰撞增加。
// n = 10,n - 1 = 9 = 1001
// hash & 1001 的结果只能是 0,1,8,9
// 桶 2~7 永远用不到!分布极不均匀
tableSizeFor 方法:HashMap 构造时传入的 initialCapacity 不一定是 2 的幂,但内部会通过 tableSizeFor() 方法调整为 ≥ initialCapacity 的最小 2 的幂。
// 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 + 1; // 返回 ≥ cap 的最小 2 的幂
}
// tableSizeFor(10) = 16
// tableSizeFor(17) = 32
高级拓展
2 的幂对扩容的帮助:扩容时容量翻倍(仍然是 2 的幂),元素的新位置只有两种可能:原位置 或 原位置 + 旧容量。因为扩容只多了一位二进制,hash 在这一位是 0 则位置不变,是 1 则位置 + oldCap。
// 扩容时元素重新分配(JDK 8 优化)
// 旧容量 = 16(10000),新容量 = 32(100000)
// hash = 5(00101):oldIndex = 5 & 15 = 5
// newIndex = 5 & 31 = 5(不变)
// hash = 21(10101):oldIndex = 21 & 15 = 5
// newIndex = 21 & 31 = 21 = 5 + 16(原位置+旧容量)
为什么扰动函数要右移 16 位?因为 HashMap 容量通常不会超过 2^16(65536),取模时只用到了 hash 的低 16 位。为了让高位也参与运算,将高 16 位与低 16 位异或,减少碰撞。
面试加分项:能完整说出"2的幂 → 位运算代替取模 → 扩容时元素位置规律"这条逻辑链。
实战场景
场景:正确设置 HashMap 初始容量
// 反例:直接传元素数量
Map<String, Object> map = new HashMap<>(100);
// tableSizeFor(100) = 128,threshold = 128 * 0.75 = 96
// 存到第 97 个元素时就会扩容!
// 正例:initialCapacity = 元素数量 / 0.75 + 1
int expectedSize = 100;
int initialCapacity = (int) (expectedSize / 0.75) + 1; // = 134
Map<String, Object> map = new HashMap<>(initialCapacity);
// tableSizeFor(134) = 256,threshold = 256 * 0.75 = 192
// 存 100 个元素不会触发扩容
// Guava 工具方法
Map<String, Object> map = Maps.newHashMapWithExpectedSize(100);
面试模拟
Q:如果 HashMap 容量不是 2 的幂会怎样?
A:1) (n-1) & hash 不再等价于取模,分布不均匀;2) 扩容时无法用 hash & oldCap 快速判断新位置;3) 某些桶永远用不到,碰撞严重。HashMap 通过 tableSizeFor() 强制调整为 2 的幂,所以用户传任意值最终都是 2 的幂。
Q:tableSizeFor 是怎么工作的?
A:通过五次右移+或运算,将最高位 1 之后的所有位都变成 1,最后 +1 得到 2 的幂。例如 cap=10(1010),处理后变成 1111(15),+1=16。这是一种巧妙的位运算技巧,时间复杂度 O(1)。