一、HashMap put操作的时候,index下标如何确定
HashMap<String,Integer> map = new HashMap<>();
map.put(“book”,1);
HashMap为k-v模型,put的过程中,计算下标时,分为两步,首先对 key 进行 hash 操作,其次再通过hash值和数组长度-1进行&运算得到下标,
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
int index = (array.length - 1) & hash; //array为哈希表的长度
-
hash函数是先拿到key 的hashcode,是32位的int值,再让hashcode的高16位和低16位进行异或操作。
可以看到这个函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或。 -
而计算下标index的时候,是使用&位操作,而非%求余。
二、为什么要 高16位 ^ 低16位
1. 数组空间无法满足
因为 key.hashCode() 函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为-2147483648~2147483647,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。
2.降低hash碰撞
- 这么做可以在数组array的长度比较小的时候,也能保证考虑到高低bit都参与到hash的计算中;右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
那么为什么不用 &、| 运算呢? 因为&运算结果偏向于0,而 | 运算结果偏向于1;而 ^ 运算结果为 1 和 0 的概率一致
3.提升速度
算法一定要尽可能高效,因为这是高频操作, 因此采用位运算
三、为什么数组的长度是2^n
- 因为可以有效的降低下标index值的冲突,从而满足哈希算法均匀分布的原则
举个例子:以“book”为Key来演示整个过程:
1.计算book的hashcode,结果为十进制的3029737,二进制为0000 0000 0000 0010 1110 0011 1010 1110 1001
3hash =(hashcode>>>16) ^ hashcode, 结果为:1110 0011 1010 1110 1011
2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制为1111。
3.把以上两个结果做与运算,1110 0011 1010 1110 1011 & 0000 0000 0000 1111 = 1011,十进制是11,所以 index=11。
可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。
假设HashMap的长度是10,重复刚才的运算步骤:
hash: 1110 0011 1010 1110 1011
length-1: 1001
index: 1001 = 9
单独看一个好像没有问题,再来试一个新的值
hash: 1110 0011 1010 1110 1001
length-1: 1001
index: 1001 = 9
hash: 1110 0011 1010 1110 1111
length-1: 1001
index: 1001 = 9
通过例子发现,虽然Hash的倒数第二第三位都有改变,但是运算的index结果都是1001。也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111)!
这样,显然不符合Hash算法均匀分布的原则。
反观长度16或者其他2的幂,因为这样(数组长度-1)正好相当于一个“低位掩码”。与操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。这也保证了index的值始终是在[0,array.leng)
Length-1的值是所有二进制位全为1,这种情况下,index的结果在&运算后就大致的可以看做等同于Hash后几位的值。只要输入的Hash本身分布均匀,Hash算法的结果就是均匀的。
1110 0011 1010 1110 1011
& 0000 0000 0000 0000 1111
----------------------------------
0000 0000 0000 0000 0101 //高位全部归零,只保留末四位