ChatGPT解决这个技术问题 Extra ChatGPT

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

这个问题的答案是社区的努力。编辑现有答案以改进这篇文章。它目前不接受新的答案或交互。

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


1
18 revs, 10 users 77%

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

equals() (javadoc) 必须定义等价关系(它必须是 reflexivesymmetrictransitive)。此外,它必须是一致的(如果对象没有被修改,那么它必须保持返回相同的值)。此外,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()当对象在集合中时,进入集合永远不会改变。确保这一点的防弹方法是使您的密钥不可变,which has also other benefits


关于 appendSuper() 的附加点:当且仅当您想继承超类的相等行为时,您应该在 hashCode() 和 equals() 中使用它。例如,如果您直接从 Object 派生,则没有意义,因为默认情况下所有 Object 都是不同的。
您可以让 Eclipse 为您生成两种方法:Source > Generate hashCode() 和 equals()。
@Darthenius Eclipse 生成的 equals 使用 getClass() 在某些情况下可能会导致问题(请参阅 Effective Java item 8)
鉴于 instanceof 如果其第一个操作数为空(再次有效 Java)返回 false,则不需要第一次空检查。
m
maaartinus

如果您正在处理使用像 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() 用于 hashCode 并使用 getId() 加上 getName()(仅用于偏执)用于 equals()。如果 hashCode() 存在一些“冲突”风险,这是可以的,但对于 equals() 则绝对不行。

hashCode() 应使用 equals() 中不变的属性子集


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

关于 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)) 结果应为真。

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

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


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

对于继承友好的实现,请查看 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。那正确吗?
A
Andy Thomas

仍然惊讶于没有人为此推荐 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。
L
Luna Kong

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

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

使用 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 实现“相等”的键。
J
Johannes Schaub - litb

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


u
user1431282

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

H
Hermes

逻辑上我们有:

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

但反之则不然!


D
Darren Greaves

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

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


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