繁体   English   中英

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

[英]What issues should be considered when overriding equals and hashCode in Java?

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

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

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() 在对象在集合中时不会更改。 确保这一点的万无一失的方法是使您的密钥不可变,这还有其他好处

如果您正在处理使用像 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() 如果这样做,当一个对象被持久化时,它的hashCode改变。 如果对象在HashSet ,您将“永远”找不到它。

在我的Person示例中,我可能会使用getName()作为hashCodegetId()加上getName() (仅用于偏执)作为equals() 如果hashCode()有一些“冲突”的风险是可以的,但对于equals()永远没有问题。

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

关于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 如果 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()

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

概括:

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

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

仍然感到惊讶的是,没有人为此推荐番石榴库。

 //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.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

玩得开心! @.@

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

  1. 使用instanceof运算符。
  2. 使用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;
  }

}

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

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

逻辑上我们有:

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

反之则不然!

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM