在Java中重写equals和hashCode时应考虑哪些问题?

617

在覆盖 equalshashCode 方法时,必须考虑哪些问题/陷阱?

11个回答

1480

理论(适用于语言律师和数学倾向者):

equals()javadoc)必须定义一个等价关系(它必须是自反的对称的传递的)。此外,它必须是一致的(如果对象未被修改,则必须继续返回相同的值)。此外,o.equals(null)必须始终返回false。

hashCode()javadoc)也必须是一致的(如果对象在equals()方面未被修改,则必须继续返回相同的值)。

两种方法之间的关系是:

每当a.equals(b),那么a.hashCode()必须与b.hashCode()相同。

实际应用:

如果您重写了一个,则应该重写另一个。

使用计算equals()的相同字段集合来计算hashCode()

使用Apache Commons Lang库中的优秀辅助类EqualsBuilderHashCodeBuilder。例如:

public class Person {
    private String name;
    private int age;
    // ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
       if (!(obj instanceof Person))
            return false;
        if (obj == this)
            return true;

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

同时记住:

当使用基于哈希的CollectionMap,例如HashSetLinkedHashSetHashMapHashtableWeakHashMap时,请确保在对象放入集合后,其键对象的hashCode()不会更改。确保此点的最可靠方法是使您的键不可变,这也有其他好处


16
关于appendSuper()的补充说明:仅当您希望继承超类的相等行为时,才应在hashCode()和equals()中使用它。例如,如果您是直接从Object派生的,那么没有必要使用它,因为默认情况下所有的Object都是独立的。 - Antti Kissaniemi
319
你可以让 Eclipse 为你生成这两个方法:选择 Source > Generate hashCode() 和 equals()。 - Rok Strniša
29
Netbeans 也是如此:http://developmentality.wordpress.com/2010/08/24/java-creating-correct-equals-and-hashcode-method/ - seinecle
6
Eclipse 生成的 equals 方法使用了 getClass(),在某些情况下可能会引起问题(详见《Effective Java》第8条)。 - AndroidGecko
7
鉴于 "instanceof" 运算符在其第一个操作数为null时返回false,因此第一个空值检查是不必要的(再次引用Effective Java)。 - izaban
显示剩余20条评论

303

如果你正在处理使用对象关系映射(ORM)(如Hibernate)持久化的类,那么有一些问题值得注意,如果你没有认为这已经过于复杂!

延迟加载的对象是子类

如果你的对象使用ORM进行持久化,在许多情况下,你将处理动态代理,以避免过早地从数据存储中加载对象。这些代理被实现为你自己类的子类。这意味着this.getClass() == o.getClass()会返回false。例如:

Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy

如果你正在处理ORM,使用o instanceof Person是唯一会正确执行的方法。

延迟加载的对象具有null字段

ORM通常使用getter强制加载延迟加载的对象。这意味着如果person是延迟加载的,则person.name将为null,即使person.getName()强制加载并返回"John Doe"。在我的经验中,这更常见地出现在hashCode()equals()中。

如果你正在处理ORM,请确保始终使用getter,并且在hashCode()equals()中永远不要使用字段引用。

保存对象将更改其状态

永久化对象通常使用id字段保存对象的主键。当首次保存对象时,该字段将自动更新。不要在hashCode()中使用id字段。但可以在equals()中使用它。

我经常使用的模式是

if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

但是:不能在hashCode()中包含getId()。如果这样做,当对象持久化时,它的hashCode会发生变化。如果该对象在HashSet中,则永远找不到它。

在我的Person示例中,我可能会使用getName()作为hashCode,并使用getId()加上getName()(只是为了谨慎起见)作为equals()。对于hashCode()来说,“碰撞”的风险有些也可以接受,但对于equals()来说则决不能这样。

hashCode()应该使用从equals()中非变更的属性的子集


2
@Johannes Brodwall:我不理解“保存对象将更改其状态”!hashCode必须返回int,那么你如何使用getName()?你能举个例子来说明你的hashCode吗? - jimmybondy
@jimmybondy:getName将返回一个String对象,该对象还具有可用的hashCode。 - mateusz.fiolka

88

关于obj.getClass() != getClass()的澄清。

这个语句是由于equals()方法不友好于继承导致的。Java语言规范(JLS)指定如果A.equals(B) == true那么B.equals(A)也必须返回true。如果省略了这个语句,覆盖equals()(并改变其行为)的继承类将会破坏这个规范。

考虑以下没有这个语句会发生什么的例子:

    class A {
      int field1;

      A(int field1) {
        this.field1 = field1;
      }

      public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
      }
    }

    class B extends A {
        int field2;

        B(int field1, int field2) {
            super(field1);
            this.field2 = field2;
        }

        public boolean equals(Object other) {
            return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
        }
    }    

执行 new A(1).equals(new A(1))new B(1,1).equals(new B(1,1)) 的结果都是 true,这很正常。

但如果我们尝试同时使用这两个类,情况就不一样了:

A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;

显然,这是错误的。

如果您想确保对称条件a=b当且仅当b=a,并且遵循Liskov替换原则,请不仅在B实例的情况下调用super.equals(other),而且在检查A实例后也要这样做:

if (other instanceof B )
   return (other != null && ((B)other).field2 == field2 && super.equals(other)); 
if (other instanceof A) return super.equals(other); 
   else return false;

输出结果将为:

a.equals(b) == true;
b.equals(a) == true;

如果a不是B的引用,则它可能是类A的引用(因为您扩展了它),在这种情况下,您也要调用super.equals()


2
你可以通过以下方式使等式对称(如果比较一个超类对象和子类对象,则始终使用子类的equals方法) if (obj.getClass() != this.getClass() && obj.getClass().isInstance(this)) return obj.equals(this); - pihentagy
6
如果实现类没有覆写 equals 方法,那我就会收到一个 StackOverflow 错误。这很不好。 - Ran Biron
2
你不会得到一个stackoverflow。如果equals方法没有被重写,你将再次调用相同的代码,但递归条件将始终为false! - Jacob Raihle
1
@pihentagy:如果有两个不同的派生类,它会如何表现?如果一个ThingWithOptionSetA可以等于一个Thing,只要所有额外选项都具有默认值,而对于ThingWithOptionSetB也是如此,那么只有当两个对象的所有非基本属性匹配其默认值时,才可能将ThingWithOptionSetAThingWithOptionSetB进行比较,但我不知道如何测试。 - supercat
11
这个问题在于它违反了传递性。如果你添加 B b2 = new B(1,99),那么 b.equals(a) == truea.equals(b2) == true 但是 b.equals(b2) == false - nickgrim

47

要实现继承友好的方案,请查看Tal Cohen的解决方案,如何正确实现equals()方法?

摘要:

在他的书《Effective Java编程语言指南》(Addison-Wesley, 2001)中,Joshua Bloch声称:“在保留equals合同的同时,没有办法扩展一个可实例化类并添加一个方面。” Tal持不同意见。

他的解决方案是通过双向调用另一个非对称的blindlyEquals()方法实现equals(),blindlyEquals()方法由子类重写,equals()方法被继承,但从未被重写。

例如:

class Point {
    private int x;
    private int y;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return (p.x == this.x && p.y == this.y);
    }
    public boolean equals(Object o) {
        return (this.blindlyEquals(o) && o.blindlyEquals(this));
    }
}

class ColorPoint extends Point {
    private Color c;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return (super.blindlyEquals(cp) && 
        cp.color == this.color);
    }
}

请注意,如果要满足 Liskov替换原则,equals()方法必须跨继承层次结构进行操作。


11
看一下这里解释的canEqual方法 - 相同的原则使得这两种解决方案都能工作,但是使用canEqual方法可以避免重复比较相同的字段(上面的p.x == this.x将会在双向测试中都被比较): http://www.artima.com/lejava/articles/equality.html - Blaisorblade
2
无论如何,我认为这不是一个好主意。它使得等式合同变得不必要的混乱 - 一个带有两个点参数a和b的人必须意识到a.getX() == b.getX()和a.getY() == b.getY()可能是真的,但a.equals(b)和b.equals(a)都是假的(如果只有一个是ColorPoint)。 - Kevin
1
基本上,这就像 if (this.getClass() != o.getClass()) return false,但是它很灵活,只有在派生类修改了 equals 方法时才返回 false。这样对吗? - Aleksandr Dubinsky
你正在强制另一个对象 o 定义 blindyEquals,这似乎不正确。通过 o.blindlyEquals(this) 你假设另一个对象将有该方法。如果你继续这样做,你将最终得到一堆臃肿的样板代码,每个类都有它们自己版本的 blindyEquals,只为了让它可以在彼此之间使用。每一个这样的假设都很可怕。 - milanHrabos

32

仍然惊讶于没有人为此推荐石榴图书馆。

 //Sample taken from a current working project of mine just to illustrate the idea

    @Override
    public int hashCode(){
        return Objects.hashCode(this.getDate(), this.datePattern);
    }

    @Override
    public boolean equals(Object obj){
        if ( ! obj instanceof DateAndPattern ) {
            return false;
        }
        return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
                && Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
    }

26
java.util.Objects.hash()和java.util.Objects.equals()是Java 7(发布于2011年)的一部分,所以您不需要使用Guava来实现它们。 - herman
1
当然,你应该避免使用Java 6,因为自2013年2月以来,Oracle不再为其提供公共更新。 - herman
7
this.getDate() 中的 this 除了增加混乱外,没有任何意义。 - Steve Kuo
1
你的“not instanceof”表达式需要一个额外的括号:if (!(otherObject instanceof DateAndPattern)) {。我同意hernan和Steve Kuo的观点(尽管这是个人偏好的问题),但还是给你点赞。 - Amos M. Carpenter

28

在java.lang.Object超类中有两种方法,我们需要覆盖它们以定制对象。

public boolean equals(Object obj)
public int hashCode()

只要相等的对象,它们产生的哈希码必须相同, 但是不相等的对象并不一定会产生不同的哈希码。

public class Test
{
    private int num;
    private String data;
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if((obj == null) || (obj.getClass() != this.getClass()))
            return false;
        // object must be Test at this point
        Test test = (Test)obj;
        return num == test.num &&
        (data == test.data || (data != null && data.equals(test.data)));
    }

    public int hashCode()
    {
        int hash = 7;
        hash = 31 * hash + num;
        hash = 31 * hash + (null == data ? 0 : data.hashCode());
        return hash;
    }

    // other methods
}

如果你想获得更多信息,请查看此链接:http://www.javaranch.com/journal/2002/10/equalhash.html

这是另一个例子,http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

玩得开心!@.@


抱歉,我不理解关于hashCode方法的这个声明:如果它使用的变量比equals()方法多,那么它是不合法的。但是,如果我使用更多的变量编码,我的代码可以编译通过。为什么它是不合法的? - Sam

20

在检查成员相等性之前,有几种方法可以检查类相等性,我认为根据不同情况两种方法都很有用。

  1. 使用 instanceof 运算符。
  2. 使用this.getClass().equals(that.getClass())

我在实现一个接口(例如java.util集合接口)所规定的equals算法或者在一个final equals 实现中使用第一种方法。当equals可以被覆盖时,这通常是一个糟糕的选择,因为它会破坏对称性。

选项#2允许安全地扩展类而不必覆盖equals或破坏对称性。

如果你的类还实现了Comparable接口,则equals和compareTo方法也应该一致。以下是一个可比较类中equals方法的模板:

final class MyClass implements Comparable<MyClass>
{

  …

  @Override
  public boolean equals(Object obj)
  {
    /* If compareTo and equals aren't final, we should check with getClass instead. */
    if (!(obj instanceof MyClass)) 
      return false;
    return compareTo((MyClass) obj) == 0;
  }

}

1
+1。getClass()和instanceof都不是万能的解决方案,这是一个很好的解释如何处理两者。没有理由不使用this.getClass() == that.getClass()而不是使用equals()。 - Paul Cantrell
这里有一个问题。匿名类如果没有添加任何方面或覆盖equals方法,即使它们应该相等,也会失败getClass检查。 - steinybot
@Steiny 我不清楚不同类型的对象是否应该相等;我在考虑将接口的不同实现作为一个通用匿名类的不同实现。你能给出一个例子来支持你的前提吗? - erickson
MyClass a = new MyClass(123); MyClass b = new MyClass(123) { // 覆盖某些方法 }; // 使用 this.getClass().equals(that.getClass()) 时,a.equals(b) 为 false - steinybot
1
@Steiny 对的。在大多数情况下,特别是当方法被覆盖而不是添加时,应该这样做。考虑我上面的例子。如果它不是“final”,并且compareTo()方法被覆盖以反转排序顺序,则不应将子类和超类的实例视为相等。当这些对象在树中一起使用时,根据instanceof实现“相等”的键可能无法找到。 - erickson

16

如果你对等号感兴趣,可以查看Angelika Langer等号的秘密。我非常喜欢它。她还是一个关于Java泛型的很好的FAQ。在这里查看她的其他文章(向下滚动到"Core Java"),她还继续讨论了第二部分和"混合类型比较"。阅读时愉快!


12

equals() 方法用于确定两个对象的相等性。

因为整数值10始终等于10,但是这个 equals() 方法是关于两个对象的相等性的。当我们说对象时,它将具有属性。为了决定相等性,考虑了这些属性。不需要考虑所有属性来确定相等性,并且根据类定义和上下文可以决定。然后可以覆盖 equals() 方法。

每当我们覆盖 equals() 方法时,都应该覆盖 hashCode() 方法。如果没有,会发生什么? 如果在应用程序中使用哈希表,它将不能按预期运行。由于 hashCode 用于确定存储的值的相等性,所以它将不会为键返回正确的相应值。

Object 类中给出的默认实现是 hashCode() 方法使用对象的内部地址并将其转换为整数并返回。

public class Tiger {
  private String color;
  private String stripePattern;
  private int height;

  @Override
  public boolean equals(Object object) {
    boolean result = false;
    if (object == null || object.getClass() != getClass()) {
      result = false;
    } else {
      Tiger tiger = (Tiger) object;
      if (this.color == tiger.getColor()
          && this.stripePattern == tiger.getStripePattern()) {
        result = true;
      }
    }
    return result;
  }

  // just omitted null checks
  @Override
  public int hashCode() {
    int hash = 3;
    hash = 7 * hash + this.color.hashCode();
    hash = 7 * hash + this.stripePattern.hashCode();
    return hash;
  }

  public static void main(String args[]) {
    Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
    Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
    Tiger siberianTiger = new Tiger("White", "Sparse", 4);
    System.out.println("bengalTiger1 and bengalTiger2: "
        + bengalTiger1.equals(bengalTiger2));
    System.out.println("bengalTiger1 and siberianTiger: "
        + bengalTiger1.equals(siberianTiger));

    System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
    System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
    System.out.println("siberianTiger hashCode: "
        + siberianTiger.hashCode());
  }

  public String getColor() {
    return color;
  }

  public String getStripePattern() {
    return stripePattern;
  }

  public Tiger(String color, String stripePattern, int height) {
    this.color = color;
    this.stripePattern = stripePattern;
    this.height = height;

  }
}

示例代码输出:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: –1227465966

7

从逻辑上讲:

a.getClass().equals(b.getClass()) && a.equals(b)a.hashCode() == b.hashCode()

但并反过来成立!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接