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


Answers:


1439

理论(针对语言律师和数学倾向者):

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

hashCode()javadoc)也必须是一致的(如果未根据修改对象equals(),则它必须保持返回相同的值)。

关系的两种方法之间是:

每当a.equals(b),则a.hashCode()必须与相同b.hashCode()

在实践中:

如果覆盖一个,则应覆盖另一个。

使用用于计算的相同字段集equals()进行计算hashCode()

使用优秀的辅助类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()永远不会改变。确保这一点的防弹方法是使您的钥匙不可变,这还有其他好处


12
关于appendSuper()的其他要点:当且仅当您要继承超类的相等行为时,才应在hashCode()和equals()中使用它。例如,如果直接从Object派生,则没有意义,因为默认情况下所有Object都是不同的。
2009年

312
您可以使Eclipse为您生成两个方法:Source> Generate hashCode()和equals()。
RokStrniša2011年

27
同样是真实的使用Netbeans:developmentality.wordpress.com/2010/08/24/...
seinecle

6
@Darthenius Eclipse生成的equals使用getClass()在某些情况下可能会引起问题(请参阅有效的Java项目8)
AndroidGecko 2013年

7
如果第一个空instanceof操作数的第一个操作数为null则返回false(再次有效Java),因此不必进行第一个空检查。
izaban 2013年

295

如果您正在处理使用像Hibernate这样的使用对象关系映射器(ORM)持久化的类,则有一些值得注意的问题,如果您不认为这已经变得不合理地复杂的话!

延迟加载的对象是子类

如果您的对象是使用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则只有使用才能正确运行。

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

ORM通常使用吸气剂来强制加载延迟加载的对象。这意味着,person.name将是null,如果person是懒加载,即使person.getName()迫使装载并返回“李四”。以我的经验,这种现象在hashCode()和出现的频率更高equals()

如果要处理ORM,请确保始终使用getter,并且切勿在hashCode()和中使用字段引用equals()

保存对象将更改其状态

持久对象通常使用id字段来保存对象的键。首次保存对象时,此字段将自动更新。请勿在中使用id字段hashCode()。但是您可以在中使用它equals()

我经常使用的模式是

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

但是:你可以不包括getId()hashCode()。如果这样做,则持久化对象后,对象将hashCode发生更改。如果对象位于中HashSet,您将“永远”不再找到它。

在我的Person例子,我可能会使用getName()hashCode,并getId()getName()(只是偏执)的equals()。如果存在“冲突”的风险是可以的hashCode(),但绝对不可以equals()

hashCode() 应该使用以下属性的不变子集 equals()


2
@Johannes Brodwall:我不明白Saving an object will change it's statehashCode必须返回int,那么您将如何使用getName()?您能举个例子吗hashCode
jimmybondy 2012年

@jimmybondy:getName将返回一个String对象,该对象也具有可使用的hashCode
mateusz.fiolka,2013年

85

有关的澄清obj.getClass() != getClass()

该语句是equals()继承不友好的结果。JLS(Java语言规范)指定了if,A.equals(B) == trueB.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))导致发出真实的,因为它应该。

这看起来非常好,但是看看如果我们尝试同时使用这两个类会发生什么:

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

显然,这是错误的。

如果要确保对称条件。如果b = a,则a = b,并且Liskov替换原理super.equals(other)不仅在B实例的情况下调用,而且在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
如果这样(obj.getClass()!= this.getClass()&& obj.getClass()。isInstance(this),则可以使等值对称(如果将超类对象与子类对象进行比较,则始终使用子类的等值)。 )返回obj.equals(this);
pihentagy

5
@pihentagy-然后,当实现类未覆盖equals方法时,我将得到一个stackoverflow。不好玩。
冉·比隆

2
您不会得到stackoverflow。如果equals方法没有被覆盖,您将再次调用相同的代码,但是递归的条件将始终为false!
2013年

@pihentagy:如果有两个不同的派生类,那会如何表现?如果a ThingWithOptionSetA可以等于a Thing,并且所有附加选项都具有默认值,并且同样对a而言ThingWithOptionSetB,那么只有两个对象的所有非基本属性都与它们的默认值匹配时,才ThingWithOptionSetA可以将a等于a进行比较。ThingWithOptionSetB我看不出您如何对此进行测试。
2013年

7
这样做的问题是它破坏了传递性。如果添加B b2 = new B(1,99),然后b.equals(a) == truea.equals(b2) == true但是b.equals(b2) == false
nickgrim 2015年

46

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

摘要:

约书亚·布洛赫(Joshua Bloch)在他的《有效的Java编程语言指南》(艾迪生-韦斯利,2001年)中宣称:“根本没有办法扩展可实例化的类并在保留平等契约的同时增加方面。” 塔尔不同意。

他的解决方案是通过两种方式调用另一个非对称的blindEquals()来实现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()必须跨继承层次结构工作。


10
看一下这里解释的canEqual方法-相同的原理使两个解决方案都起作用,但是使用canEqual时,您不会两次比较相同的字段(上面,px == this.x将在两个方向上进行测试):artima.com /lejava/articles/equality.html
Blaisorblade 2011年

2
无论如何,我认为这不是一个好主意。这使Equals合约不必要地造成混淆-带有两个Point参数a和b的人必须意识到a.getX()== b.getX()和a.getY()== b.getY的可能性()可以为true,但a.equals(b)和b.equals(a)都为false(如果只有一个是ColorPoint)。
凯文

基本上,这类似于if (this.getClass() != o.getClass()) return false,但很灵活,它仅在派生类麻烦修改等于时才返回false。那正确吗?
Aleksandr Dubinsky

31

仍然感到惊讶的是,没有人为此建议使用番石榴库。

 //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());
    }

23
java.util.Objects.hash()和java.util.Objects.equals()是Java 7(2011年发布)的一部分,因此您不需要Guava。
herman

1
当然,但是您应该避免这种情况,因为Oracle不再提供Java 6的公共更新(自2013年2月以来就是这种情况)。
Herman 2013年

6
您的this参与this.getDate()毫无意义(除了混乱)
郭富城2014年

1
您的“非instanceof”表达式需要加一个括号:if (!(otherObject instanceof DateAndPattern)) {。与Hernan和Steve Kuo达成协议(尽管这是个人喜好问题),但还是+1。
阿莫斯·卡彭特

26

在超类中有两种方法,分别是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(),则不合法。但是,如果我使用更多变量进行编码,则我的代码会编译。为什么不合法?
Adryr83 '18

19

在检查成员相等性之前,有两种方法可以检查类是否相等,我认为这两种方法在正确的情况下都是有用的。

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

我在finalequals实现中使用#1 ,或者在实现为equals规定算法的接口时使用#1 (例如java.utilcollection接口,这是使用with (obj instanceof Set)或正在实现的任何接口进行检查的正确方法)。当可以覆盖equals时,这通常是一个错误的选择,因为这会破坏对称性。

选项#2允许安全地扩展该类,而不会覆盖等号或破坏对称性。

如果您的类也是Comparable,则equalscompareTo方法也应保持一致。这是Comparable类中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 =新的MyClass(123); MyClass b = new MyClass(123){//覆盖某些方法}; //使用this.getClass()。equals(that.getClass())时a.equals(b)为false
steinybot 2015年

1
@Steiny对。在大多数情况下,应该这样做,尤其是如果某个方法被覆盖而不是添加了。考虑上面的示例。如果不是final,并且compareTo()重写了该方法以颠倒排序顺序,则不应将子类和超类的实例视为相等。当这些对象在树中一起使用时,instanceof可能找不到根据实现是“相等”的键。
erickson


11

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

因为int值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()

反之亦然!


6

我发现的一个难题是,两个对象包含彼此的引用(一个示例是父/子关系,并在父上使用了一种便利方法来获取所有子对象)。
例如,在进行Hibernate映射时,这类事情相当普遍。

如果您在hashCode或equals测试中包含关系的两端,则有可能进入以StackOverflowException结尾的递归循环。
最简单的解决方案是在方法中不包括getChildren集合。


5
我认为这里的基本理论是区分对象的属性集合关联。该asssociations不应参与equals()。如果一个疯狂的科学家创造了我的副本,我们将是等效的。但是我们不会有同一个父亲。
Raedwald,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.