在 Java 中覆盖 equals 和 hashCode 时应该考虑哪些问题?
Posted
技术标签:
【中文标题】在 Java 中覆盖 equals 和 hashCode 时应该考虑哪些问题?【英文标题】:What issues should be considered when overriding equals and hashCode in Java? 【发布时间】:2010-09-06 20:31:11 【问题描述】:覆盖equals
和hashCode
时必须考虑哪些问题/陷阱?
【问题讨论】:
【参考方案1】:如果您正在处理使用诸如 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
会改变。如果对象在 HashSet
中,您将“永远”不会再次找到它。
在我的Person
示例中,我可能会将getName()
用于hashCode
和getId()
加上getName()
(仅用于偏执狂)用于equals()
。如果hashCode()
存在“冲突”风险,那是可以的,但对于equals()
,绝对不行。
hashCode()
应该使用来自equals()
的不变属性子集
【讨论】:
@Johannes Brodwall:我不明白Saving an object will change it's state
! hashCode
必须返回int
,那么你将如何使用getName()
?你能给你的hashCode
举个例子吗?
@jimmybondy: getName 将返回一个 String 对象,该对象也有一个可以使用的 hashCode 【参考方案2】:
理论(针对语言律师和数学爱好者):
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()当对象在集合中时,您放入集合中的内容永远不会改变。确保这一点的万无一失的方法是使您的密钥不可变,which has also other benefits。
【讨论】:
关于 appendSuper() 的附加说明:当且仅当您想继承超类的相等行为时,您应该在 hashCode() 和 equals() 中使用它。例如,如果您直接从 Object 派生,则没有意义,因为默认情况下所有 Object 都是不同的。 你可以让 Eclipse 为你生成两种方法:Source > Generate hashCode() 和 equals()。 Netbeans 也是如此:developmentality.wordpress.com/2010/08/24/… @Darthenius Eclipse 生成的 equals 使用 getClass() 在某些情况下可能会导致问题(参见 Effective Java item 8) 鉴于instanceof
在其第一个操作数为空时返回false(再次有效Java)这一事实,第一次空检查是不必要的。【参考方案3】:
关于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))
的结果应该是 true。
这看起来都很好,但是看看如果我们尝试同时使用这两个类会发生什么:
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()
。
【讨论】:
您可以通过这种方式使等号对称(如果将超类对象与子类对象进行比较,请始终使用子类的等号) if (obj.getClass() != this.getClass() && obj. getClass().isInstance(this)) return obj.equals(this); @pihentagy - 那么当实现类不覆盖equals方法时,我会得到一个***。不好玩。 你不会得到***。如果equals方法没有被覆盖,你会再次调用相同的代码,但是递归的条件永远是假的! @pihentagy:如果有两个不同的派生类,会有什么表现?如果ThingWithOptionSetA
可以等于Thing
,前提是所有额外选项都有默认值,ThingWithOptionSetB
也是如此,那么ThingWithOptionSetA
应该可以与ThingWithOptionSetB
进行比较仅当两个对象的所有非基本属性都匹配它们的默认值时,但我看不出你是如何测试的。
这个问题是它破坏了传递性。如果添加B b2 = new B(1,99)
,则添加b.equals(a) == true
和a.equals(b2) == true
,但添加b.equals(b2) == false
。【参考方案4】:
超类中有两个方法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
玩得开心! @.@
【讨论】:
对不起,我不明白这个关于 hashCode 方法的说法:如果它使用的变量多于 equals() 是不合法的。但是,如果我使用更多变量进行编码,我的代码就会编译。为什么不合法?【参考方案5】:仍然很惊讶没有人为此推荐 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());
【讨论】:
java.util.Objects.hash() 和 java.util.Objects.equals() 是 Java 7(2011 年发布)的一部分,因此您不需要 Guava。 当然可以,但您应该避免这种情况,因为 Oracle 不再为 Java 6 提供公共更新(自 2013 年 2 月以来一直如此)。 你在this.getDate()
中的this
没有任何意义(除了混乱)
您的“not instanceof”表达式需要一个额外的括号:if (!(otherObject instanceof DateAndPattern))
。同意 hernan 和 Steve Kuo(尽管这是个人喜好问题),但仍然 +1。【参考方案6】:
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
【讨论】:
【参考方案7】:逻辑上我们有:
a.getClass().equals(b.getClass()) && a.equals(b)
⇒ a.hashCode() == b.hashCode()
但不是反之亦然!
【讨论】:
【参考方案8】:对于继承友好的实现,请查看 Tal Cohen 的解决方案,How Do I Correctly Implement the equals() Method?
总结:
在他的书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 Substitution Principle,equals() 必须跨继承层次结构工作。
【讨论】:
看看这里解释的 canEqual 方法 - 相同的原理使两种解决方案都有效,但是使用 canEqual 你不会两次比较相同的字段(上面,px == this.x 将被测试双向):artima.com/lejava/articles/equality.html 无论如何,我认为这不是一个好主意。它使 Equals 合约不必要地令人困惑——接受两个 Point 参数 a 和 b 的人必须意识到 a.getX() == b.getX() 和 a.getY() == b.getY 的可能性() 可以为真,但 a.equals(b) 和 b.equals(a) 都为假(如果只有一个是 ColorPoint)。 基本上,这就像if (this.getClass() != o.getClass()) return false
,但它很灵活,如果派生类费心修改equals,它只会返回false。对吗?【参考方案9】:
对于 equals,请查看Angelika Langer 的 Secrets of Equals。我非常爱它。她也是关于 Generics in Java 的一个很好的常见问题解答。查看她的其他文章here(向下滚动到“Core Java”),在那里她还继续介绍了第 2 部分和“混合类型比较”。尽情阅读吧!
【讨论】:
【参考方案10】:我发现的一个问题是两个对象包含彼此的引用(一个例子是父/子关系,在父级上使用便捷方法来获取所有子级)。 例如,在进行 Hibernate 映射时,这类事情相当普遍。
如果您在 hashCode 或 equals 测试中包含关系的两端,则可能会进入以 ***Exception 结束的递归循环。 最简单的解决方案是不在方法中包含 getChildren 集合。
【讨论】:
我认为这里的基本理论是区分对象的属性、聚合和关联。 关联不应参与equals()
。如果一个疯狂的科学家创造了我的复制品,我们将是等价的。但我们不会有同一个父亲。【参考方案11】:
在检查成员是否相等之前,有几种方法可以检查类是否相等,我认为这两种方法在适当的情况下都很有用。
-
使用
instanceof
运算符。
使用this.getClass().equals(that.getClass())
。
我在 final
equals 实现中使用 #1,或者在实现规定 equals 算法的接口时(例如 java.util
集合接口 - 使用 (obj instanceof Set)
或任何您的接口进行检查的正确方法)重新实施)。当 equals 可以被覆盖时,这通常是一个糟糕的选择,因为这会破坏对称性。
选项 #2 允许在不覆盖等于或破坏对称性的情况下安全地扩展类。
如果你的类也是Comparable
,那么equals
和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;
【讨论】:
为此 +1。 getClass() 和 instanceof 都不是灵丹妙药,这很好地解释了如何处理这两者。不要认为有任何理由不使用 this.getClass() == that.getClass() 而不是使用 equals()。 这有一个问题。不添加任何方面也不覆盖 equals 方法的匿名类将无法通过 getClass 检查,即使它们应该相等。 @Steiny 我不清楚不同类型的对象应该相等;我正在考虑将接口的不同实现作为常见的匿名类。你能举个例子来支持你的前提吗? MyClass a = new MyClass(123); MyClass b = new MyClass(123) // 重写一些方法 ; // 使用 this.getClass().equals(that.getClass()) 时 a.equals(b) 为 false @Steiny 对。在大多数情况下应该如此,特别是如果一个方法被覆盖而不是添加。考虑我上面的例子。如果它不是final
,并且compareTo()
方法被重写以反转排序顺序,则不应将子类和超类的实例视为相等。当这些对象在树中一起使用时,可能找不到根据instanceof
实现“相等”的键。以上是关于在 Java 中覆盖 equals 和 hashCode 时应该考虑哪些问题?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我需要覆盖 Java 中的 equals 和 hashCode 方法?
为啥我需要覆盖 Java 中的 equals 和 hashCode 方法?
Java:Effective java学习笔记之 覆盖equals时总要覆盖hashcode