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

617

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

Мэтт Шеппард
источник

Ответы:

1439

Теория (для языковых юристов и математически склонных):

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 () ключевых объектов, которые вы помещаете в коллекцию, никогда не изменяется, пока объект находится в коллекции. Пуленепробиваемый способ обеспечить это - сделать ваши ключи неизменяемыми, что имеет и другие преимущества .

Antti Sykäri
источник
12
Дополнительное замечание о appendSuper (): вы должны использовать его в hashCode () и equals () тогда и только тогда, когда вы хотите наследовать поведение равенства суперкласса. Например, если вы производите прямо от объекта, нет смысла, потому что все объекты по умолчанию различны.
Антти Киссаниеми
312
Вы можете заставить Eclipse сгенерировать для вас два метода: Source> Generate hashCode () и equals ().
Рок Стрниша
27
То же самое относится и к Netbeans: developmentality.wordpress.com/2010/08/24/…
Seinecle
6
@Darthenius Eclipse, сгенерированный equals, использует getClass (), что может вызвать проблемы в некоторых случаях (см. Пункт «Эффективная Java» 8)
AndroidGecko
7
Первая проверка на нулевое значение не обязательна, учитывая тот факт, что instanceofвозвращается false, если ее первый операнд имеет значение null (снова эффективная Java).
Изабан
295

Есть некоторые проблемы, на которые стоит обратить внимание, если вы имеете дело с классами, которые сохраняются с использованием 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()принудительно загружается и возвращает «Джон Доу». По моему опыту, это всплывает чаще в 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()

Йоханнес Бродуолл
источник
2
@ Йоханнес Бродволл: я не понимаю Saving an object will change it's state! hashCodeдолжен вернуться int, так как вы будете использовать getName()? Можете ли вы привести пример для вашегоhashCode
jimmybondy
@jimmybondy: getName вернет объект String, который также имеет хэш-код, который можно использовать
mateusz.fiolka
85

Разъяснение по поводу obj.getClass() != getClass().

Это утверждение является результатом equals()недружественного наследования. JLS (спецификация языка Java) указывает, что if A.equals(B) == truethen 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() тоже вызываете .

Ран Бирон
источник
2
Вы можете сделать симметрии равными таким образом (если сравнивать объект суперкласса с объектом подкласса, всегда используйте равенства подкласса) if (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) вернуть obj.equals (это);
Пиентагия
5
@pihentagy - тогда я получу переполнение стека, когда класс реализации не переопределяет метод equals. не весело.
Ран Бирон
2
Вы не получите переполнение стека. Если метод equals не переопределен, вы снова вызовете тот же код, но условие рекурсии всегда будет ложным!
Джейкоб Райхл
@pihentagy: Как это ведет себя, если есть два разных производных класса? Если a ThingWithOptionSetAможет быть равным при Thingусловии, что все дополнительные параметры имеют значения по умолчанию, и аналогично для a ThingWithOptionSetB, то должна быть возможность ThingWithOptionSetAсравнения a равным a, ThingWithOptionSetBтолько если все неосновные свойства обоих объектов соответствуют их значениям по умолчанию, но Я не вижу, как вы проверяете это.
суперкат
7
Проблема с этим в том, что он нарушает транзитивность. Если вы добавите B b2 = new B(1,99), то b.equals(a) == trueи a.equals(b2) == trueно b.equals(b2) == false.
Никгрим
46

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

Кевин Вонг
источник
10
Взгляните на метод canEqual, описанный здесь - тот же принцип заставляет работать оба решения, но с canEqual вы не сравниваете одни и те же поля дважды (выше, px == this.x будет тестироваться в обоих направлениях): artima.com /lejava/articles/equality.html
Blaisorblade
2
В любом случае, я не думаю, что это хорошая идея. Это делает контракт 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, но гибко в том смысле, что возвращает ложь только в том случае, если производный класс (ы) пытается изменить значение equals. Это правильно?
Александр Дубинский
31

Все еще поражен тем, что никто не рекомендовал библиотеку гуавы для этого.

 //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());
    }
Евгений
источник
23
java.util.Objects.hash () и java.util.Objects.equals () являются частью Java 7 (выпущенной в 2011 году), поэтому для этого вам не нужен Guava.
Герман
1
конечно, но вам следует избегать этого, поскольку Oracle больше не предоставляет общедоступные обновления для Java 6 (это имело место с февраля 2013 года).
Герман
6
Для произнесения thisв this.getDate()средстве ничего (кроме беспорядка)
Steve Kuo
1
Ваше выражение «не InstanceOf» необходим дополнительный кронштейн: if (!(otherObject instanceof DateAndPattern)) {. Согласитесь с Эрнаном и Стивом Куо (хотя это вопрос личных предпочтений), но, тем не менее, +1.
Амос М. Карпентер
26

В суперклассе есть два метода как 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 (). Но если я кодирую с большим количеством переменных, мой код компилируется. Почему это не законно?
Adryr83
19

Есть несколько способов выполнить проверку на равенство классов перед проверкой равенства членов, и я думаю, что оба полезны в правильных обстоятельствах.

  1. Используйте instanceofоператора.
  2. Использование this.getClass().equals(that.getClass()).

Я использую # 1 в finalреализации equals или при реализации интерфейса, который предписывает алгоритм для equals (например, 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
+1 за это. Ни getClass (), ни instanceof не являются панацеей, и это хорошее объяснение того, как подходить к обоим. Не думайте, что есть причина не делать this.getClass () == that.getClass () вместо использования equals ().
Пол Кантрелл
Есть одна проблема с этим. Анонимные классы, которые не добавляют никаких аспектов и не переопределяют метод equals, не пройдут проверку getClass, даже если они должны быть равны.
Steinybot
@ Стейни Мне не понятно, что объекты разных типов должны быть равны; Я думаю о различных реализациях интерфейса как об общем анонимном классе. Можете ли вы привести пример в поддержку вашей предпосылки?
Эриксон
MyClass a = новый MyClass (123); MyClass b = new MyClass (123) {// Переопределить некоторый метод}; // a.equals (b) ложно при использовании this.getClass (). equals (that.getClass ())
steinybot
1
@ Стейни Верно. Как и в большинстве случаев, особенно если метод переопределен, а не добавлен. Рассмотрим мой пример выше. Если это не так final, и compareTo()метод был переопределен для изменения порядка сортировки, экземпляры подкласса и суперкласса не должны рассматриваться как равные. Когда эти объекты использовались вместе в дереве, ключи, которые были «равны» в соответствии с instanceofреализацией, могли бы быть недоступны для поиска.
Эриксон
16

Для равных, заглянуть в секреты Равных по Angelika Langer . Я люблю это очень сильно. Она также является отличным FAQ по Generics в Java . Посмотрите ее другие статьи здесь (прокрутите вниз до «Базовая Java»), где она также продолжает Часть 2 и «Сравнение смешанных типов». Приятного чтения!

Йоханнес Шауб - Литб
источник
11

Метод 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
Рохан Камат
источник
7

По логике мы имеем:

a.getClass().equals(b.getClass()) && a.equals(b)a.hashCode() == b.hashCode()

Но не наоборот!

Khaled.K
источник
6

Я обнаружил одну ошибку: два объекта содержат ссылки друг на друга (один пример - отношения родитель / потомок с удобным методом для родителя, чтобы получить всех детей).
Такого рода вещи довольно распространены, например, при отображении Hibernate.

Если вы включите оба конца отношения в свой hashCode или в тесты equals, можно попасть в рекурсивный цикл, который заканчивается в исключении StackOverflowException.
Самое простое решение - не включать коллекцию getChildren в методы.

Даррен Гривз
источник
5
Я думаю, что основная теория здесь заключается в различении атрибутов , агрегатов и ассоциаций объекта. В asssociations не должны участвовать в equals(). Если бы сумасшедший ученый создал меня дубликат, мы были бы эквивалентны. Но у нас не было бы того же отца.
Raedwald