在Java中重写equals和hashCode时应考虑哪些问题?
覆盖" equals"和" hashCode"时必须考虑哪些问题/陷阱?
解决方案
回答
理论(针对语言律师和数学倾向者):
equals()(javadoc)必须定义一个等效关系(它必须是自反的,对称的和可传递的)。另外,它必须是一致的(如果未修改对象,则必须保持返回相同的值)。此外,o.equals(null)必须始终返回false。
hashCode()(javadoc)也必须是一致的(如果未按照equals()修改对象,则必须保持返回相同的值)。
两种方法之间的关系为:
Whenever a.equals(b), then a.hashCode() must be same as 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()不会在对象位于集合中时保持不变。确保这一点的防弹方法是使钥匙不可变,这还有其他好处。
回答
关于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;
显然,这是错误的。
如果要确保对称条件。 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()"。
回答
在检查成员相等性之前,有两种方法可以检查类是否相等,我认为这两种方法在正确的情况下都是有用的。
- 使用
instanceof
运算符。 - 使用
this.getClass()。equals(that.getClass())
。
我在最终的equals实现中使用#1,或者在实现规定了equals算法的接口时使用(例如java.util`集合接口,是使用(obj instanceof Set)或者任何接口检查的正确方法)正在实施)。当可以覆盖equals时,这通常是一个错误的选择,因为这会破坏对称性。
选项#2允许安全地扩展该类,而不会覆盖等号或者破坏对称性。
如果类也是"可比较的",则"等于"和" compareTo"方法也应保持一致。这是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()方法?
概括:
约书亚·布洛赫(Joshua Bloch)在他的《有效的Java编程语言指南》(Addison-Wesley,2001年)一书中声称:"根本没有办法扩展可实例化的类并在保留平等合约的同时增加一个方面。"塔尔不同意。
他的解决方案是通过两种方式分别调用另一个非对称的blindEquals()来实现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()必须跨继承层次结构工作。
回答
如果我们正在处理使用像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通常使用吸气剂来强制加载延迟加载的对象。这意味着,即使" 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"中,则"永远"不会再次找到它。
在我的"个人"示例中,我可能会将" getName()"用于" hashCode"和" getId()",以及将" getName()"(仅用于偏执狂)用于" equals()"。如果" hashCode()"存在"冲突"的风险是可以的,但对于" equals()"则永远不可行。
hashCode()应该使用equals()中不变的属性子集
回答
对于平等,请看安吉莉卡·兰格(Angelika Langer)的《平等的秘密》。我非常爱它。她还是有关Java泛型的很好的常见问题解答。在这里查看她的其他文章(向下滚动至" Core Java"),她还将继续进行第2部分和"混合类型比较"。祝他们阅读愉快!