JDK 源码中 HashMap 的 hash 方法原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/GoSaint/article/details/88977196

先来一段代码,看下HashMap是如何计算hash值的。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

上述的这段代码叫做扰动函数。⼤家都知道上⾯代码⾥的key.hashCode()函数调⽤的是key键值类型⾃带的哈希函数,返回int型散列值。 理论上散列值是⼀个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符 号的int表值范围从-2147483648到2147483648。前后加起来⼤概40亿的映射空间。只要哈希函数映射得 ⽐较均匀松散,⼀般应⽤是很难出现碰撞的。 但问题是⼀个40亿⻓度的数组,内存是放不下的。你想,HashMap扩容之前的数组初始⼤⼩才16。所以这 个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来访问数组下 标。源码中模运算是在这个indexFor( )函数⾥完成的。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        ......
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            // n为数组的长度
        if ((p = tab[i = (n - 1) & hash]) == null)
            // p = tab[i = (n - 1) & hash  计算元素在数组中的位置
        ......
} 

putVal的代码也是比较简单的,就是散列值和数组长度减1做“与”运算。

顺便说⼀下,这也正好解释了为什么HashMap的数组⻓度要取2的整次幂。因为这样(数组⻓度-1)正好 相当于⼀个“低位掩码”。“与”操作的结果就是散列值的⾼位全部归零,只保留低位值,⽤来做数组下 标访问。以初始⻓度16为例,16-1=15。2进制表⽰是00000000 00000000 00001111。和某散列值 做“与”操作如下,结果就是截取了最低的四位值。 

  10100101 11000100 00100101
& 00000000 00000000 00001111
----------------------------------
  00000000 00000000 00000101 

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后⼏位的话,碰撞也会很严重。更要 命的是如果散列本⾝做得不好,分布上成等差数列的漏洞,恰好使最后⼏个低位呈现规律性重复,就⽆⽐ 蛋疼。 这时候“扰动函数”的价值就体现出来了,说到这⾥⼤家应该猜出来了。看下⾯这个图,

                                 

右位移16位,正好是32bit的⼀半,⾃⼰的⾼半区和低半区做异或,就是为了混合原始哈希码的⾼位和低 位,以此来加⼤低位的随机性。⽽且混合后的低位掺杂了⾼位的部分特征,这样⾼位的信息也被变相保留 下来。

猜你喜欢

转载自blog.csdn.net/GoSaint/article/details/88977196