Вопрос-ответ

What issues should be considered when overriding equals and hashCode in Java?

Какие проблемы следует учитывать при переопределении 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() должна работать во всех иерархиях наследования, если должен быть соблюден принцип подстановки Лискова.

java