在覆盖 equals
和 hashCode
方法时,必须考虑哪些问题/陷阱?
在覆盖 equals
和 hashCode
方法时,必须考虑哪些问题/陷阱?
equals()
(javadoc)必须定义一个等价关系(它必须是自反的,对称的和传递的)。此外,它必须是一致的(如果对象未被修改,则必须继续返回相同的值)。此外,o.equals(null)
必须始终返回false。
hashCode()
(javadoc)也必须是一致的(如果对象在equals()
方面未被修改,则必须继续返回相同的值)。
两种方法之间的关系是:
每当
a.equals(b)
,那么a.hashCode()
必须与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()不会更改。确保此点的最可靠方法是使您的键不可变,这也有其他好处。
如果你正在处理使用对象关系映射(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
是唯一会正确执行的方法。
延迟加载的对象具有null字段
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()
中非变更的属性的子集
hashCode
必须返回int
,那么你如何使用getName()
?你能举个例子来说明你的hashCode
吗? - jimmybondy关于obj.getClass() != getClass()
的澄清。
这个语句是由于equals()
方法不友好于继承导致的。Java语言规范(JLS)指定如果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))
的结果都是 true,这很正常。
但如果我们尝试同时使用这两个类,情况就不一样了:
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()
。
ThingWithOptionSetA
可以等于一个Thing
,只要所有额外选项都具有默认值,而对于ThingWithOptionSetB
也是如此,那么只有当两个对象的所有非基本属性匹配其默认值时,才可能将ThingWithOptionSetA
与ThingWithOptionSetB
进行比较,但我不知道如何测试。 - supercatB b2 = new B(1,99)
,那么 b.equals(a) == true
和 a.equals(b2) == true
但是 b.equals(b2) == false
。 - nickgrim要实现继承友好的方案,请查看Tal Cohen的解决方案,如何正确实现equals()方法?
摘要:
在他的书《Effective Java编程语言指南》(Addison-Wesley, 2001)中,Joshua Bloch声称:“在保留equals合同的同时,没有办法扩展一个可实例化类并添加一个方面。” Tal持不同意见。
他的解决方案是通过双向调用另一个非对称的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()方法必须跨继承层次结构进行操作。
if (this.getClass() != o.getClass()) return false
,但是它很灵活,只有在派生类修改了 equals 方法时才返回 false。这样对吗? - Aleksandr Dubinskyo
定义 blindyEquals
,这似乎不正确。通过 o.blindlyEquals(this)
你假设另一个对象将有该方法。如果你继续这样做,你将最终得到一堆臃肿的样板代码,每个类都有它们自己版本的 blindyEquals
,只为了让它可以在彼此之间使用。每一个这样的假设都很可怕。 - milanHrabos仍然惊讶于没有人为此推荐石榴图书馆。
//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());
}
this.getDate()
中的 this
除了增加混乱外,没有任何意义。 - Steve Kuoif (!(otherObject instanceof DateAndPattern)) {
。我同意hernan和Steve Kuo的观点(尽管这是个人偏好的问题),但还是给你点赞。 - Amos M. Carpenter在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
玩得开心!@.@
在检查成员相等性之前,有几种方法可以检查类相等性,我认为根据不同情况两种方法都很有用。
instanceof
运算符。this.getClass().equals(that.getClass())
。我在实现一个接口(例如java.util
集合接口)所规定的equals算法或者在一个final
equals 实现中使用第一种方法。当equals可以被覆盖时,这通常是一个糟糕的选择,因为它会破坏对称性。
选项#2允许安全地扩展类而不必覆盖equals或破坏对称性。
如果你的类还实现了Comparable
接口,则equals和compareTo方法也应该一致。以下是一个可比较类中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;
}
}
compareTo()
方法被覆盖以反转排序顺序,则不应将子类和超类的实例视为相等。当这些对象在树中一起使用时,根据instanceof
实现“相等”的键可能无法找到。 - erickson如果你对等号感兴趣,可以查看Angelika Langer的等号的秘密。我非常喜欢它。她还是一个关于Java泛型的很好的FAQ。在这里查看她的其他文章(向下滚动到"Core Java"),她还继续讨论了第二部分和"混合类型比较"。阅读时愉快!
equals() 方法用于确定两个对象的相等性。
因为整数值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()
但并非反过来成立!