Какие проблемы / подводные камни следует учитывать при отмене equals и hashCode?




equals() (javadoc) должен определять отношение эквивалентности (это должно быть рефлексивный, симметричный и переходный). Кроме того, он должен быть последовательный (если объекты не изменены, он должен продолжать возвращать то же значение). Кроме того, o.equals(null) всегда должен возвращать false.
hashCode() (javadoc) также должен быть последовательный (если объект не изменен с точки зрения equals(), он должен продолжать возвращать то же значение).
связь между двумя методами:
Whenever
a.equals(b), thena.hashCode()must be same asb.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, Хеш-таблица или WeakHashMap, убедитесь, что hashCode () ключевых объектов, которые вы помещаете в коллекцию, никогда не изменяется, пока объект находится в коллекции. Самый надежный способ гарантировать это - сделать ваши ключи неизменяемыми, который имеет и другие преимущества.
Особое внимание следует уделять переходным полям.
Отличное обсуждение этого вопроса в книге «Эффективная Java».
Вы можете заставить Eclipse сгенерировать для вас два метода: Source> Generate hashCode () и equals ().
имеет значение, если мы все случайно выбрали одни и те же простые числа?
То же самое и с Netbeans: developmentality.wordpress.com/2010/08/24/…
@ Дартениус: твой самый полезный!
@bestsss: Общее правило, которому следует следовать, заключается в том, что в любой момент времени, когда изменяемый аспект состояния объекта может измениться, объект должен иметь одного четко определенного владельца. Если объект переопределяет equals, чтобы зависеть от некоторого изменяемого состояния, любой словарь, в котором он был сохранен, будет «владеть» этим аспектом его состояния. Поскольку любой контекст выполнения, который пытается изменить это состояние, должен владеть им, это будет означать, что объект должен быть сначала удален из Словаря. В противном случае, если кто-то хочет изменить это правило, переопределите hashCode, чтобы он зависел только от неизменяемых свойств.
@Darthenius или ярлык: ALTShiftS, а затем h.
@Darthenius Eclipse сгенерированный equals использует getClass (), что в некоторых случаях может вызвать проблемы (см. Эффективный элемент Java 8)
Первая проверка на null не требуется, учитывая тот факт, что instanceof возвращает false, если его первый операнд равен нулю (снова эффективная Java).
@AndroidGecko, может быть, это было раньше. Eclipse Kepler не генерировал код, содержащий getClass ().
Android Studio также может сгенерировать для вас equals () и hashCode ().
Математический термин - «отношение эквивалентности», а не «отношение равенства».
equals используется для добавления / удаления элементов из коллекций, таких как CopyOnWriteArraySet, HashSet, если hashCode равен для двух разных объектов и т. д. equals должен быть симметричным, т.е. если B.equals (C) возвращает true, то C.equals (B) должен возвращать тот же результат. В противном случае ваше добавление / удаление этих XXXSet приведет к путанице. stackoverflow.com/questions/24920563/…
Вы пересчитываете хэш-код каждый раз, когда вызывается hashCode (). Если объект неизменяемый, кешируйте вычисленный хэш-код в экземпляре. Обратитесь к hashCode impl в классе String.
при использовании instanceof вместо getClass () симметрия не всегда сохраняется
Этот ответ - только половина дела. Здесь не упоминается ни одна из ловушек при переопределении метода equals в подклассе. На самом деле это заблуждение, поскольку простое добавление appendSuper в производный класс решит проблему. Это почти наверняка нарушит симметрию или транзитивность.
К сожалению, на самом деле никакой ответ не решает проблему, кроме, возможно, stackoverflow.com/a/55736/367796. Даже тогда я был бы более склонен пойти с комментарием @Blaisorblade, чтобы использовать подход canEqual stackoverflow.com/questions/27581/…
@ AnttiSykäri вы также можете добавить пример, например - когда мы хотим вставить определенный пользователем объект в SET, тогда нам нужно реализовать метод hashCode и equals, чтобы он добавлял только уникальный объект в SET
В IntelliJ также есть мастер: щелкните правой кнопкой мыши имя класса> Создать .. (или ⌘N)> equals () и hashCode ().
почему instanceof вместо if ((obj == null) || (obj.getClass() != this.getClass()))? instanceof может создать проблему для родителя и ребенка.
удивлены тем фактом, что enum не является частью ответа, не должны ли мы беспокоиться о таких полях?
«Используйте тот же набор полей, который вы используете для вычисления equals () для вычисления hashCode ()» Меня учили, что хэш-код был «быстрым поиском», просто чтобы сузить объем того, что затем требует полной проверки «равно» . Поэтому в большинстве ситуаций я бы просто выбрал одно поле (например, «возраст» в этом примере), а не всегда включал все поля. Это плохая практика?
Пояснение по поводу 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 и принцип подстановки Лискова вызывает 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()тоже.
Таким образом, вы можете сделать равные симметричными (при сравнении объекта суперкласса с объектом подкласса всегда используйте равные значения подкласса) if (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) return obj.equals (this);
@pihentagy - тогда я бы получил stackoverflow, когда класс реализации не переопределяет метод equals. не смешно.
Вы не получите stackoverflow. Если метод equals не переопределен, вы снова вызовете тот же код, но условие рекурсии всегда будет ложным!
@pihentagy: Как это ведет себя, если есть два разных производных класса? Если ThingWithOptionSetA может быть равен Thing при условии, что все дополнительные параметры имеют значения по умолчанию, а также для ThingWithOptionSetB, тогда должно быть возможно, чтобы ThingWithOptionSetA был равен ThingWithOptionSetB, только если все небазовые свойства обоих объектов соответствуют их значениям по умолчанию, но я не понимаю, как вы это проверяете.
Проблема в том, что он нарушает транзитивность. Если добавить B b2 = new B(1,99), то b.equals(a) == true и a.equals(b2) == true, но b.equals(b2) == false.
Есть несколько способов проверить равенство классов перед проверкой равенства членов, и я думаю, что оба они полезны в определенных обстоятельствах.
instanceof.this.getClass().equals(that.getClass()).Я использую № 1 в реализации равенства final или при реализации интерфейса, который предписывает алгоритм для равенства (например, интерфейсы сбора java.util - правильный способ проверки с помощью (obj instanceof Set) или любого другого интерфейса, который вы реализуете). Обычно плохой выбор, когда можно переопределить равенство, потому что это нарушает свойство симметрии.
Вариант № 2 позволяет безопасно расширять класс без переопределения равенства или нарушения симметрии.
Если ваш класс также Comparable, методы equals и compareTo также должны быть согласованы. Вот шаблон для метода equals в классе Comparable:
final class MyClass implements Comparable<MyClass>
{
…
@Override
public boolean equals(Object obj)
{
/* If compareTo and equals aren't final, we should check with getClass instead. */
if (!(obj instanceof MyClass))
return false;
return compareTo((MyClass) obj) == 0;
}
}
+1 за это. Ни getClass (), ни instanceof не являются панацеей, и это хорошее объяснение того, как подойти к обоим. Не думайте, что есть причина не делать this.getClass () == that.getClass () вместо использования equals ().
С этим есть одна проблема. Анонимные классы, которые не добавляют никаких аспектов и не переопределяют метод equals, не пройдут проверку getClass, даже если они должны быть равны.
@Steiny Мне непонятно, что объекты разных типов должны быть равны; Я думаю о различных реализациях интерфейса как об общем анонимном классе. Можете ли вы привести пример, подтверждающий вашу предпосылку?
MyClass a = новый MyClass (123); MyClass b = new MyClass (123) {// Переопределить какой-либо метод}; // a.equals (b) ложно при использовании this.getClass (). equals (that.getClass ())
@Steiny Верно. Как и должно быть в большинстве случаев, особенно если метод переопределяется, а не добавляется. Рассмотрим мой пример выше. Если это не final, и метод compareTo() был переопределен для изменения порядка сортировки, экземпляры подкласса и суперкласса не должны считаться равными. Когда эти объекты использовались вместе в дереве, ключи, которые были «равны» согласно реализации instanceof, могли быть недоступны.
Я обнаружил одну ловушку, когда два объекта содержат ссылки друг на друга (один из примеров - отношения родитель / потомок с помощью вспомогательного метода для родительского элемента для получения всех потомков). Подобные вещи довольно распространены, например, при отображении Hibernate.
Если вы включите оба конца отношения в свой хэш-код или тесты на равенство, можно попасть в рекурсивный цикл, который заканчивается исключением StackOverflowException. Самое простое решение - не включать коллекцию getChildren в методы.
Я думаю, что основная теория здесь состоит в том, чтобы различать атрибуты, агрегаты и ассоциаты объекта. ассоциации не должен участвовать в equals(). Если бы сумасшедший ученый создал мою копию, мы были бы эквивалентны. Но у нас не было бы одного отца.
Для реализации, удобной для наследования, ознакомьтесь с решением Тала Коэна, Как правильно реализовать метод equals ()?
Резюме:
В своей книге Руководство по эффективному языку программирования Java (Addison-Wesley, 2001) Джошуа Блох утверждает, что «просто невозможно расширить экземпляр класса и добавить аспект при сохранении контракта равенства». Таль не согласен.
Его решение - реализовать 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 () должен работать в иерархиях наследования, если Принцип замены Лискова должно быть удовлетворено.
Взгляните на метод canEqual, описанный здесь - тот же принцип заставляет оба решения работать, но с canEqual вы не сравниваете одни и те же поля дважды (выше p.x == this.x будет проверяться в обоих направлениях): artima.com/lejava/articles/equality.html
В любом случае, я не думаю, что это хорошая идея. Это делает контракт Equals излишне запутанным - тот, кто принимает два параметра Point, a и b, должен осознавать возможность того, что a.getX () == b.getX () и a.getY () == b.getY () может быть истинным, но a.equals (b) и b.equals (a) оба будут ложными (если только один из них является ColorPoint).
По сути, это похоже на if (this.getClass() != o.getClass()) return false, но гибко в том, что он возвращает false только в том случае, если производный класс (классы) потрудится изменить equals. Это правильно?
Есть некоторые проблемы, на которые стоит обратить внимание, если вы имеете дело с классами, которые сохраняются с помощью Object-Relationship Mapper (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 изменится. Если объект находится в HashSet, вы «никогда» его больше не найдете.
В моем примере с Person я, вероятно, использовал бы getName() для hashCode и getId() плюс getName() (просто для паранойи) для equals(). Это нормально, если есть некоторый риск «коллизий» для hashCode(), но никогда не подходит для equals().
hashCode() должен использовать неизменяемое подмножество свойств из equals().
@ Йоханнес Бродвалл: я не понимаю Saving an object will change it's state! hashCode должен возвращать int, так как вы будете использовать getName()? Можете привести пример для вашего hashCode
@jimmybondy: getName вернет объект String, который также имеет хэш-код, который можно использовать
Для равных загляните в Секреты равных by Анжелика Лангер. Я люблю это очень сильно. Еще она отличный FAQ про Дженерики в Java. Просмотрите другие ее статьи здесь (прокрутите вниз до «Core Java»), где она также продолжает часть 2 и «сравнение смешанных типов». Получайте удовольствие от их чтения!
Все еще удивлен, что никто не рекомендовал для этого библиотеку guava.
//Sample taken from a current working project of mine just to illustrate the idea
@Override
public int hashCode(){
return Objects.hashCode(this.getDate(), this.datePattern);
}
@Override
public boolean equals(Object obj){
if ( ! obj instanceof DateAndPattern ) {
return false;
}
return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
&& Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
}
java.util.Objects.hash () и java.util.Objects.equals () являются частью Java 7 (выпущенной в 2011 году), поэтому вам не нужна Guava для этого.
конечно, но вам следует избегать этого, поскольку Oracle больше не предоставляет публичные обновления для Java 6 (так было с февраля 2013 года).
Ваш this в this.getDate() ничего не значит (кроме беспорядка)
Для вашего выражения not instanceof нужна дополнительная скобка: if (!(otherObject instanceof DateAndPattern)) {. Согласен с Эрнаном и Стивом Куо (хотя это вопрос личных предпочтений), но тем не менее +1.
По логике имеем:
a.getClass().equals(b.getClass()) && a.equals(b) ⇒ a.hashCode() == b.hashCode()
А вот нет наоборот!
Метод equals () используется для определения равенства двух объектов.
поскольку int значение 10 всегда равно 10. Но этот метод equals () касается равенства двух объектов. Когда мы говорим «объект», у него будут свойства. Эти свойства учитываются при принятии решения о равенстве. Необязательно, чтобы все свойства принимались во внимание для определения равенства, и это может быть принято в отношении определения класса и контекста. Затем можно переопределить метод equals ().
мы всегда должны переопределять метод hashCode () всякий раз, когда мы переопределяем метод equals (). Если нет, что будет? Если мы используем хэш-таблицы в нашем приложении, оно не будет вести себя должным образом. Поскольку hashCode используется для определения равенства сохраненных значений, он не будет возвращать правильное соответствующее значение для ключа.
Данная реализация по умолчанию - это метод hashCode () в классе Object, который использует внутренний адрес объекта, преобразует его в целое число и возвращает его.
public class Tiger {
private String color;
private String stripePattern;
private int height;
@Override
public boolean equals(Object object) {
boolean result = false;
if (object == null || object.getClass() != getClass()) {
result = false;
} else {
Tiger tiger = (Tiger) object;
if (this.color == tiger.getColor()
&& this.stripePattern == tiger.getStripePattern()) {
result = true;
}
}
return result;
}
// just omitted null checks
@Override
public int hashCode() {
int hash = 3;
hash = 7 * hash + this.color.hashCode();
hash = 7 * hash + this.stripePattern.hashCode();
return hash;
}
public static void main(String args[]) {
Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
Tiger siberianTiger = new Tiger("White", "Sparse", 4);
System.out.println("bengalTiger1 and bengalTiger2: "
+ bengalTiger1.equals(bengalTiger2));
System.out.println("bengalTiger1 and siberianTiger: "
+ bengalTiger1.equals(siberianTiger));
System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
System.out.println("siberianTiger hashCode: "
+ siberianTiger.hashCode());
}
public String getColor() {
return color;
}
public String getStripePattern() {
return stripePattern;
}
public Tiger(String color, String stripePattern, int height) {
this.color = color;
this.stripePattern = stripePattern;
this.height = height;
}
}
Пример вывода кода:
bengalTiger1 and bengalTiger2: true
bengalTiger1 and siberianTiger: false
bengalTiger1 hashCode: 1398212510
bengalTiger2 hashCode: 1398212510
siberianTiger hashCode: –1227465966
В суперклассе java.lang.Object есть два метода. Нам нужно переопределить их на настраиваемый объект.
public boolean equals(Object obj)
public int hashCode()
Равные объекты должны генерировать один и тот же хэш-код, пока они равны, однако неравные объекты не должны создавать разные хэш-коды.
public class Test
{
private int num;
private String data;
public boolean equals(Object obj)
{
if (this == obj)
return true;
if ((obj == null) || (obj.getClass() != this.getClass()))
return false;
// object must be Test at this point
Test test = (Test)obj;
return num == test.num &&
(data == test.data || (data != null && data.equals(test.data)));
}
public int hashCode()
{
int hash = 7;
hash = 31 * hash + num;
hash = 31 * hash + (null == data ? 0 : data.hashCode());
return hash;
}
// other methods
}
Если вы хотите получить больше, проверьте эту ссылку как http://www.javaranch.com/journal/2002/10/equalhash.html
Это еще один пример, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html
Радоваться, веселиться! @. @
Извините, но я не понимаю этого утверждения о методе hashCode: это незаконно, если он использует больше переменных, чем equals (). Но если я кодирую с большим количеством переменных, мой код компилируется. Почему это не законно?
Дополнительный момент о appendSuper (): вы должны использовать его в hashCode () и equals () тогда и только тогда, когда вы хотите унаследовать поведение равенства суперкласса. Например, если вы производите прямо от Object, в этом нет смысла, потому что все объекты по умолчанию различны.