JDK 1.8 中 HashMap 的 hash 算法和寻址算法

源代码

hash() 方法

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

h = key.hashCode() 表示 hkey 对象的 hashCode 返回值;

h >>> 16h 右移 16 位,因为 int4 字节,32 位,所以右移 16 位后变成: 左边 160 + 右边原 h 的高 16 位;

最后把这两个进行异或返回。

异或:二进制位运算。如果一样返回 0,不一样则返回 1。
例:两个二进制 110 和 100 进行异或

110
^ 100
结果 = 010

putVal() 中寻址部分

tab[i = (n - 1) & hash]
tab 就是 HashMap 里的 table 数组 Node<K,V>[] table;
n 是这个数组的长度 length;
hash 就是上面 hash() 方法返回的值;

为什么不直接用 hashCode() % length ?

看完源码会有疑问,为什么不直接用 key 对象的 hashCode 对哈希表长度取模 ?

寻址为什么不用取模 ?

对于上面寻址算法, 由于计算机对比取模, 与运算会更快。所以为了效率, HashMap 中规定了哈希表长度为 2k 次方,而 2^k-1 转为二进制就是 k 个连续的 1,那么 hash & (k 个连续的 1) 返回的就是 hash 的低 k 个位,该计算结果范围刚好就是 02^k-1,即 0length - 1,跟取模结果一样。

也就是说,哈希表长度 length2 的整次幂时,hash & (length - 1) 的计算结果跟 hash % length 一样,而且效率还更好。

为什么不直接用 hashCode() 而是用它的高 16 位进行异或计算新 hash 值 ?

int 类型占 32 位,可以表示 2^32 种数(范围:-2^312^31-1),而哈希表长度一般不大,在 HashMap 中哈希表的初始化长度是 16HashMap 中的 DEFAULTINITIALCAPACITY),如果直接用 hashCode 来寻址,那么相当于只有低 4 位有效,其他高位不会有影响。这样假如几个 hashCode 分别是 2^102^202^30,那么寻址结果 index 就会一样而发生冲突,所以哈希表就不均匀分布了。
为了减少这种冲突,HashMap 中让 hashCode 的高位也参与了寻址计算(进行扰动),即把 hashCode16 位与 hashCode 进行异或算出 hash,然后根据 hash 来做寻址。

文章作者: David Liu
文章链接: https://davidliu.now.sh/2020/10/24/jdk1-8-hash-map-hash/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 David Liu's Blog