总结:
- hashmap的容量为什么是 2 的 n 次方?
将容量设为2的n次方,是为了保证 n-1 的二进制低位全为 1,从而确保哈希值的每一个低位都能参与到最终的索引计算中,让数据能“均匀”地散布到数组的所有位置。
- 扰动函数的作用
让hashCode 的高位信息参与到了最终的哈希桶定位计算中, 即使两个对象的原始 hashCode 只有高位不同而低位相同,经过这番“扰动”后,它们最终计算出的哈希桶索引也大概率会变得不同。
源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过putVal方法中的(n - 1) & hash决定该Node的存储位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
第一步:理解 h >>> 16 这个操作的含义和作用。
这个操作叫做“无符号右移16位”。它的作用非常直观:就是将一个二进制数(在这里是 hashCode 的32位二进制表示)的所有位都向右移动16个位置。
我们以你提到的对象 A 为例:
-
对象 A 的
hashCode(我们称之为h) 是:10000100011100010000011110000000- 为了方便观察,我将它分成了两部分:左边是“高16位”,右边是“低16位”。
执行 h >>> 16 操作后,这个二进制数会变成:
00000000000000001000010001110001
发生了什么?
原本在“高16位”的部分,被整体移动到了“低16位”的位置上。而原本的“低16位”和移出来的“高16位”超出的部分,都被丢弃了。左边空出的16个位置则全部用0来填充。
请问,对于这一步——“通过右移16位,来获取原始 hashCode 的高16位部分”,您是否理解了?如果理解了,我们再进行下一步,解释为什么要这么做以及异或运算(^)的作用。
第二步:理解 ^ (异或) 运算在这里的作用。
这个操作叫做“按位异或”,符号是 ^。它的规则正如你所说:对两个二进制数的每一个位进行比较,如果两个位不同(一个0一个1),结果就是1;如果两个位相同(都是0或都是1),结果就是0。
现在,我们将第一步的结果和原始的 hashCode 值进行异或运算,也就是 h ^ (h >>> 16)。
我们还是用对象 A 的 hashCode 举例:
- 原始
h:
10000100011100010000011110000000(高16位 | 低16位) h >>> 16:
00000000000000001000010001110001(高16位移到低16位后的结果)
现在,我们把这两个二进制数按位进行 ^ 运算:
1000010001110001 0000011110000000 (原始 h)
^ 0000000000000000 1000010001110001 (h >>> 16)
-----------------------------------
1000010001110001 1000001111110001 (最终的 hash() 方法返回值)
请注意观察结果的后16位(低16位) :
- 原始
h的低16位是:0000011110000000 - 最终结果的低16位是:
1000001111110001
看到了吗? 原始 hashCode 的高16位信息(1000...)通过异或运算, “混合”或者说“注入” 到了低16位中,从而改变了低16位的值。
现在请思考:这个“混合”操作解决了什么问题?
请问,对于这一步——“通过异或运算,将原始 hashCode 的高位信息和低位信息混合在一起,得到一个新的、更混乱的哈希值”,您是否理解了?如果理解了,我们再进行最后一步,看看这个新的哈希值如何避免我们最初提到的问题。
说得非常到位!“更加混乱”正是关键所在。这个过程也常被称为“扰动函数”(Disturbance Function),目的就是增加随机性。
现在我们进入最后一步。
第三步:将新的哈希值代入 (n - 1) & hash 公式,看看效果如何。
默认的hash 是 key.hashCode()
回顾一下我们最初遇到的问题:
- 当数组长度
n为16时,n - 1就是15,二进制是0000...00001111。 &运算的特点是:只有当两个位都是1时,结果才是1。- 这意味着,无论
hash值是什么,(n - 1) & hash运算的结果只取决于hash值的低4位,因为n - 1的高位全是0。
这正是问题的根源:直接使用 a.hashCode() 时,只有低位的几个比特参与了最终位置的计算,高位信息被完全浪费了,这极易导致冲突。
现在,我们使用了 hash() 方法处理后的新哈希值:
- 原始
a.hashCode()的低16位:0000011110000000 - 新
hash值的低16位:1000001111110001(这是高16位和低16位混合后的结果)
现在我们再来计算对象A的索引位置:
(16 - 1) & (新hash值)- 也就是
...00001111&...11110001
取两个二进制数的低4位进行 & 运算:
1111 & 0001 = 0001,结果是1。
我们再来看看对象B的情况(为了对比,我们假设B的高位和低位差异很大):
- 原始
b.hashCode():0111011100111000101000010100000 - 经过
h ^ (h >>> 16)混合后,它的新哈希值的低16位会变成:0111011100111000^101000010100000=1101011101111000 - 取新哈希值的低4位
1000,与1111进行&运算:1111&1000=1000,结果是8。
结论:
通过 h >>> 16 和 ^ 这两个看似复杂的操作,HashMap 成功地让 hashCode 的高位信息参与到了最终的哈希桶定位计算中。即使两个对象的原始 hashCode 只有高位不同而低位相同,经过这番“扰动”后,它们最终计算出的哈希桶索引也大概率会变得不同。
这就大大降低了哈希冲突的概率,使得数据在哈希表中分布得更加均匀。


2727

被折叠的 条评论
为什么被折叠?



