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
| Оператор/метод | Що порівнює? | Для чого потрібен? |
|---|---|---|
|
Посилання (адреси в памʼяті) | Перевірка «той самий обʼєкт?» |
|
Вміст обʼєктів | Рівність за бізнес-логікою |
|
Порядок (менше/рівно/більше) | Сортування, упорядкування |
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 можуть вважати елементи рівними для порядку, але різними за рівністю — це джерело «привидів» і дублікатів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ