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().
При использовании коллекции или карты на основе хэша, такой как HashSet, LinkedHashSet, HashMap, Hashtable или WeakHashMap, убедитесь, что hashCode() ключевых объектов, которые вы помещаете в коллекцию, никогда не изменяется, пока объект находится в коллекции. Самый надежный способ гарантировать это - сделать ваши ключи неизменяемыми, что имеет и другие преимущества.
Ответ 2
Есть некоторые проблемы, на которые стоит обратить внимание, если вы имеете дело с классами, которые сохраняются с использованием средства сопоставления объектных отношений (ORM), такого как Hibernate, если вы уже не думали, что это неоправданно сложно!
Лениво загруженные объекты являются подклассами
Если ваши объекты сохраняются с использованием ORM, во многих случаях вы будете иметь дело с динамическими прокси, чтобы избежать слишком ранней загрузки объекта из хранилища данных. Эти прокси реализованы как подклассы вашего собственного класса. Это означает, чтоthis.getClass() == o.getClass() вернет false. Например:
Personsaved=newPerson("John Doe"); Longkey= dao.save(saved); dao.flush(); Personretrieved= 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().
Но: вы не можете включить 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() (и изменяют его поведение), нарушит эту спецификацию.
Рассмотрим следующий пример того, что происходит, когда оператор опущен:
classA { int field1;
A(int field1) { this.field1 = field1; }
publicbooleanequals(Object other) { return (other != null && other instanceof A && ((A) other).field1 == field1); } }
classBextendsA { int field2;
B(int field1, int field2) { super(field1); this.field2 = field2; }
publicbooleanequals(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= 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) returnsuper.equals(other); elsereturnfalse;
Который выведет:
a.equals(b) == true; b.equals(a) == true;
Где, если a это не ссылка на B, то это может быть ссылка на класс A (потому что вы расширяете его), в этом случае вы вызываете super.equals()тоже.
В своей книге Эффективное руководство по языку программирования Java (Addison-Wesley, 2001) Джошуа Блох утверждает, что "Просто нет способа расширить инстанцируемый класс и добавить аспект при сохранении контракта equals". Tal не согласен.
Его решение заключается в реализации equals() путем вызова другого несимметричного blindlyEquals() обоими способами. Функция blindlyEquals() переопределяется подклассами, функция equals() наследуется и никогда не переопределяется.