JavaRush /Java блог /Random UA /Кава-брейк #168. Навіщо перевизначати методи equals і has...

Кава-брейк #168. Навіщо перевизначати методи equals і hashcode Java?

Стаття з групи Random UA

Навіщо перевизначати методи equals і hashcode Java?

Джерело: Medium Зміст цієї статті присвячено двом тісно пов'язаним між собою методам: equals() та hashcode() . Ви дізнаєтесь, як вони взаємодіють один з одним і як їх правильно перевизначати. Кава-брейк #168.  Навіщо перевизначати методи equals і hashcode Java?  - 1

Чому ми перевизначаємо метод equals()?

У Java ми можемо перевантажувати поведінка таких операторів, як == , += , -+ . Вони працюють відповідно до заданого процесу. Наприклад розглянемо роботу оператора == .

Як працює оператор ==?

Він перевіряє, чи вказують два порівнювані посилання на той самий екземпляр у пам'яті. Оператор == матиме значення true тільки в тому випадку, якщо ці два посилання представляють той самий екземпляр у пам'яті. Давайте поглянемо на приклад коду:
public class Person {
      private Integer age;
      private String name;

      ..getters, setters, constructors
      }
Допустимо, у вашій програмі ви створабо два об'єкти Person у різних місцях і бажаєте їх порівняти.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println( person1 == person2 ); --> will print false!
З точки зору бізнесу ці два об'єкти виглядають однаково, чи не так? Але для JVM вони не збігаються. Оскільки вони обидва створені за допомогою ключового слова new , ці екземпляри розташовані в різних сегментах пам'яті. Тому оператор == поверне false . Але якщо ми не можемо перевизначити оператор == , то як сказати JVM, що ми хочемо, щоб ці два об'єкти оброблялися однаково? Тут у гру входить метод .equals() . Ви можете перевизначити equals() , щоб перевірити, чи деякі об'єкти мають однакові значення для певних полів, щоб вважати їх рівними. Ви можете вибрати, які поля потрібно порівняти. Якщо ми говоримо, що два об'єкти Person будуть однаковими тільки тоді, коли вони мають однаковий вік і те саме ім'я, то в цьому випадку IDE згенерує для автоматичного створення equals() щось таке:
@Override
public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                name.equals(person.name);
    }
Повернемося до нашого попереднього прикладу.
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 ); --> will print false!
System.out.println ( person1.equals(person2) ); --> will print true!
Так, ми не можемо перевантажити оператор == для порівняння об'єктів так, як ми хочемо, але Java дає нам інший спосіб - метод equals() , який ми можемо перевизначити на власний розсуд. Майте на увазі, що якщо ми не надамо нашу версію користувача .equals() (також відому як перевизначення) у нашому класі, то зумовлений .equals() з класу Object і оператор == будуть поводитися однаково. Метод за замовчуванням equals() , успадкований від Object , перевірятиме, чи збігаються обидва порівнювані екземпляри в пам'яті!

Чому ми перевизначаємо метод hashCode()?

Деякі структури даних Java, такі як HashSet і HashMap , зберігають свої елементи на основі хеш-функції, яка застосовується до цих елементів. Хеш-функцією є hashCode() . Якщо ми маємо вибір у перевизначенні методу .equals() , то у нас також має бути вибір у перевизначенні методу hashCode() . На це є причина. Адже реалізація за умовчанням hashCode() успадкована від Object вважає всі об'єкти в пам'яті унікальними! Але повернемося до цих структур хеш-даних. Для цих структур даних існує правило. HashSet не може містити значення, що повторюються, а HashMap не може містити повторювані ключі. HashSet реалізовано за допомогою HashMap таким чином, що кожне значення HashSet зберігається як ключ у HashMap . Як працює HashMap ? HashMap – це власний масив із кількома сегментами. Кожен сегмент має пов'язаний список ( linkedList ). У цьому списку зберігаються наші ключі. HashMap знаходить правильний linkedList для кожного ключа, застосовуючи метод hashCode() , а потім виконує ітерацію по всіх елементах цього linkedList і застосовує метод equals() до кожного з цих елементів, щоб перевірити, чи міститься там цей елемент. Дублікати ключів не допускаються. Кава-брейк #168.  Навіщо перевизначати методи equals і hashcode Java?  - 2Коли ми поміщаємо щось усередину HashMap , то ключ зберігається в одному з цих зв'язаних списків. В якому зв'язаному списку зберігатиметься цей ключ, показує результат методу hashCode() для цього ключа. Тобто, якщо key1.hashCode() в результаті виходить 4, цей key1 буде зберігатися в 4-му сегменті масиву в існуючому там LinkedList . За промовчанням метод hashCode() повертає різні результати для кожного екземпляра. Якщо ми маємо значення за умовчанням equals() , яке поводиться як == , розглядаючи всі екземпляри у пам'яті як різні об'єкти, то проблем буде. Як ви пам'ятаєте, у нашому попередньому прикладі було сказано, що ми хочемо, щоб екземпляри Person вважалися рівними, якщо їх вік та імена збігаються.
Person person1 = new Person("Mike", 34);
    Person person2 = new Person("Mike", 34);
    System.out.println ( person1.equals(person2) );  --> will print true!
Тепер давайте створимо карту (map) для зберігання цих екземплярів у вигляді ключів з певним рядком як парне значення.
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
У класі Person ми не перевизначабо метод hashCode , але ми маємо перевизначений метод equals . Оскільки значення за промовчанням hashCode дає різні результати для різних Java-екземплярів person1.hashCode() і person2.hashCode() , є великі шанси отримати різні результати. Наша карта може закінчуватись різними person у різних зв'язаних списках. Кава-брейк #168.  Навіщо перевизначати методи equals і hashcode Java?  - 3Це суперечить логіці HashMap . Адже HashMap не може мати кілька однакових ключів! Справа в тому, що за умовчанням hashCode() успадкованого від класу Object недостатньо. Навіть по тому, як ми перевизначабо метод equals() класу Person . Ось чому ми повинні перевизначити метод hashCode() після того, як ми визначабо метод equals . Тепер давайте це виправимо. Нам потрібно перевизначити наш метод hashCode() , щоб він враховував самі поля, що й equals() , саме age і name .
public class Person {
      private Integer age;
      private String name;

      ..getters, setters, constructors
@Override
public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                name.equals(person.name);
    }
@Override
public int hashCode() {
        int prime = 31;
        return prime*Objects.hash(name, age);
    }
У методі hashCode() ми використовували просте значення (можна використовувати будь-які інші значення). Тим не менш, пропонується використовувати прості числа, щоб створювати менше проблем. Давайте спробуємо ще раз зберегти ці ключі в нашому HashMap :
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
person1.hashCode() та person2.hashCode() будуть однакові. Допустимо, рівні 0. HashMap перейде в сегмент 0 і в ньому LinkedList збереже person1 як ключ зі значенням "1". У другому випадку, коли HashMap знову перейде до кошика 0, щоб зберегти ключ person2 зі значенням “2”, він побачить, що вже існує інший рівний йому ключ. Таким чином, він перезапише попередній ключ. І в нашому HashMap існуватиме лише ключ person2 . Кава-брейк #168.  Навіщо перевизначати методи equals і hashcode Java?  - 4Так ми дізналися, як працює правило HashMap , яке свідчить, що не можна використовувати кілька однакових ключів! Однак майте на увазі, що нерівні екземпляри можуть мати однаковий хешкод, а однакові екземпляри повинні повертати однаковий хешкод.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ