617

equals覆盖和时必须考虑哪些问题/陷阱hashCode

4

11 回答 11

1468

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

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

还要记住:

使用基于散列的集合映射(例如HashSetLinkedHashSetHashMapHashtableWeakHashMap)时,请确保您放入集合中的关键对象的 hashCode() 在对象位于集合中时不会更改。确保这一点的万无一失的方法是使您的密钥不可变,这还有其他好处

于 2008-08-26T09:12:42.910 回答
298

如果您正在处理使用像 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.name是延迟加载,即使强制加载并返回“John Doe”也是如此。以我的经验,这种情况在和中更常见。nullpersonperson.getName()hashCode()equals()

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

保存一个对象会改变它的状态

持久对象通常使用一个id字段来保存对象的键。首次保存对象时,该字段将自动更新。不要在hashCode(). 但是你可以在equals().

我经常使用的一个模式是

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

但是:你不能包含getId()hashCode(). 如果你这样做了,当一个对象被持久化时,它hashCode就会改变。如果对象在 a 中HashSet,您将“永远”不会再次找到它。

在我的示例中,Person我可能会getName()为. 如果 有一些“碰撞”的风险是可以的,但对于 是绝对不行的。hashCodegetId()getName()equals()hashCode()equals()

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

于 2008-11-02T02:58:13.500 回答
88

关于obj.getClass() != getClass().

该语句是equals()继承不友好的结果。JLS(Java 语言规范)指定 if A.equals(B) == truethenB.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() 调用.

于 2008-08-28T13:16:01.593 回答
47

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

概括:

在他的《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 替换原则,equals() 必须跨继承层次结构工作。

于 2008-09-11T03:06:45.653 回答
31

仍然惊讶于没有人为此推荐 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());
    }
于 2013-02-12T07:17:10.793 回答
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

玩得开心!@.@

于 2013-12-20T06:14:09.953 回答
20

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

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

我在 equals 实现中使用 #1 final,或者在实现为 equals 规定算法的接口时(如java.util集合接口——检查与(obj instanceof Set)或您正在实现的任何接口的正确方法)。当 equals 可以被覆盖时,这通常是一个糟糕的选择,因为这会破坏对称性。

选项 #2 允许在不覆盖等于或破坏对称性的情况下安全地扩展类。

如果您的课程也是Comparable,则equalsandcompareTo方法也应该是一致的。这是一个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;
  }

}
于 2008-08-28T18:25:36.760 回答
16

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

于 2009-02-27T22:05:04.477 回答
11

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
于 2013-10-24T10:56:39.713 回答
7

逻辑上我们有:

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

但反之则不然

于 2013-03-24T14:34:02.590 回答
6

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

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

于 2008-09-02T21:06:47.053 回答