JDK1.8中HashMap如何应对Hash冲突?

在JDK1.8中,哈希表底层的存储是通过数组+链表/红黑树进行的。JDK1.7中是数组+链表。 当我们通过hashmap.put(kety,value)函数来将对象放入哈希表中,需要通过散列函数进行哈希值的计算,从而来确定对象应该放在数组的哪一个位置。当不同的元素放在数组的同一个位置即发生了hash冲突。

这里我们不讨论发生hash冲突时,HashMap是怎么进行操作的(放在之后的文章中,主要分辨JDK1.7和JDK1.8中HashMap底层有什么不同)。而是关系前一步,当不同的对象需要存储到HashMapz中时,底层是如何计算hash值,以及如何防止hash冲突的产生的呢?

1.如何计算hash值?

当我们向HashMap中放入对象时,执行了put方法。put方法最终会执行putVal()方法,在putVal()方法中又会执行hash(key)这个方法,并将计算的哈希值作为结果返回给putVal()方法。代码如下:

public V put(key,value){
    return putVal(hash(key),key,value,false,true);
}
static final int hash(Object key){
    int h; //32位整数
    //判断key是否为null,如果是null,则直接返回0
    //如果不为null,则返回(h=key.hashCode())^h(>>>16);
    return (key==null)? 0:(h=key.hashCode())^h(>>>16);
}
复制代码

抛去key=null的情况,为了得到哈希值,一共经历了3个步骤: ① h = key.hashCode()方法 ② h>>>16 右移16位 ③ 将①②的结果进行异或

1.1 key.hashCode()方法

这一步根据key的值计算初步的hash值。

如果,对于用户自己创建的类对象,若我们自己重写了hashCode()方法,则执行自己重写的hashCode()方法。若没有重写,则执行Object类中定义的hashCode()方法,即返回对象的内存地址值。

如果,使用java中定义的引用类型:String、Integer则需要进行分开讨论,因为这些类重写了hashCode()方法。Integer返回自己的Integer值。

1.2 h>>>16 右移16位

将第一步中计算的无符号h值向右移动16位,记为h'。这么做的结果就是,h'的高16位全部为0,低16位是原来的高16位。

1.3 将h与h'进行异或操作

第三步,则是将前面计算的两个结果进行异或,得到最终的hash值。

h的高16位 h的低16位
异或XOR 异或XOR
h'的高16位(全0) h'的低16位

由于h’高16位全为0,则与h的高16位异或还是h的高16位。 后面的16位结果则是,h的高16位与低16位的异或结果。

1.4 分析

为什么第一步已经计算出hash值的前提下,还需要继续进行计算hash值呢?结果很容易想到,就是为了防止第一步计算的hash值是冲突的。

那么为什么②+③步通过保留h的高16位,再将高16位和低16位进行异或作为新的低16位能够阻止哈希冲突?

这里需要引出后面一点的内容,如何通过hash值确定数组的位置?

2.如何通过hash值确定数组的位置?

无论是JDK1.8还是JDK1.7 哈希表在创建后底层的数组的size初始值都是16,区别是1.8在使用了put方法后才初始化,是懒汉模式。前面计算得到的hash值是一个32位的无符号整数,它的值是不冲突的(大概率),但是无法对应到16个整数上呀!

HashMap底层是这么做的:

   //将数组的大小减1和hash值按位与操作,与操作保证了结果是在0-n之间的。
   i = (n-1)&hash; //i为数组索引,n为数组大小 
复制代码

我们知道初始化数组大小为n=16。在哈希表建立前期,n较小的情况下,n所对应的无符号32位整数的前16位均为0。因此,在进行与操作时,前面计算的h值的高16位是无效的,那么,当哈希值在低16位相同,而高16位不同的情况下,就无法阻止哈希冲突。这就是为什么,前面计算key的hash值时,将高16位与低16位异或后作为新的低16位。这大大降低了(n-1)&hash之后的数组索引冲突。

3.为什么要进行n-1的细节处理

这是因为HashMap规定了数组的长度必须是2的整数幂次,与HashMap的扩容方法有关。 基于上述分析,n一定为偶数。那么n转化为二进制后,最低位一定为0,再与h值进行异或时,最终的结果最低位也一定为0,转化为整数索引就是偶数。这就浪费了数组索引为奇数的存储空间。

而n-1处理后,使得n-1一定为奇数,二进制最低位为1,按位与运算后最低位为0或者1,得到的数组索引既可能为奇数也可能为偶数,充分利用了数组的空间,降低了哈希冲突的发生。

猜你喜欢

转载自juejin.im/post/7039905645474086920