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

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

600919 次浏览

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

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

还记得:

当使用基于哈希的集合地图时,例如HashSetLinkedHashSetHashMap哈希表WeakHashMap,确保你放入集合中的关键对象的hashCode()在对象在集合中时永远不会改变。确保这一点的防弹方法是让你的密钥不可变,它还有其他好处吗

关于obj.getClass() != getClass()的澄清。

此语句是equals()不友好继承的结果。JLS (Java语言规范)指定如果A.equals(B) == true,那么B.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替换原理不仅在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()

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

  1. 使用instanceof操作符。
  2. 使用# EYZ0。

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

选项#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;
}


}
我发现的一个问题是两个对象包含对彼此的引用(一个例子是父/子关系,在父上有一个方便的方法来获取所有的子) 例如,在执行Hibernate映射时,这类事情是相当常见的 如果你在hashCode或equals测试中包含关系的两端,就有可能进入一个以StackOverflowException结束的递归循环 最简单的解决方案是在方法中不包含getChildren集合

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

简介:

在他的书有效的Java编程语言指南 (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);
}
}

注意,如果要满足利斯科夫替换原理, equals()必须跨继承层次结构工作。

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

惰性加载对象是子类

如果您的对象是使用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()的不变属性子集

对于等号,查看平等的秘密Angelika兰格。我非常喜欢它。她也是关于 Java中的泛型的一个很好的常见问题。查看她的其他文章在这里(向下滚动到“核心Java”),在那里她还继续讲述了第2部分和“混合类型比较”。祝你阅读愉快!

仍然令人惊讶的是,没有人推荐番石榴库。

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

逻辑上我们有:

# eyz0⇒

但是反之亦然!

Equals()方法用于确定两个对象是否相等。

因为10的int值总是等于10。但是这个equals()方法是关于两个对象是否相等的。当我们说对象时,它会有属性。要决定是否相等,就要考虑这些性质。没有必要考虑所有属性来确定相等性,可以根据类定义和上下文来确定相等性。然后equals()方法可以被重写。

无论何时重写equals()方法,我们都应该重写hashCode()方法。如果不是,会发生什么?如果我们在应用程序中使用哈希表,它将不能像预期的那样运行。由于hashCode用于确定存储的值是否相等,因此它不会为键返回正确的对应值。

给出的默认实现是对象类中的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

超类中有两个方法,如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

这是另一个例子, # EYZ0 < / p >

玩得开心!@ .@