在Java中重写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()在对象位于集合中时永远不会更改。 确保这一点的防弹方法是使您的密钥不可变,这也有其他好处。
有些问题值得注意,如果您正在处理使用像Hibernate这样的Object-Relationship Mapper(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通常使用getters强制加载延迟加载的对象。 这意味着,即使person.getName()强制加载并返回“John Doe”,如果person是懒加载的, person.name将为null 。 根据我的经验,这在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() 。 如果你这样做,当一个对象被持久化时,它的hashCode改变。 如果对象位于HashSet ,您将“再也不会”找到它。
在我的Person例子中,我可能会使用getName()作为hashCode , getId()加上getName() (仅仅用于偏执狂)为equals() 。 如果hashCode()存在一些“冲突”的风险,但对于equals()永远不会有问题。
hashCode()应该使用equals()不变的属性子集
关于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() 了 。
上一篇: What issues should be considered when overriding equals and hashCode in Java?
