详解HashMap中的Hash算法(扰动函数)

面试中经常会问HashMap的源码,因为HashMap不仅是日常开发中最常用到的类,还因为里面还包括了很多巧妙的算法。

HashMap里对Key取Hash和通过Hash找到在数组中的位置需要调用下面两段代码:

// 以下来源JDK8源码:

// 找到元素在数组中的位置,n为数组长度。
i = (n - 1) & hash

// 计算Key的Hash值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

为什么HashMap数组的长度要是2的整数幂?

我们希望理想的情况是:任意一个Key落在数组中的位置是足够散列的,这样可以减少Hash碰撞的概率。

假设计算出的Hash值是足够散列的,由于Hash值是一个int类型的值,大部分情况下HashMap数组是不会那么长的。所以在有限的数组长度内,当然是取Hash值的低几位算是比较理想的散列方式。

正因为此,而任何2的整数幂,减一得到的二进制位全部是一。如:16-1=15,二进制表示为:1111;32-1=31,二进制表示为:11111。所以让Hash值与(&)上n-1后得到的就是低位Hash值,如:

    00100100 10100101 11000100 00100101    // Hash值
&   00000000 00000000 00000000 00001111    // 16 - 1 = 15
----------------------------------
    00000000 00000000 00000000 00000101    // 高位全部归零,只保留末四位。

Hash算法(扰动函数)

接着上面的理解,所以我们需要一个Hash函数得到足够散列的Hash值。

而任何一个Object类型的hashCode方法得到的Hash值是一个int型,Java中int型是4*8=32位的。显然很少有HashMap的数组有40亿这么长。如果只是取低几位的Hash值的话,那么那些低位相同,高位不同的Hash值就碰撞了,如:

// Hash碰撞示例:
H1: 00000000 00000000 00000000 00000101 & 1111 = 0101
H2: 00000000 11111111 00000000 00000101 & 1111 = 0101

为了解决这类问题,HashMap想了一种办法(扰动):将Hash值的高16位右移并与原Hash值取异或运算(^),混合高16位和低16位的值,得到一个更加散列的低16位的Hash值。如:

00000000 00000000 00000000 00000101 // H1
00000000 00000000 00000000 00000000 // H1 >>> 16
00000000 00000000 00000000 00000101 // hash = H1 ^ (H1 >>> 16) = 5

00000000 11111111 00000000 00000101 // H2
00000000 00000000 00000000 11111111 // H2 >>> 16
00000000 00000000 00000000 11111010 // hash = H2 ^ (H2 >>> 16) = 250

最终:

// 没有Hash碰撞
index1 = (n - 1) & H1 = (16 - 1) & 5 = 5
index2 = (n - 1) & H2 = (16 - 1) & 250 = 10

再加上在程序中位运算是很快的,所以这算是一种非常巧妙并且高效的Hash函数。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章