Google首席Java架构师Joshua Bloch在他的著作《Effective Java》中提出了一种简单通用的hashCode算法

1. 初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17;

2. 选取equals方法中用于比较的所有域,然后针对每个域的属性进行计算:

  (1) 如果是boolean值,则计算f ? 1:0

  (2) 如果是byte\char\short\int,则计算(int)f

  (3) 如果是long值,则计算(int)(f ^ (f >>> 32))

  (4) 如果是float值,则计算Float.floatToIntBits(f)

  (5) 如果是double值,则计算Double.doubleToLongBits(f),然后返回的结果是long,再用规则(3)去处理long,得到int

  (6) 如果是对象应用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。  否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode 值为0

  (7) 如果是数组,那么需要为每个元素当做单独的域来处理。如果你使用的是1.5及以上版本的JDK,那么没必要自己去    重新遍历一遍数组,java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上,

  java.util.Arrays.hashCode(long[])的具体实现:

public static int hashCode(long a[]) {
        if (a == null)
            return 0;

        int result = 1;
        for (long element : a) {
            int elementHash = (int)(element ^ (element >>> 32));
            result = 31 * result + elementHash;
        }

        return result;
}

Arrays.hashCode(...)只会计算一维数组元素的hashCOde,如果是多维数组,那么需要递归进行hashCode的计算,那么就需要使用Arrays.deepHashCode(Object[])方法。

3. 最后,要如同上面的代码,把每个域的散列码合并到result当中:result = 31 * result + elementHash;

4. 测试,hashCode方法是否符合文章开头说的基本原则,这些基本原则虽然不能保证性能,但是可以保证不出错。

 

 为什么每次需要使用乘法去操作result? 

主要是为了使散列值依赖于域的顺序,还是上面的那个例子,Test t = new Test(1, 0)跟Test t2 = new Test(0, 1), t和t2的最终hashCode返回值是不一样的。

 为什么是31? 31是个神奇的数字,因为任何数n * 31就可以被JVM优化为 (n << 5) -n,移位和减法的操作效率要比乘法的操作效率高的多。具体解释为:

看一看 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;
    }

可以从注释看出:空字符串的 hashCode 方法返回是 0。并且注释中也给了个公式,可以了解了解。

  String 源码中也使用的 31,然后网上说有这两点原因:

原因一:更少的乘积结果冲突

  31是质子数中一个“不大不小”的存在,如果你使用的是一个如2的较小质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了。

  所以从 31,33,37,39 等中间选择了 31 的原因看原因二。

原因二:31 可以被 JVM 优化

  JVM里最有效的计算方式就是进行位运算了:

  * 左移 << : 左边的最高位丢弃,右边补全0(把 << 左边的数据*2的移动次幂)。
  * 右移 >> : 把>>左边的数据/2的移动次幂。
  * 无符号右移 >>> : 无论最高位是0还是1,左边补齐0。   

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

另外,在Objects类中,还可以使用hash方法一次性传多个参数,分别为各个参数调用不同的Objects.hashCode方法,比如:

public int hashCode() {
		return Objects.hash(name,age);
	}

插播一下,利用lombok项目提供的注解功能,能有效的解决这种重复的基础工作。只需要两步。

添加lombok项目的依赖

<dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.16.20</version>
 </dependency>

添加注解:@Data@EqualsAndHashCode

import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(exclude = {"score","time"})
public class EqualsDemo {
    private String name;
    private int age;
    private boolean gender;
    private float score;
    private String time;
}

另外,可以看一下 Objects的hash()和hashCode()比较: