HashCode算法为什么采用31作为乘数

2020-07-03  本文已影响0人  Djbfifjd

一、简述

由于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里最有效的计算方式就是进行位运算了:

所以 : 31 * i = (i << 5) - i(左边 31*2=62,右边 2*2^5-2=62) - 两边相等,JVM就可以高效的进行计算啦。

二、理解

  1. 首先hash函数必须要选用质数(质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数又称为素数),这个是被科学家论证过的hash函数减少冲突的一个理论。
  2. 如果设置为偶数的话会存在溢出的情况,导致信息丢失(因为使用偶数相当于使用了移位运算)
  3. 可以兼顾到虚拟机的性能,虚拟机默认使用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这数值,已经可以通过原理上进行理解了。

上一篇下一篇

猜你喜欢

热点阅读