Що таке контракт equals() і hashCode()?
Метод equals() повинен поводитися як математичне поняття 'рівності':
🟢 Junior Level
Контракт equals() і hashCode() — це набір правил, яких повинні дотримуватися методи equals() і hashCode() в Java. Якщо їх порушити, колекції на кшталт HashMap і HashSet працюватимуть неправильно.
Два простих правила:
- Якщо два об’єкти рівні за
equals(), їхhashCode()обов’язаний бути однаковим - Якщо
hashCode()однаковий — об’єкти не обов’язково рівні (це називається колізія)
Приклад правильного контракту:
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person p = (Person) o;
return age == p.age && name.equals(p.name);
}
@Override
public int hashCode() {
return 31 * name.hashCode() + age;
}
}
Головне правило: якщо об’єкт використовуватиметься як ключ в HashMap/HashSet — перевизначайте обидва методи. Виняток: якщо ви НАВМИСНЕ хочете identity-поведінку (наприклад, для weak references).
🟡 Middle Level
Контракт equals()
Метод equals() повинен поводитися як математичне поняття ‘рівності’:
- Рефлексивність: об’єкт рівний самому собі:
a.equals(a) = true - Симетричність: якщо A дорівнює B, то B дорівнює A
- Транзитивність: якщо A=B і B=C, то A=C
| Властивість | Опис |
|---|---|
| Рефлексивність | x.equals(x) завжди true |
| Симетричність | Якщо x.equals(y), то y.equals(x) |
| Транзитивність | Якщо x.equals(y) і y.equals(z), то x.equals(z) |
| Консистентність | Повторні виклики дають один результат |
| Порівняння з null | x.equals(null) завжди false |
Контракт hashCode()
- Консистентність: значення не змінюється, якщо не змінюються поля з
equals() - Обов’язковий зв’язок:
x.equals(y)→x.hashCode() == y.hashCode() - Необов’язковий зв’язок: однаковий hashCode ≠ рівні об’єкти
Як правильно реалізовувати
Алгоритм Джошуа Блоха:
int result = 17;
result = 31 * result + field1;
result = 31 * result + (field2 != null ? field2.hashCode() : 0);
return result;
Чому 17 і 31: 17 — початкове ‘зерно’ (prime seed), щоб хеш перших полів не був нульовим. 31 — непарне просте число: множення на 31 = зсув вліво на 5 мінус 1 (31 * i = (i << 5) - i), що ефективно на CPU. Переповнення int — не баг, а норма: хеш ЗАВЖДИ wrap-around, це закладено в математику.
Типові помилки
- Перевизначити equals, забути hashCode — дублікати в HashSet
- Використовувати mutable поля — ключ “ламається” після зміни
- Класичний баг JDK:
TimestampрозширюєDate.date.equals(timestamp)може повернути true, алеtimestamp.equals(date)— false, бо Timestamp перевіряє наносекунди, яких немає у Date. Порушена симетричність!
Коли НЕ використовувати кастомні equals/hashCode
Не використовуйте equals/hashCode для об’єктів з business-ідентичністю, яка може змінюватися. Наприклад, entity з auto-generated ID: до збереження id=null, після — id=42. Два об’єкти з id=null ‘рівні’, але після збереження — вже ні. Це порушує консистентність в HashMap.
🔴 Senior Level
Internal Implementation в HashMap
HashMap використовує обидва методи для O(1) пошуку:
hashCode()→ знаходить бакет:index = (n-1) & hash(key)equals()→ знаходить ключ всередині бакета
Наслідки порушення:
- Рівні об’єкти з різними hashCode потрапляють в різні бакети →
get()повернеnull HashSetдопустить дублікати → порушення бізнес-логікиremove()не знайде об’єкт → витік пам’яті
Порушення симетричності при наслідуванні
class Point2D { int x, y; }
class Point3D extends Point2D { int z; }
Якщо Point3D.equals() перевіряє всі 3 поля, а Point2D.equals() тільки 2:
new Point2D(1,1).equals(new Point3D(1,1,0))→true(Point2D не бачить z)new Point3D(1,1,0).equals(new Point2D(1,1))→false(Point3D не instanceof Point3D)- Порушення симетричності!
Рішення: використовувати getClass() замість instanceof, або композицію замість наслідування.
Edge Cases
- URL.equals() — робить DNS-запит, порушує консистентність при збої мережі
- Float.NaN —
Float.NaN.equals(Float.NaN)= true, алеFloat.NaN == Float.NaN= false - Масиви —
equals()порівнює посилання, не вміст; використовуйтеArrays.equals()
Performance Implications
- Поганий hashCode → колізії → деградація HashMap до O(log n) або O(n)
- Важкий equals() (порівняння великих рядків/байтових масивів) → велика константа в O(1)
- Кешування hashCode (як у
String) — критичне для продуктивності
Java 14+: Records
public record Person(String name, int age) {}
Records автоматично генерують equals() і hashCode() на основі всіх компонентів, гарантуючи дотримання контракту.
Security
Порушення контракту може призводити до DoS-атак через Hash Flooding. Якісний hashCode() — це не тільки коректність, але й безпека.
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- equals() — рефлексивність, симетричність, транзитивність, консистентність, null = false
- hashCode() — якщо equals = true, то hashCode ОБОВ’ЯЗАН збігатися
- Зворотне хибне: однаковий hashCode ≠ рівні об’єкти (колізія)
- Алгоритм Блоха: початкове 17, множник 31 (=(i«5)-i), включати всі поля з equals
- 31 — просте непарне, JVM оптимізує множення через зсув
- Переповнення int — норма, хеш завжди wrap-around
- Records (Java 14+) автоматично генерують обидва методи
Часті уточнюючі питання:
- Що буде якщо перевизначити тільки equals? — рівні об’єкти потраплять в різні бакети, get поверне null
- Чому 31, а не 17 або 37? — 31 = (i«5)-i, одна операція замість множення
- Що таке порушення симетричності? — A.equals(B) = true, але B.equals(A) = false (класичний баг: Timestamp vs Date)
- Коли НЕ перевизначати? — якщо потрібна identity-семантика (порівняння за посиланням, а не вмістом)
Червоні прапорці (НЕ говорити):
- «hashCode повинен бути унікальним» — ні, колізії допустимі
- «Можна перевизначити тільки один метод» — ні, завжди пара
- «URL хороший ключ» — ні, URL.equals() робить DNS-запит, порушує консистентність
Пов’язані теми:
- [[3. Як HashMap визначає, в який bucket покласти елемент]]
- [[8. Якщо два об’єкти рівні за equals(), що можна сказати про їх hashCode()]]
- [[9. Якщо два об’єкти мають однаковий hashCode(), чи обов’язково вони рівні за equals()]]
- [[10. Що станеться, якщо перевизначити equals() але не перевизначити hashCode()]]