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

Які вимоги пред'являються до ключа HashMap?

Ключ HashMap повинен дотримуватися кількох важливих правил:

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

🟢 Junior Level

Ключ HashMap повинен дотримуватися кількох важливих правил:

  1. Перевизначити equals() і hashCode() — обидва методи повинні працювати разом
  2. Бути незмінюваним (immutable) — після створення ключ не повинен змінюватися
  3. Повертати однаковий hashCode при повторних викликах — якщо поля не змінювалися

Хороші ключі:

String key = "userId";           // Ідеальний ключ
Integer id = 42;                 // Теж відмінно
record Key(String name) {}       // Java 14+ — автоматично immutable

Погані ключі:

StringBuilder key = new StringBuilder("userId"); // Мутабельний!
List<String> listKey = new ArrayList<>();         // Можна змінити

🟡 Middle Level

Основні вимоги

Вимога Опис Наслідки порушення
Контракт equals/hashCode Якщо equals() = true, то hashCode() однаковий Втрата елементів в карті
Іммутабельність Поля ключа не повинні змінюватися “Зламаний ключ”, витік пам’яті
Консистентність Повторні виклики дають той самий результат Непередбачувана поведінка
Якісний розподіл hashCode повинен рівномірно розподіляти ключі Колізії, деградація продуктивності

Чому String — ідеальний ключ

  1. Іммутабельний за дизайном
  2. Кешує свій hashCode
  3. Реалізує Comparable
  4. Хороший розподіл hashCode

Anti-patterns

// Антипатерн 1: hashCode залежить від часу
@Override public int hashCode() { return System.currentTimeMillis(); }

// Антипатерн 2: hashCode залежить від випадкових чисел
@Override public int hashCode() { return new Random().nextInt(); }

// Антипатерн 3: URL як ключ (робить мережеві запити в equals)
Map<URL, Data> map = new HashMap<>(); // Дуже повільно!

Підтримка Comparable

Якщо ключ реалізує Comparable<K>, HashMap використовує compareTo() замість tieBreakOrder() для балансування дерева при колізіях (Java 8+). Це швидше, оскільки compareTo() працює зі вмістом об’єкта, а tieBreakOrder() вимагає додаткових обчислень через System.identityHashCode().

🔴 Senior Level

Формальні вимоги

З документації Map:

The behavior of a map is not specified if the value of an object used as a key is changed in a manner that affects equals comparisons.

Контракт Consistency

Методи equals() і hashCode() повинні бути консистентними:

  • Не повинні залежати від зовнішніх ресурсів (мережа, файлова система)
  • Не повинні використовувати випадкові/часові значення
  • URL.equals() — класичний приклад порушення: робить DNS-запит, блокується при збої мережі

Internal: Comparable для Treeification

При treeification бакета:

// Якщо key instanceof Comparable:
//   Використовується compareTo() для балансування дерева
// Якщо ні:
//   tieBreakOrder() через System.identityHashCode() — повільніше

Реалізація Comparable суттєво прискорює балансування дерева, оскільки compareTo() працює зі вмістом об’єкта, а tieBreakOrder() вимагає додаткових обчислень.

Memory Implications

  • Кожен запис = об’єкт Node (~32-48 байт) + ключ + значення
  • String-літерали з String Pool — економлять пам’ять, оскільки однакові літерали посилаються на один об’єкт. Це НЕ стосується new String(...).
  • Record-ключі — компактні, але вимагають Java 14+

Edge Cases

  1. null-ключ: Допускається один, завжди в table[0]. У великих системах — антипатерн
  2. Enum як ключ: Ідеальний варіант — EnumMap. Він швидший, бо використовує масив з ordinal() як індекс — O(1) без hashCode і колізій.
  3. Масив як ключ: equals() порівнює посилання, не вміст. Використовуйте обгортку

Security

Ключі можуть бути вектором атаки:

  • Hash Flooding: підбір ключів з однаковим hashCode
  • DoS через важкий equals() (порівняння великих даних)
  • Side-channel attacks через timing аналіз equals()

Best Practices

  1. Record для складних ключів (Java 14+)
  2. Final поля — мінімум, що можна зробити
  3. Defensive copy в конструкторі і геттерах
  4. Не використовувати mutable колекції як ключі
  5. Мінімізувати поля в equals/hashCode — тільки ідентифікатори

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Контракт equals/hashCode: якщо equals = true, hashCode однаковий
  • Іммутабельність: поля ключа не повинні змінюватися після створення
  • Консистентність: повторні виклики hashCode = той самий результат
  • Якісний розподіл: hashCode повинен рівномірно розподіляти ключі
  • String — ідеальний ключ: immutable, кешує hashCode, реалізує Comparable
  • Підтримка Comparable прискорює treeification (compareTo замість tieBreakOrder)

Часті уточнюючі питання:

  • Чому URL поганий ключ? — equals() робить DNS-запит, порушує консистентність
  • Чи можна масив як ключ? — ні, equals порівнює посилання; використовуйте обгортку
  • Чому EnumMap швидший за HashMap? — масив з ordinal() як індекс, O(1) без hashCode
  • Чи можна null як ключ? — так, один, але у великих системах — антипатерн

Червоні прапорці (НЕ говорити):

  • «Можна mutable ключ якщо не міняти його» — крихке рішення
  • «hashCode може залежати від часу» — ні, порушує консистентність
  • «Всі об’єкти підходять як ключ» — ні, повинні дотримуватися контракту

Пов’язані теми:

  • [[7. Що таке контракт equals() і hashCode()]]
  • [[12. Чи можна використовувати змінюваний об’єкт як ключ в HashMap]]
  • [[15. Чому String частіше використовується як ключ в HashMap]]