HashMap 之 长度为什么是 2 的幂次方?hash为什么要右移16位异或?

1. 先给答案:

为了能让 HashMap *存取高效*,进一步降低hash冲突的几率,尽量较少碰撞,也就是要尽量把数据分配均匀。

2. 前景知识:

2.1 长度为什么是 2 的幂次方?

HashMap 可以指定初始容量。当不指定初始容量时,默认是16,每次扩容都增加为2倍。当指定初始容量时,使用 该初始容量的 2次幂 作为HashMap的容量。

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。
JDK1.8 之后 HashMap 底层是 数组+链表+红黑树,红黑树进一步加快搜索过程。

HashMap散列对象位置的过程分为3步:
1)获取key的hashcode:h=key.hashCode()
2)将获得的hash值与 h 进行无符号右移的值进行‘异或’:hash值等于 (h = key.hashCode()) ^ (h >>> 16);
3)将第2)步的异或结果与数组长度减 1 进行 按位与 & 操作,得到下标:int index = (n - 1) & hash。若该下标处已经有元素,就判断该元素与要存入的元素的hash值 以及 key 是否相同,若相同就覆盖;若不同就通过拉链法解决冲突。

因为int类型是32位,在二进制计算机中的范围值为 -231到 231-1,所以,Hash 值的范围值-2147483648 到 2147483647。前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。

用之前要先做对数组的长度取模运算hash % length,得到的余数是数组下标。而计算机中 求余% 效率不如 位运算&,源码中做了优化hash & (length - 1)。然而,

hash % length == hash & (length - 1)的前提是 length 是 2的n次方。

原因如下:
按位与运算 & :先把值变成二进制,有0则为0,全为1才为1,
如果HashMap的长度 length是 奇数,则 length-1 是 偶数,偶数的最后一位是 0,hash & (length - 1)的最后一位肯定是 0,为偶数。这时,任何hash值都只能被散列到偶数下标的位置上,浪费了空间,增加了碰撞。

如果HashMap的长度 length是 偶数,则 length-1 是 奇数,奇数的最后一位是 1,hash & (length - 1)的最后一位取决于 hash值,整体可奇可偶,也就保证了散列的均匀。

因此,length 取 2 的幂次方,能使元素在哈希表中均匀地散列,为了使不同 hash 值发生碰撞的概率较小。

2.2 hash为什么要右移16位异或?h >>> 16

亦或运算 ^:不同为1,相同为0,能更好的保留各部分的特征(变化的地方01都有)

h >>> 16:意思是把 h 的二进制数值向右移动 16 位,因为 int 是32位,那么右移 16 位后,就是把 高16位 移到 低16位 上,而 高16位 都为 0 了。
(h = key.hashCode()) ^ (h >>> 16):意思是用 高16位 和 低16位 进行异或运算。让高位也参与运算,减小碰撞。

?那为什么不适用 & 或 |,而使用 ^ ?
采用 & 运算计算出来的值会向 1 靠拢,采用 | 运算计算出来的值会向 0 靠拢,^ 能更好的保留各部分的特征。

猜你喜欢

转载自blog.csdn.net/weixin_45463572/article/details/130414956