HashMap的哈希/扰动函数的设计
总结来说,hash函数的作用就是:将 key 的 hashCode 值进行处理,得到最终的哈希值。
当new 一个 HashMap,并通过 put 方法添加一个元素时,会用到这个方法。
public class Main {
public static void main(String[] args) {
HashMap map=new HashMap();
map.put("sanguo",45);
}
}
我们查看put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
可以看到hash(key)方法,下面,我们来看hash方法具体实现。代码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
下面是对该方法的一些解释:
- 参数 key:需要计算哈希码的键值。
key == null ? 0 : (h = key.hashCode()) ^ (h >>> 16)
:这是一个三目运算符,如果键值为 null,则哈希码为 0(依旧是说如果键为 null,则存放在第一个位置);否则,通过调用hashCode()
方法获取键的哈希码,并将其与右移 16 位的哈希码进行异或运算。^
运算符:异或运算符是 Java 中的一种位运算符,它用于将两个数的二进制位进行比较,如果相同则为 0,不同则为 1。h >>> 16
:将哈希码向右移动 16 位,相当于将原来的哈希码分成了两个 16 位的部分。最终返回的是经过异或运算后得到的哈希码值。- 为什么要进行异或操作呢?因为对于 hashCode 的高位和低位,它们的分布是比较均匀的,如果只是简单地将它们加起来或者进行位运算,容易出现哈希冲突,而异或操作可以避免这个问题。
为什么能降低hash碰撞
因为:哈希函数是先拿到key 的hashcode,是⼀个32位的int类型的数值,然后让hashcode的高16位和低16位进行异或操作。
具体来看:
key. hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为-2147 483648~2147483647,加起来大概40亿的映射空间。 只要哈希函数映射得比较均匀松散,⼀般应用是很难出现碰撞的。但问题是⼀个40亿长度的数组,内存是放不下的。假如HashMap数组的始大小才16,就需要用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。源码中模运算就是把散列值和数组长度 - 1 做⼀个 " 与 & " 操作,位运算比取余 % 运算要快。数组下标i如下。
p = tab[i = (n - 1) & hash
这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样数组长度 - 1正好相当于⼀个 “低位掩码”。 与操作的结果就是散列值的⾼位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16 - 1 = 15。2 进制表⽰是 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1111 。和某个散列值做与操作如下,结果就是截取了最低的四位值。
这样是要快捷⼀些,但是新的问题来了,就算散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。这时候扰动函数的价值就体现出来了,看⼀下扰动函数的示意图: