JavaRush /Курси /JAVA 25 SELF /Контракти equals і hashCode

Контракти equals і hashCode

JAVA 25 SELF
Рівень 29 , Лекція 0
Відкрита

1. Вступ

Як ми порівнюємо обʼєкти

У Java обʼєкти — це не просто дані, у кожного є власна адреса в памʼяті. Оператор == відповідає на запитання «це одна й та сама коробочка?», тобто порівнює посилання (адреси), а не вміст. Метод equals призначений для порівняння саме вмісту. За замовчуванням, якщо його не перевизначити, equals поводиться як ==.

Person p1 = new Person("Іван", 20);
Person p2 = new Person("Іван", 20);

System.out.println(p1 == p2); // false — це різні обʼєкти в памʼяті!

Щоб два різні обʼєкти вважалися рівними за даними (наприклад, збігаються всі значущі поля), необхідно перевизначити equals. І якщо ви плануєте використовувати обʼєкти в хеш-колекціях, обовʼязково разом із ним коректно перевизначайте й hashCode.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @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() {
        return Objects.hash(name, age); // щоб працювало з HashSet/HashMap
    }
}

Таке перевизначення критично важливе для коректної роботи з HashSet/HashMap: без equals і hashCode колекції вважатимуть навіть однакові за даними обʼєкти різними.

Еквівалентність у equals залежить від ваших вимог: можна порівнювати за всіма полями або лише за частиною з них (наприклад, лише електронною адресою у User). Головне — узгодженість і дотримання контракту.

Де це особливо важливо?

  • У колекціях на базі хеш-таблиць: HashSet, HashMap, LinkedHashSet тощо.
  • Під час пошуку й видалення елементів у колекціях: без коректного equals потрібний обʼєкт може не знайтися.
  • У бізнес-логіці: наприклад, два User з однаковим email мають вважатися одним користувачем.

hashCode — навіщо він потрібен?

Хеш-колекції (наприклад, HashSet, HashMap) використовують хеш-таблиці. Метод hashCode обчислює ціле число — «адресу бакета», куди потрапить обʼєкт. Якщо два обʼєкти рівні за equals, їхній hashCode зобовʼязаний збігатися. Порушите це правило — колекції почнуть поводитися непередбачувано.

2. Контракт equals і hashCode

Контракт equals

Основні вимоги до поведінки equals:

  • Рефлексивність: a.equals(a) завжди true.
  • Симетричність: якщо a.equals(b) — true, то й b.equals(a) — true.
  • Транзитивність: якщо a.equals(b) і b.equals(c), то a.equals(c) — також true.
  • Узгодженість: якщо обʼєкти не змінюються, результат викликів стабільний.
  • Порівняння з null: будь-який обʼєкт не дорівнює null.

Контракт hashCode

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

Іншими словами, однаковий hashCode — необхідна, але недостатня умова рівності: однаковий хеш не гарантує рівність за equals.

3. Реалізація equals і hashCode: приклад

Розглянемо клас Person, де рівність визначається за полями name і age.

public class Person {
    private String name;
    private int age;

    // Конструктор, гетери, сетери...

    @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 != null ? name.equals(person.name) : person.name == null);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age; // 31 — поширений вибір простого числа
        return result;
    }
}
  • Спочатку швидкі перевірки: посилання та клас.
  • Потім — порівняння значущих полів.
  • У hashCode використовується просте число 31 для зменшення колізій.

Використання Objects.equals і Objects.hash

Починаючи з Java 7, клас Objects спрощує код і робить його безпечнішим щодо null:

import java.util.Objects;

@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 &&
           Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

4. equals, hashCode і compareTo: як вони повʼязані

Як повʼязані equals і compareTo?

Інтерфейс Comparable задає метод compareTo, який повертає відʼємне/нульове/додатне число для «менше/рівно/більше». Бажано, щоб із a.compareTo(b) == 0 випливало a.equals(b). Зворотне не є обовʼязковим.

Якщо порушити узгодженість (наприклад, compareTo порівнює лише за віком, а equals — за імʼям і віком), то впорядковані колекції на кшталт TreeSet/TreeMap можуть поводитися неочікувано: обʼєкти вважаються «рівними» з точки зору порядку, але не рівними за вмістом.

equals і hashCode в колекціях

  • У HashSet і HashMap операції додавання/пошуку/видалення покладаються на коректну реалізацію equals і hashCode.
  • Без перевизначення ці колекції вважають обʼєкти «різними», навіть якщо їхні дані однакові.

5. Приклади: як це працює в колекціях

HashSet: зберігання унікальних обʼєктів

Set<Person> people = new HashSet<>();
people.add(new Person("Іван", 20));
people.add(new Person("Іван", 20)); // Дублікат

System.out.println(people.size()); // 1, якщо equals/hashCode реалізовано правильно

Без коректного equals/hashCode до набору потраплять обидва обʼєкти.

HashMap: пошук за ключем

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Анна", 25);
Person p2 = new Person("Анна", 25);

map.put(p1, "Користувач 1");
System.out.println(map.get(p2)); // "Користувач 1", якщо equals/hashCode реалізовано правильно

Без контракту колекція поверне null — для неї це «різні» ключі.

6. Найкращі практики: поради щодо реалізації

  • Включайте до equals/hashCode усі поля, що визначають «ідентичність» обʼєкта.
  • Не використовуйте змінні поля (які можуть змінитися після додавання до колекцій) під час обчислення hashCode.
  • Користуйтеся генерацією методів в IDE — менше шансів на помилки.
  • У equals спочатку перевіряйте this == o, потім клас, потім — поля.
  • Для порівняння полів-обʼєктів використовуйте Objects.equals.
  • Для хеш-коду використовуйте Objects.hash або перевірений шаблон із множником 31.

7. Корисні нюанси

Чому не можна використовувати лише hashCode?

Колізії неминучі: різні обʼєкти можуть мати однаковий hashCode. Хеш — лише швидкий орієнтир для бакета; остаточне рішення про рівність приймає equals.

Чи можна не перевизначати equals і hashCode?

Лише якщо ви певні, що обʼєкти ніколи не будуть порівнюватися за вмістом і не стануть ключами/унікальними елементами в колекціях. На практиці це рідкість.

Відмінність між ==, equals і compareTo

Оператор/метод Що порівнює? Для чого потрібен?
==
Посилання (адреси в памʼяті) Перевірка «той самий обʼєкт?»
equals
Вміст обʼєктів Рівність за бізнес-логікою
compareTo
Порядок (менше/рівно/більше) Сортування, упорядкування

8. Типові помилки під час реалізації equals і hashCode

Помилка № 1: перевизначили equals, але забули hashCode. Обʼєкти вважаються рівними, проте потрапляють у різні бакети хеш-таблиці — пошук і видалення працюють некоректно.

Помилка № 2: використовуєте змінні поля в hashCode. Якщо поле зміниться після додавання до колекції, обʼєкт «загубиться»: хеш зміниться, а бакет — ні.

Помилка № 3: порушено симетричність/транзитивність equals. a.equals(b) дає true, а b.equals(a) — false, або порушується транзитивність — колекції починають поводитися непередбачувано.

Помилка № 4: не перевіряєте клас у equals. Порівняння обʼєктів різних класів призводить до некоректних результатів або винятків.

Помилка № 5: порівнюєте рядки й обʼєкти через ==. Оператор == порівнює посилання; використовуйте equals для вмісту.

Помилка № 6: немає узгодженості compareTo та equals. Якщо a.compareTo(b) == 0, але !a.equals(b), колекції TreeSet/TreeMap можуть вважати елементи рівними для порядку, але різними за рівністю — це джерело «привидів» і дублікатів.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ