问题
重写equals
和hashCode
时必须考虑哪些问题/陷阱?
#1 热门回答(1365 赞)
###理论(语言律师和数学倾向):
equals()
(javadoc)必须定义一个等价关系(它必须是自反的,对称的和反式的)。另外,它必须是一致的(如果没有修改对象,那么它必须保持返回相同的值)。此外,o.equals(null)
必须始终返回false。
hashCode()
(javadoc)也必须是一致的(如果对象没有按照equals()
进行修改,它必须保持返回相同的值)。
两种方法之间的关系**是:
每当a.equals(b),则a.hashCode()必须与b.hashCode()相同。
在实践中:
如果你覆盖一个,那么你应该覆盖另一个。
使用用于计算equals()
的同一组字段来计算hashCode()
。
使用来自theApache Commons Langlibrary的优秀助手类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。
#2 热门回答(280 赞)
如果你正在处理使用像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()`中不变的属性子集
#3 热门回答(78 赞)
关于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如果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()
****。