为什么覆盖equals时总要覆盖hashCode

为什么覆盖equals时总要覆盖hashCode

请注意,本文编写于  1,088  天前,最后修改于  723  天前,其中某些信息可能已经过时。

为什么要覆盖hashcode

每个覆盖 equals 方法的类中,也必须覆盖 hashCode 方法。如果不这样做的话,就会违反 Object.hashCode 的通用约定,这个约定的内容如下:

摘自 Object 规范[JavaSE6]:

  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一的返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致
  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果
  • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该都知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的整体性能
因此如果覆盖 equals 方法时没有覆盖 hashCode 方法,就相当于违反了第二条约定:相等的对象必须具有相等的散列码

具体地,根据类的 equals 方法,两个截然不同的实例在逻辑上有可能是相等的,但是,根据 Object 类的 hashCode 方法,它们仅仅是两个没有任何共同之处的对象,所以如果不覆盖 hashCode 的话,这两个在逻辑上相等的对象的 hashCode 返回的结果是两个随机的整数,而不是第二条约定那样返回两个相等的整数。

说的可能繁琐,不如举个例子来看看。

假设我们有一个矩形类 Rectangle,它的定义如下:

public class Rectangle{
    private int width;
    private int height;
    
    public Rectangle(int width,int height){
        this.width = width;
        this.height = height;
    }
}

那么从逻辑上说,如果两个矩形的宽和高是一样的,那么我们就认为这两个矩形是相同的。但是对对象而言,即使两个矩形对象的宽和高一样,但是由于这两个并不是同一个对象,所以进行比较的话结果是 false。

Rectangle rectangle1 = new Rectangle(3, 4);
Rectangle rectangle2 = new Rectangle(3, 4);
System.out.println(rectangle1 == rectangle2); //false
System.out.println(rectangle1.equals(rectangle2); //false

所以,首先要实现只要宽和高相同就是同一个矩形这种逻辑,我们需要覆盖其 equals 方法:

@Override
public boolean equals(Object obj) {
    Rectangle o = (Rectangle) obj;
    if (length == o.length){
        if (width == o.width){
            return true;
        }else return false;
    } else if (width == o.length) {
        if (length == o.width) {
            return true;
        }else return false;
    }
    return false;
}
//输出结果
System.out.println(rectangle1.equals(rectangle2); //true
那么为什么要覆盖 hashCode 呢?

主要是因为如果不继续覆盖 hashCode 的话,那么会导致 Rectangle 类无法跟所有基于散列(hash)的集合一起使用,包括 HashMap、HashSet 和 HashTable 等。

如果不覆盖 hashCode 使用 HashMap 时,我们可以测试一下:

HashMap<Rectangle, String> map2 = new HashMap<>(16);
map2.put(rectangle1, "nice");
map2.put(rectangle2, "bad");

System.out.println("size = " + map2.size()); // 2

这时候的输出结果是 2,但是我们已经认为 rectangle1rectangle2 在逻辑上是相等的了,根据 HashMap 的特性,rectangle1rectangle2 应该是同一个 key 值,也就是说,rectangle2 应该会覆盖掉 rectangle1 的 value 值,即 bad 应该覆盖了 nice,最终结果应该是 size = 1,但事实却并不是像我们想象的那样。这是因为 put 操作是通过hashCode 计算得出对象的散列值,由于没有覆盖 hashCode 方法,导致这两个对象的 hashcode 值像两个随机数,因此大概率会分配到不同的散列桶中(不知道这个概念可以看一下 HashMap 原理),而即使这两个对象恰好被放到同一个散列桶中,底层还是会认为这两个不是同一个对象,这是因为 HashMap 有一个优化,可以将每个项相关联的散列码(hashcode)缓存起来,如果散列码不匹配,也不必再去检验对象的等同性。

如何覆盖hashcode

想要让 rectangle1rectangle2 做为同一个 key,在散列时落在同一个桶中,我们就必须要来覆盖hashCode方法。一个好的散列函数通常倾向于为不相等的对象产生不相等的散列码,这也正是上述第三条约定的含义。通常我们覆盖 hashCode 的计算方法如下:

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + length*width;
    return result;
}

上面的值 17 是任选的,而之所以选择 31,一方面是因为它是一个奇素数,因为如果乘数是偶数,并且乘法溢出的话,信息就会丢失;另外一方面 31 有一个很好的特性,就是可以用移位和减法来代替乘法,可以得到更好的性能:31 * i == (i << 5) - i,现代的 VM 都可以自动完成这种优化。

在我们覆盖 hashCode 后,再试一次调用:

HashMap<Rectangle, String> map2 = new HashMap<>(16);
map2.put(rectangle1, "nice");
map2.put(rectangle2, "nice");
System.out.println("size =" + map2.size()); //1

这样就可以得到我们想要的结果了!

参考文章

  1. 《Effective Java》第9条:覆盖equals时总要覆盖hashcode
  2. 为什么覆盖equals时也要覆盖hashCode方法
  3. equals() and hashCode() methods in Java

本文由 Sanarous 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可,转载前请务必署名
本文链接:https://bestzuo.cn/posts/3751282415.html
最后更新于:2020-07-03 17:45:55

切换主题 | SCHEME TOOL