在 Java 中覆盖 equals 和 hashCode 时应该考虑哪些问题?

Posted

技术标签:

【中文标题】在 Java 中覆盖 equals 和 hashCode 时应该考虑哪些问题?【英文标题】:What issues should be considered when overriding equals and hashCode in Java? 【发布时间】:2010-09-06 20:31:11 【问题描述】:

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

【问题讨论】:

【参考方案1】:

如果您正在处理使用诸如 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 通常使用 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() 用于hashCodegetId() 加上getName()(仅用于偏执狂)用于equals()。如果hashCode() 存在“冲突”风险,那是可以的,但对于equals(),绝对不行。

hashCode() 应该使用来自equals() 的不变属性子集

【讨论】:

@Johannes Brodwall:我不明白Saving an object will change it's statehashCode 必须返回int,那么你将如何使用getName()?你能给你的hashCode举个例子吗? @jimmybondy: getName 将返回一个 String 对象,该对象也有一个可以使用的 hashCode 【参考方案2】:

理论(针对语言律师和数学爱好者):

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

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

这两种方法之间的关系是:

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

在实践中:

如果你覆盖了一个,那么你应该覆盖另一个。

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

使用来自Apache Commons Lang 库的优秀帮助类EqualsBuilder 和HashCodeBuilder。一个例子:

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

还要记住:

当使用基于散列的Collection 或Map(例如HashSet、LinkedHashSet、HashMap、Hashtable 或WeakHashMap)时,请确保关键对象的 hashCode()当对象在集合中时,您放入集合中的内容永远不会改变。确保这一点的万无一失的方法是使您的密钥不可变,which has also other benefits。

【讨论】:

关于 appendSuper() 的附加说明:当且仅当您想继承超类的相等行为时,您应该在 hashCode() 和 equals() 中使用它。例如,如果您直接从 Object 派生,则没有意义,因为默认情况下所有 Object 都是不同的。 你可以让 Eclipse 为你生成两种方法:Source > Generate hashCode() 和 equals()。 Netbeans 也是如此:developmentality.wordpress.com/2010/08/24/… @Darthenius Eclipse 生成的 equals 使用 getClass() 在某些情况下可能会导致问题(参见 Effective Java item 8) 鉴于instanceof 在其第一个操作数为空时返回false(再次有效Java)这一事实,第一次空检查是不必要的。【参考方案3】:

关于obj.getClass() != getClass()的说明。

此语句是equals() 继承不友好的结果。 JLS(Java 语言规范)规定如果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)) 的结果应该是 true。

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

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

显然,这是错误的。

如果要确保对称条件。 a=b if b=a 和 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()

【讨论】:

您可以通过这种方式使等号对称(如果将超类对象与子类对象进行比较,请始终使用子类的等号) if (obj.getClass() != this.getClass() && obj. getClass().isInstance(this)) return obj.equals(this); @pihentagy - 那么当实现类不覆盖equals方法时,我会得到一个***。不好玩。 你不会得到***。如果equals方法没有被覆盖,你会再次调用相同的代码,但是递归的条件永远是假的! @pihentagy:如果有两个不同的派生类,会有什么表现?如果ThingWithOptionSetA 可以等于Thing,前提是所有额外选项都有默认值,ThingWithOptionSetB 也是如此,那么ThingWithOptionSetA 应该可以与ThingWithOptionSetB 进行比较仅当两个对象的所有非基本属性都匹配它们的默认值时,但我看不出你是如何测试的。 这个问题是它破坏了传递性。如果添加B b2 = new B(1,99),则添加b.equals(a) == truea.equals(b2) == true,但添加b.equals(b2) == false【参考方案4】:

超类中有两个方法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() 是不合法的。但是,如果我使用更多变量进行编码,我的代码就会编译。为什么不合法?【参考方案5】:

仍然很惊讶没有人为此推荐 guava 库。

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

【讨论】:

java.util.Objects.hash() 和 java.util.Objects.equals() 是 Java 7(2011 年发布)的一部分,因此您不需要 Guava。 当然可以,但您应该避免这种情况,因为 Oracle 不再为 Java 6 提供公共更新(自 2013 年 2 月以来一直如此)。 你在this.getDate() 中的this 没有任何意义(除了混乱) 您的“not instanceof”表达式需要一个额外的括号:if (!(otherObject instanceof DateAndPattern)) 。同意 hernan 和 Steve Kuo(尽管这是个人喜好问题),但仍然 +1。【参考方案6】:

equals() 方法用于判断两个对象是否相等。

因为 10 的 int 值总是等于 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()

不是反之亦然!

【讨论】:

【参考方案8】:

对于继承友好的实现,请查看 Tal Cohen 的解决方案,How Do I Correctly Implement the equals() Method?

总结:

在他的书Effective Java Programming Language Guide(Addison-Wesley,2001 年)中,Joshua Bloch 声称“根本没有办法扩展一个可实例化的类并添加一个方面,同时保留等价契约。”塔尔不同意。

他的解决方案是通过双向调用另一个非对称的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 Substitution Principle,equals() 必须跨继承层次结构工作。

【讨论】:

看看这里解释的 canEqual 方法 - 相同的原理使两种解决方案都有效,但是使用 canEqual 你不会两次比较相同的字段(上面,px == this.x 将被测试双向):artima.com/lejava/articles/equality.html 无论如何,我认为这不是一个好主意。它使 Equals 合约不必要地令人困惑——接受两个 Point 参数 a 和 b 的人必须意识到 a.getX() == b.getX() 和 a.getY() == b.getY 的可能性() 可以为真,但 a.equals(b) 和 b.equals(a) 都为假(如果只有一个是 ColorPoint)。 基本上,这就像if (this.getClass() != o.getClass()) return false,但它很灵活,如果派生类费心修改equals,它只会返回false。对吗?【参考方案9】:

对于 equals,请查看Angelika Langer 的 Secrets of Equals。我非常爱它。她也是关于 Generics in Java 的一个很好的常见问题解答。查看她的其他文章here(向下滚动到“Core Java”),在那里她还继续介绍了第 2 部分和“混合类型比较”。尽情阅读吧!

【讨论】:

【参考方案10】:

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

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

【讨论】:

我认为这里的基本理论是区分对象的属性聚合关联关联不应参与equals()。如果一个疯狂的科学家创造了我的复制品,我们将是等价的。但我们不会有同一个父亲。【参考方案11】:

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

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

我在 final equals 实现中使用 #1,或者在实现规定 equals 算法的接口时(例如 java.util 集合接口 - 使用 (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。 getClass() 和 instanceof 都不是灵丹妙药,这很好地解释了如何处理这两者。不要认为有任何理由不使用 this.getClass() == that.getClass() 而不是使用 equals()。 这有一个问题。不添加任何方面也不覆盖 equals 方法的匿名类将无法通过 getClass 检查,即使它们应该相等。 @Steiny 我不清楚不同类型的对象应该相等;我正在考虑将接口的不同实现作为常见的匿名类。你能举个例子来支持你的前提吗? MyClass a = new MyClass(123); MyClass b = new MyClass(123) // 重写一些方法 ; // 使用 this.getClass().equals(that.getClass()) 时 a.equals(b) 为 false @Steiny 对。在大多数情况下应该如此,特别是如果一个方法被覆盖而不是添加。考虑我上面的例子。如果它不是final,并且compareTo() 方法被重写以反转排序顺序,则不应将子类和超类的实例视为相等。当这些对象在树中一起使用时,可能找不到根据instanceof 实现“相等”的键。

以上是关于在 Java 中覆盖 equals 和 hashCode 时应该考虑哪些问题?的主要内容,如果未能解决你的问题,请参考以下文章

Java 对象 覆盖equals时总要覆盖hashCode

为啥我需要覆盖 Java 中的 equals 和 hashCode 方法?

为啥我需要覆盖 Java 中的 equals 和 hashCode 方法?

Java:Effective java学习笔记之 覆盖equals时总要覆盖hashcode

覆盖 Java 中的 hashCode() 和 equals() 方法

Java:Effective java学习笔记之 覆盖equals时请遵守通用约定