在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?