Які питання слід враховувати при переосмисленні рівних та хеш-кодів на Java?


617

Які проблеми / підводні камені слід враховувати при переборі equalsта hashCode?

Відповіді:


1439

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

equals()( javadoc ) має визначати відношення еквівалентності (воно повинно бути рефлексивним , симетричним та транзитивним ). Крім того, він повинен бути послідовним (якщо об'єкти не модифіковані, то він повинен постійно повертати те саме значення). Крім того, o.equals(null)завжди потрібно повертати помилкове.

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


12
Додатковий пункт про appendSuper (): ви повинні використовувати його в hashCode () і дорівнює (), якщо і тільки якщо ви хочете успадкувати поведінку рівності надкласу. Наприклад, якщо ви отримуєте прямо з Object, немає сенсу, оскільки всі об'єкти за замовчуванням відрізняються.
Антті Кіссаніємі

312
Ви можете отримати Eclipse для створення двох методів для вас: Джерело> Створити hashCode () та дорівнює ().
Рок Стрішні

27
Те ж саме і з Netbeans: developmentmentality.wordpress.com/2010/08/24/…
seinecle

6
@Darthenius Eclipse, що генерується, використовує getClass (), який у деяких випадках може спричинити проблеми (див. Ефективний пункт 8 Java)
AndroidGecko

7
Перша перевірка нуля не потрібна, враховуючи той факт, що instanceofповертає значення false, якщо його перший операнд є null (знову ефективний Java).
izaban

295

Є деякі проблеми, які варто помітити, якщо ви маєте справу з класами, які зберігаються за допомогою Mapper-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поле для утримання ключа об'єкта. Це поле буде автоматично оновлено при першому збереженні об'єкта. Не використовуйте поле ідентифікатора в hashCode(). Але ви можете використовувати його в equals().

Я часто використовую схему

if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

Але: ви не можете включити getId()в hashCode(). Якщо ви зробите це, коли об’єкт зберігається, його hashCodeзміниться. Якщо об’єкт знаходиться в a HashSet, ви "ніколи" не знайдете його знову.

У моєму Personприкладі я, мабуть, використовував би getName()для hashCodeі getId()плюс getName()(лише для параної) equals(). Це добре, якщо є певний ризик "зіткнення" hashCode(), але ніколи не гаразд equals().

hashCode() слід використовувати підменю властивостей, що не змінюються equals()


2
@Johannes Brodwall: я не розумію 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) вказує , що якщо 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() теж .


2
Ви можете зробити рівними симетричними таким чином (якщо порівнювати об’єкт надкласу з об'єктом підкласу, завжди використовуйте рівняння підкласу), якщо (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) повернути obj.equals (це);
pihentagy

5
@pihentagy - тоді я отримав би stackoverflow, коли клас реалізації не перекриє метод рівних. не смішно.
Ран Бірон

2
Ви не отримаєте потокового потоку. Якщо метод equals не буде відмінено, ви знову наберете той же код, але умова для рекурсії завжди буде помилковою!
Яків Райхле

@pihentagy: Як це поводиться, якщо є два різні похідні класи? Якщо a ThingWithOptionSetAможе дорівнювати за Thingумови, що всі додаткові параметри мають значення за замовчуванням, і аналогічно для a ThingWithOptionSetB, тоді a має бути можливим ThingWithOptionSetAпорівняння, рівне a, ThingWithOptionSetBлише якщо всі неосновні властивості обох об'єктів відповідають їх за замовчуванням, але Я не бачу, як ви тестуєте на це.
supercat

7
Проблема в цьому полягає в тому, що він порушує транзитивність. Якщо додати B b2 = new B(1,99), то b.equals(a) == trueі a.equals(b2) == trueале b.equals(b2) == false.
nickgrim

46

Для здійснення сприйняття успадкованості ознайомтеся з рішенням Тала Коена: Як я правильно реалізую метод рівних ()?

Підсумок:

У своїй книзі « Ефективний посібник з мовного програмування Java» (Аддісон-Уеслі, 2001) Джошуа Блох стверджує, що «просто немає способу розширити клас миттєвих дій і додати аспект при збереженні контракту на рівні». Тал не погоджується.

Його рішення полягає у здійсненні рівняння () шляхом виклику іншого несиметричного сліпого рівняння () обома способами. 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);
    }
}

Зауважте, що для задоволення принципу заміни Ліскова рівні () повинні працювати в ієрархіях успадкування .


10
Погляньте на пояснення тут методу canEqual - той самий принцип змушує обидва рішення працювати, але з canEqual ви не порівнюєте однакові поля двічі (вище, px == this.x буде перевірено в обох напрямках): artima.com /lejava/articles/equality.html
Blaisorblade

2
У будь-якому випадку, я не думаю, що це гарна ідея. Це робить контракт рівних зайвим заплутаним - той, хто приймає два параметри точки, a і b, повинен усвідомлювати можливість того, що a.getX () == b.getX () і a.getY () == b.getY () може бути істинним, але a.equals (b) і b.equals (a) обидва є помилковими (якщо лише одна є ColorPoint).
Кевін

В основному це схоже if (this.getClass() != o.getClass()) return false, але гнучко в тому, що воно повертає помилкове лише у тому випадку, коли похідний клас (и) заважають модифікувати рівняння. Це так?
Олександр Дубінський

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()значенні нічого (крім безладу)
Стів Куо

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: він не є законним, якщо він використовує більше змінних, ніж рівний (). Але якщо я кодую з більшою кількістю змінних, мій код складається. Чому це не законно?
Adryr83

19

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

  1. Скористайтеся instanceofоператором.
  2. Використовуйте this.getClass().equals(that.getClass()).

Я використовую №1 в finalрівній реалізації або при реалізації інтерфейсу, який прописує алгоритм для рівних (як java.utilінтерфейси колекції - правильний спосіб перевірити за допомогою (obj instanceof Set)чи будь-якого інтерфейсу, який ви реалізуєте). Це, як правило, поганий вибір, коли рівняння можна перекрити, оскільки це порушує властивість симетрії.

Варіант №2 дозволяє безпечно розширювати клас, не змінюючи рівних чи порушуючи симетрію.

Якщо ваш клас також є Comparable, equalsі compareToметоди також повинні бути послідовними. Ось шаблон для методу рівних у 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 не є панацеєю, і це хороше пояснення, як підходити до обох. Не думайте, що немає причин не робити цього.getClass () == that.getClass (), а не використовувати рівняння ().
Пол Кантрелл

У цьому є одна проблема. Анонімні класи, які не додають жодних аспектів і не замінюють метод рівних, не зможуть перевірити getClass, хоча вони повинні бути рівними.
steinybot

@Steiny Мені не ясно, що об'єкти різних типів повинні бути рівними; Я думаю про різні реалізації інтерфейсу як загального анонімного класу. Чи можете ви навести приклад на підтримку вашого приміщення?
еріксон

MyClass a = новий MyClass (123); MyClass b = новий MyClass (123) {// Переозначення деякого методу}; // a.equals (b) помилково при використанні this.getClass (). дорівнює (that.getClass ())
steinybot

1
@Steiny Правильно. Як і в більшості випадків, особливо якщо метод замінено, а не додано. Розглянемо мій приклад вище. Якщо цього не було final, а compareTo()метод був скасований для зміни порядку сортування, екземпляри підкласу та надкласу не повинні вважатися рівними. Коли ці об'єкти використовуються разом у дереві, ключі, які були «рівні» відповідно до instanceofреалізації, можливо, не підлягають обробці.
erickson

16

Для рівних, заглянути в секрети Рівних по Angelika Langer . Мені це дуже подобається. Вона також чудовий FAQ про дженерики на Java . Перегляньте її інші статті тут (прокрутіть униз до "Основна Java"), де вона також продовжується з частиною 2 та "зіставленням змішаного типу". Весело читаючи їх!


11

метод рівності () використовується для визначення рівності двох об'єктів.

як int значення 10 завжди дорівнює 10. Але метод дорівнює () - це рівність двох об'єктів. Коли ми говоримо об'єкт, він матиме властивості. Для вирішення питання рівності враховуються ці властивості. Не обов'язково, щоб усі властивості враховувались для визначення рівності, а щодо визначення класу та контексту можна визначити. Тоді метод equals () може бути замінений.

ми завжди повинні переосмислювати метод hashCode (), коли ми переосмислюємо метод equals (). Якщо ні, що буде? Якщо ми будемо використовувати хештелі у своєму додатку, він не буде вести себе так, як очікувалося. Оскільки хеш-код використовується для визначення рівності збережених значень, він не поверне потрібне відповідне значення для ключа.

Дана реалізація за замовчуванням метод 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()

Але не навпаки!


6

Один я знайшов те, де два об’єкти містять посилання один на одного (один приклад - це стосунки батька / дитини з зручним методом для батьків, щоб отримати всіх дітей).
Такі речі є досить поширеними, наприклад, коли робиться спляча сплячка.

Якщо ви включите обидва кінці відносини у свій хеш-код або дорівнює тестам, можливо потрапити в рекурсивний цикл, який закінчується StackOverflowException.
Найпростіше рішення - не включати колекцію getChildren у методи.


5
Я думаю, що основна теорія тут полягає в тому, щоб розрізняти атрибути , сукупності та асоціативи об'єкта. У асоціаціях не повинно брати участь equals(). Якби божевільний вчений створив мені дублікат, ми були б рівнозначними. Але у нас не було б того самого батька.
Raedwald
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.