最近在看面试题时看到了这个问题
在 HashMap
中,数据是以键值对(key-value)的形式存在底层数组(桶)中的。我们通过键的 hashCode()
方法得到一个哈希值,然后定位到具体的数组下标,再将数据放入该位置的链表或红黑树中。
详细信息可以看深入理解HashMap底层结构-CSDN博客
这一步中就涉及到:如何将哈希值转为数组下标。
通常我们将哈希值转为数组下标,会使用下面这两种方式之一:
✅常规方式(低效):
index = hash % array.length;
模运算 %
的性能相对较低,尤其是在高并发场景中并不理想。
✅ HashMap 使用的方式(高效):
index = (array.length - 1) & hash;
这里的 &
是位运算,比 %
要快得多。但 前提条件是 array.length
必须是 2 的幂次方,这样 length - 1
的二进制刚好全是 1,才可以保留哈希值的低位用于下标定位。
这是一个非常经典的性能优化技巧,广泛应用于 Java 的集合类(如 HashMap
、ConcurrentHashMap
)中。我们来深入理解 为什么当除数是 2 的幂时,可以用 &
替代 %
来提升性能。
我们想计算一个整数 n
对某个数取模的结果:
int index = n % m;
其中,m 是 2 的幂次,比如:2, 4, 8, 16, 32, ...
当 m = 2^k(即 m 是 2 的幂),那么有如下等价关系:
n % m == n & (m - 1)
假设:
n = 10
(二进制为1010
)
m = 8
(即 2³)
则:
m - 1 = 7
(二进制为0111
)
n & (m - 1)
就是取n
的最低 3 位(因为0111
只保留低 3 位)
所以:
1010
& 0111
-------
0010 → 十进制是 2
而 10 % 8 = 2
两者结果一致
因为:
任何整数 n
都可以表示为:
n = a * m + b
,其中 0 ≤ b < m
所以 n % m = b
当 m = 2^k
时,b
就是 n
的最低 k 位的值
而 n & (m - 1)
正好就是提取这 k 位的值
因此:
n % (2^k) == n & (2^k - 1)
在 Java 中,当 HashMap
的元素数量超过「负载因子 * 当前容量」时,就会触发**扩容(resize)**操作。
在 JDK8
中,扩容不是简单地把所有节点重新计算 hash 分布,而是:
数组容量变为原来的两倍(oldCap * 2
)
元素要么留在原位,要么移动到 原位 + oldCap
判断方式
if ((e.hash & oldCap) == 0)
// 留在原位置
else
// 移动到原位置 + oldCap
为什么可以这样做?
因为数组容量是 2 的幂,oldCap
一定是某个二进制位为 1 的数。
例如:oldCap = 16 (10000)
,那么 (e.hash & oldCap)
只会判断 hash 的第 5 位是 0 还是 1:
若是 0 → 不变
若是 1 → 加上 16
这样做的优势:
优势 | 描述 |
---|---|
高性能 | 无需重新计算哈希,只需判断某一位即可 |
低成本 | 不需要 hash % newLength 这种高开销操作 |
更优分布 | 原有哈希值的高低位信息被保留,更利于数据均匀分布 |
举个实际例子
假设你有一个哈希值 h 10101100 和 n 10010000
,旧容量 oldCap = 16 (10000)
。
h & oldCap = 10101100 & 10000 = 0 => 不动
n & oldCap = 10010000 & 10000 = 10000 => 移动到 index + 16
你只要判断第 5 位是不是 1,就知道它该不该移动。
当多个键通过哈希函数映射到了数组中的同一个槽(bucket)时,就发生了哈希冲突。冲突越多:
查询效率降低
可能退化成链表甚至红黑树
影响性能上限
来看 HashMap 的核心定位语句:
index = (n - 1) & hash
如果 n
是 2 的幂,例如 n = 16
,那么 n - 1 = 15 = 00001111
。这个操作就能保留哈希值的低 4 位:
假设 hash = 10101100
index = hash & 00001111 = 1100 = 12
这意味着每一位的变化都能影响最终下标,提升了散列效果。
如果不是 2 的幂会怎么样?
例如 n = 10:
n - 1 = 9 = 00001001
index = hash & 00001001
→ 只有两位参与了索引定位
很容易发生冲突:多个 hash 虽然不同,但结果可能一样
好的哈希散列特点:
每一位的变化都会影响结果(避免聚集)
越多位参与下标计算,越容易分散
使用 2 的幂时,n - 1
会是二进制的全 1,可以让低位 hash 全部参与运算。
比较项 | 2 的幂 | 非 2 的幂 |
---|---|---|
散列分布 | 更均匀 | 容易聚集冲突 |
下标计算 | 快,位运算 | 慢,需 % |
扩容迁移 | 简单,仅判断一位 | 复杂,需重新定位 |
性能影响 | 高效 | 查找慢,冲突多 |
“使用 2 的幂次,是为了更快、更简洁地通过位运算定位数组下标,并在扩容时无需重新 hash。”
这就是 HashMap 中的工程之美,简洁而不简单!
如有疑问或觉得有帮助,欢迎点赞、收藏并留言讨论!