这个问题的答案是社区的努力。编辑现有答案以改进这篇文章。它目前不接受新的答案或交互。
覆盖 equals
和 hashCode
时必须考虑哪些问题/陷阱?
理论(适用于语言律师和数学倾向者):
equals()
(javadoc) 必须定义等价关系(它必须是 reflexive、symmetric 和 transitive)。此外,它必须是一致的(如果对象没有被修改,那么它必须保持返回相同的值)。此外,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。
如果您正在处理使用像 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()
中不变的属性子集
Saving an object will change it's state
! hashCode
必须返回 int
,那么您将如何使用 getName()
?你能为你的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 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()
too。
ThingWithOptionSetA
可以等于 Thing
,前提是所有额外选项都具有默认值,并且对于 ThingWithOptionSetB
也是如此,那么 ThingWithOptionSetA
应该可以与 ThingWithOptionSetB
进行比较仅当两个对象的所有非基本属性都与它们的默认值匹配时,但我看不到您如何对此进行测试。
B b2 = new B(1,99)
,则 b.equals(a) == true
和 a.equals(b2) == true
但 b.equals(b2) == false
。
对于继承友好的实现,请查看 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() 必须跨继承层次结构工作。
if (this.getClass() != o.getClass()) return false
,但很灵活,它仅在派生类费心修改 equals 时才返回 false。那正确吗?
仍然惊讶于没有人为此推荐 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());
}
this.getDate()
中的 this
没有任何意义(除了杂乱)
if (!(otherObject instanceof DateAndPattern)) {
。同意 Hernan 和 Steve Kuo 的观点(尽管这是个人喜好问题),但仍然 +1。
超类中有两个方法 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())。
我在 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;
}
}
final
,并且 compareTo()
方法被覆盖以反转排序顺序,则不应将子类和超类的实例视为相等。当这些对象在树中一起使用时,可能找不到根据 instanceof
实现“相等”的键。
对于 equals,请查看 Angelika Langer 的 Secrets of Equals。我非常爱它。她也是关于 Generics in Java 的一个很好的常见问题解答。查看她的其他文章 here(向下滚动到“Core Java”),其中她还继续介绍了第 2 部分和“混合类型比较”。尽情阅读吧!
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
逻辑上我们有:
a.getClass().equals(b.getClass()) && a.equals(b)
⇒ a.hashCode() == b.hashCode()
但反之则不然!
我发现的一个问题是两个对象包含彼此的引用(一个例子是父/子关系,在父级上使用便捷方法来获取所有子级)。例如,在进行 Hibernate 映射时,这类事情相当普遍。
如果您在 hashCode 或 equals 测试中包含关系的两端,则可能会进入以 StackOverflowException 结束的递归循环。最简单的解决方案是不在方法中包含 getChildren 集合。
equals()
。如果一个疯狂的科学家创造了我的复制品,我们将是等价的。但我们不会有同一个父亲。
不定期副业成功案例分享
instanceof
如果其第一个操作数为空(再次有效 Java)返回 false,则不需要第一次空检查。