HashCode算法为什么采用31作为乘数
一、简述
由于Java默认比较的是类的地址值,每个对象一定是不同的,所以重写hashCode()和equals()方法。这样就会先根据类里的属性生成hashCode,如果生成的hashCode值相同,则再使用equals()比较属性的值。两者都相同则认为这两个对象相等。hashCode()方法的源码:
1️⃣更少的乘积结果冲突
31是质子数中一个“不大不小”的存在,如果使用的是一个如2的较小质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了。
2️⃣31可以被JVM优化
JVM里最有效的计算方式就是进行位运算了:
- 【左移 <<】左边的最高位丢弃,右边补全0(把 << 左边的数据*2的移动次幂)。
- 【右移 >>】把>>左边的数据/2的移动次幂。
- 【无符号右移 >>>】无论最高位是0还是1,左边补齐0。
所以 : 31 * i = (i << 5) - i(左边 31*2=62,右边 2*2^5-2=62)
- 两边相等,JVM就可以高效的进行计算啦。
二、理解
- 首先hash函数必须要选用质数(质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数又称为素数),这个是被科学家论证过的hash函数减少冲突的一个理论。
- 如果设置为偶数的话会存在溢出的情况,导致信息丢失(因为使用偶数相当于使用了移位运算)
- 可以兼顾到虚拟机的性能,虚拟机默认使用2<<5-1 来的到很好的性能,且其是一个不大不小的质数,兼顾了性能和冲突率。
那么问题就来了 是不是有相同的奇素数可以替代31的呢?答案是:有
既然是奇素数,那么自然有相近的如3,17,31,101等,为什么31的性能和解决冲突是最优的?回顾String对象的HashCode方法:
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1️⃣选择不大不小的质数的原因
正如备注上所描述的那样,这段代码的实际执行函数如下
s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1] 推算过程为
i=0 -> h = 31 * 0 + val[0]
i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]
因此可以代入式的计算一下,假如有一个String test = “abcdef” 六个字母。使用一个较小的质数2或者一个较大的质数101 带入进去,前者2^(6-1) = 32
,后者101^(6-1) = 10 510 100 501
。 一个太小一个太大。太小的话hash冲突的概率就会相对大一些,太大的话就会太过于分散,对于hash结构的集合来说就会占用过的存储空间。因此选择不大不小的质数是非常有必要的。
2️⃣如何证明31这个数值就比其他数值优呢?
整型的数值区间是 [-2147483648, 2147483647],区间大小为 2^32。
所以这里可以将区间等分成64个子区间,每个子区间大小为 2^26
乘子2算出的哈希值几乎全部落在第32分区,也就是 [0, 67108864)数值区间内,落在其他区间内的哈希值数量几乎可以忽略不计。这也就不难解释为什么数字2作为乘子时,算出哈希值的冲突率如此之高的原因了
乘子101,冲突率很低,这也说明哈希值溢出并不一定会导致冲突率上升。但是还是因为乘积太大导致整数溢出的问题。所以我想java在设计的时候是为了兼顾性能和hash函数的分散性。
通过上面的分析,使用31这数值,已经可以通过原理上进行理解了。