Java 基础:解析 hashCode

Java 中全部的类都继承自 Object 类,Object 类中有个返回 hashCode 的本地方法。java

public native int hashCode();
复制代码

在文档的注释中很清楚的说明了 hashCode 的做用,和它应该知足的一些要求。算法

做用:给一个对象返回一个 hashCode 值,这个值在 hash table 的数据结构中有重要的做用。例如,肯定放置在 hash table 哪一个索引位置,hash 冲突的频率。segmentfault

要求数组

  1. 同一个 Java 对象,在程序运行的整个生命周期中。该对象返回的 hashCode 应该相同。
  2. 使用 equals 方法,判断为两个相等的对象,其返回的 hashCode 应该相同。
  3. 使用 equals 方法,判断为两个不相同的对象,其返回的 hashCode 应该不相同。

一般的 hashCode 生成方法是将对象的内存地址转换成一个整型数,这样就能为不一样的对象返回一个不同的 hashCode。可是这种方法不能知足上面的第二个条件,因此这种实现也不是 Java 语言所必须的实现方法。数据结构

在 String 中的实现

String 类也是继承自 Object 类,它重写了 hashCode() 方法。优化

/** Cache the hash code for the string */
private int hash; // Default to 0

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;
}
复制代码

在 String 中计算的 hashCode 会存储在 hash 变量中,而且只会计算一次。由于 String 是 final 的,而且一个 String 对象被初始化后没法修改,因此它的 hashCode 不会变化。spa

for 循环计算 hashCode 的方法是根据如下式子:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]。设计

使用 31 的缘由

31 是一个质数(Prime number),质数也称为素数。质数是大于 1 的天然数,且只能被 1 和其自己整除。code

选择 31 的缘由大概有如下几点:对象

  • 一个数乘质数后的结果,只能被 1 、质数、乘数还有结果自己整除。计算 hashCode 选择一个优质质数能够下降 hash 的冲突率。

  • 31 (2 << 5 - 1),在计算的过程当中能够被 JVM 优化。

相信第二点不少同窗都可以理解,如今解释一下第一点。

咱们列举一下 100 之内左右的质数:2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97。

从上面的质数中选择三个小中大质数:2,31,97。分析公式 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 中的每一项,都是一个数乘质数的平方项。因此咱们计算一下每一个质数的 n 次方,咱们选择 n = 5。那么结果以下:

质数 结果
2 2^5 = 32
31 31^5 = 28,629,151
97 97^5 = 8,587,340,257

能够看到经过质数 2 计算后的 hashCode 值是一个比较小的值,而经过质数 97 计算后的 hashCode 是一个比较大的值,而 31 比较适中。

咱们能够认为 hashCode 的范围若过小,可能会增长 hash 冲突的几率。而计算 hashCode 的计算乘子太大容易致使整型数的溢出(这里并非说选择 31 不会致使溢出,是指一个致使溢出的速率),从而也会致使 hash 冲突的几率。31 能够有效的减轻这两点。

更详细的内容能够看一下 stackoverflow 上面的这个问题:Why does Java's hashCode() in String use 31 as a multiplier?

设计 hashCode 算法

根据《Effective Java》第二版中的第 9 条,对于咱们本身编写的类,覆盖 equals 方法时须要覆盖 hashCode 方法。缘由在前面说过。

那么如何设计一个 hashCode 算法,书中设计了一个算法:

  1. 把某个非 0 的常数值,好比 17,保存在一个名为 result 的 int 类型的变量中。
  2. 对于对象中的每一个域,作以下操做:
    • 为该域计算 int 类型的哈希值 c :
      • 若是该域是 boolean 类型,则计算 (f?1:0)
      • 若是该域是 byte、char、short 或者 int 类型,则计算 (int)f
      • 若是该域是 long 类型,则计算 (int)(f^(f>>>32))
      • 若是该域是 float 类型,则计算 Float.floatToIntBits(f)
      • 若是该域是 double 类型,则计算 Double.doubleToLongBits(f),而后重复第三个步骤。
      • 若是该域是一个对象引用,而且该类的 equals 方法经过递归调用 equals 方法来比较这个域,一样为这个域递归的调用 hashCode,若是这个域为 null,则返回0。
      • 若是该域是数组,则要把每个元素看成单独的域来处理,递归的运用上述规则,若是数组域中的每一个元素都很重要,那么可使用 Arrays.hashCode 方法。
    • 按照公式 result = 31 * result + c,把上面步骤 2.1 中计算获得的散列码 c 合并到 result 中。
  3. 返回 result

参考

科普:为何 String hashCode 方法选择数字31做为乘子

Why does Java's hashCode() in String use 31 as a multiplier?

《Effective Java》

相关文章
相关标签/搜索