为什么 HashMap 的容量是 2 的幂次方?

2025年 阅读约 7 分钟 面试指南 · Java面试

深入解析HashMap容量设计为2的幂次方的原因,从位运算优化到均匀分布,分初级、中级、高级三个层次全面讲解。

一句话总结

容量为 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)。