Какие проблемы следует учитывать при переопределении equals и hashCode в Java?
Какие проблемы / подводные камни необходимо учитывать при переопределении equals
и hashCode
?
Переведено автоматически
Ответ 1
Теория (для языковедов и математиков):
equals()
(javadoc) должен определять отношение эквивалентности (оно должно быть рефлексивным, симметричным и транзитивным). Кроме того, он должен быть согласованным (если объекты не изменены, то он должен продолжать возвращать одно и то же значение). Кроме того, o.equals(null)
должен всегда возвращать false .
hashCode()
(javadoc) также должен быть согласованным (если объект не изменен в терминах equals()
, он должен продолжать возвращать одно и то же значение).
Связь между двумя методами такова:
Всякий раз, когда
a.equals(b)
, тоa.hashCode()
должно совпадать сb.hashCode()
.
На практике:
Если вы переопределяете одно, то вам следует переопределить другое.
Используйте тот же набор полей, который вы используете для вычисления equals()
для вычисления hashCode()
.
Используйте отличные вспомогательные классы EqualsBuilder и HashCodeBuilder из библиотеки Apache Commons Lang. Пример:
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();
}
}
Также помните:
При использовании коллекции или карты на основе хэша, такой как HashSet, LinkedHashSet, HashMap, Hashtable или WeakHashMap, убедитесь, что hashCode() ключевых объектов, которые вы помещаете в коллекцию, никогда не изменяется, пока объект находится в коллекции. Самый надежный способ гарантировать это - сделать ваши ключи неизменяемыми, что имеет и другие преимущества.
Ответ 2
Есть некоторые проблемы, на которые стоит обратить внимание, если вы имеете дело с классами, которые сохраняются с использованием средства сопоставления объектных отношений (ORM), такого как Hibernate, если вы уже не думали, что это неоправданно сложно!
Лениво загруженные объекты являются подклассами
Если ваши объекты сохраняются с использованием 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 обычно используют геттеры для принудительной загрузки объектов с отложенной загрузкой. Это означает, что person.name
будет, null
если person
загружается с задержкой, даже если person.getName()
принудительно загружается и возвращает "John Doe". По моему опыту, это чаще всего возникает в hashCode()
и equals()
.
Если вы имеете дело с ORM, обязательно всегда используйте геттеры и никогда не указывайте ссылки на поля в hashCode()
и equals()
.
Сохранение объекта изменит его состояние
Постоянные объекты часто используют id
поле для хранения ключа объекта. Это поле будет автоматически обновлено при первом сохранении объекта. Не используйте поле id в hashCode()
. Но вы можете использовать это в equals()
.
Шаблон, который я часто использую, это
if (this.getId() == null) {
return this == other;
}
else {
return this.getId().equals(other.getId());
}
Но: вы не можете включить getId()
в hashCode()
. Если вы это сделаете, при сохранении объекта его hashCode
изменения. Если объект находится в a HashSet
, вы "никогда" не найдете его снова.
В моем Person
примере я, вероятно, использовал бы getName()
for hashCode
и getId()
plus getName()
(просто для паранойи) for equals()
. Это нормально, если есть некоторый риск "коллизий" для hashCode()
, но никогда не нормально для equals()
.
hashCode()
следует использовать неизменяемое подмножество свойств из equals()
Ответ 3
Разъяснение о obj.getClass() != getClass()
.
Это утверждение является результатом того, что equals()
оно не поддерживает наследование. В JLS (спецификация языка Java) указано, что if A.equals(B) == true
then 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 и принцип подстановки Лискова вызывают 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()
тоже.
Ответ 4
Для реализации, удобной для наследования, ознакомьтесь с решением Тэла Коэна, как мне правильно реализовать метод equals()?
Краткие сведения:
В своей книге Эффективное руководство по языку программирования Java (Addison-Wesley, 2001) Джошуа Блох утверждает, что "Просто нет способа расширить инстанцируемый класс и добавить аспект при сохранении контракта equals". Tal не согласен.
Его решение заключается в реализации equals() путем вызова другого несимметричного blindlyEquals() обоими способами. Функция 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);
}
}
Обратите внимание, что equals() должна работать во всех иерархиях наследования, если должен быть соблюден принцип подстановки Лискова.