Питання 7 · Розділ 10

Що таке контракт equals() і hashCode()?

Метод equals() повинен поводитися як математичне поняття 'рівності':

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Контракт equals() і hashCode() — це набір правил, яких повинні дотримуватися методи equals() і hashCode() в Java. Якщо їх порушити, колекції на кшталт HashMap і HashSet працюватимуть неправильно.

Два простих правила:

  1. Якщо два об’єкти рівні за equals(), їх hashCode() обов’язаний бути однаковим
  2. Якщо 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()

  1. Консистентність: значення не змінюється, якщо не змінюються поля з equals()
  2. Обов’язковий зв’язок: x.equals(y)x.hashCode() == y.hashCode()
  3. Необов’язковий зв’язок: однаковий 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, це закладено в математику.

Типові помилки

  1. Перевизначити equals, забути hashCode — дублікати в HashSet
  2. Використовувати mutable поля — ключ “ламається” після зміни
  3. Класичний баг 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) пошуку:

  1. hashCode() → знаходить бакет: index = (n-1) & hash(key)
  2. 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

  1. URL.equals() — робить DNS-запит, порушує консистентність при збої мережі
  2. Float.NaNFloat.NaN.equals(Float.NaN) = true, але Float.NaN == Float.NaN = false
  3. Масиви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()]]