Питання 27 · Розділ 13

Чи можна використовувати незмінні об'єкти як ключі в HashMap?

String обчислює hashCode один раз і запам'ятовує:

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

Junior Level

Не просто можна, а потрібно! Незмінні об’єкти — ідеальні ключі для HashMap.

Map<String, Integer> ages = new HashMap<>();
ages.put("Ivan", 25);  // String — незмінний
ages.put("Ivan", 30);  // Оновлення за тим же ключем

System.out.println(ages.get("Ivan")); // 30

Чому це добре

  • hashCode() не змінюється — об’єкт завжди в правильному “бакеті”
  • equals() працює стабільно
  • Не потрібно боятися, що ключ “загубиться”

Найкращі ключі

  • String — найпопулярніший
  • Integer, Long — обгортки примітивів
  • LocalDate, UUID — стабільні ідентифікатори
  • Record — складові ключі з кількох полів

Middle Level

Як працює HashMap з ключами

  1. put(key, value) — обчислюється key.hashCode() → визначається бакет
  2. get(key) — знову обчислюється key.hashCode() → шукається в тому ж бакеті
  3. Якщо hashCode() змінився — ключ “загубиться” в іншому бакеті

Незмінні об’єкти кешують hashCode

String обчислює hashCode один раз і запам’ятовує:

// Всередині String
private int hash; // 0 за замовчуванням
public int hashCode() {
    if (hash == 0 && value.length > 0) {
        hash = ...; // обчислення один раз
    }
    return hash;
}

Проблема мутабельних ключів

List<String> key = new ArrayList<>(List.of("A"));
map.put(key, 42);
key.add("B");           // змінили ключ!
map.get(key);           // null — hashCode змінився

Об’єкт фізично в бакеті, але get шукає в іншому місці.

Важливо: equals і hashCode повинні використовувати однаковий набір полів. Якщо hashCode по id, а equals по id + name — два об’єкти з однаковим id, але різним name, потраплять в один бакет, але не будуть рівні.


Senior Level

Ідеальний ключ для HashMap

public record CompositeKey(String type, Long id) {} // автоматично final, equals, hashCode

Map<CompositeKey, Entity> map = new HashMap<>();
map.put(new CompositeKey("user", 1L), user);

Стратегія якщо ключ зобов’язаний бути мутабельним

  1. Використовуйте ідентифікатор: Long id замість самого об’єкта
  2. Видалити → змінити → додати заново:
    map.remove(key);
    key.changeSomething();
    map.put(key, value);
    
  3. IdentityHashMap — порівнює за ==, а не за equals()/hashCode()

hashCode для складових ключів

Record генерує hashCode через Objects.hashCode для кожного поля. При колізіях HashMap будує дерево (O(log n) замість O(n)). Для складових ключів комбінуйте hashCode всіх значущих полів.

Резюме для Senior

  • Незмінні ключі гарантують детермінізм хеш-колекцій
  • Запобігають важковловимим багам та витокам пам’яті
  • Record — ідеальний вибір для складових ключів
  • Проектуйте ключі колекцій як незмінні сутності
  • Кешований hashCode — додаткова оптимізація

Шпаргалка для інтерв’ю

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

  • Незмінні об’єкти — ІДЕАЛЬНІ ключі: стабільний hashCode, немає ризику втрати в бакеті
  • String, Integer, LocalDate, Record — найкращі ключі
  • String кешує hashCode — обчислюється один раз, прискорює повторні lookup’и
  • Record генерує equals/hashCode через Objects.hashCode для кожного поля
  • Мутабельний ключ = втрата об’єкта: hashCode змінився, get шукає в іншому бакеті
  • При колізіях HashMap будує дерево — O(log n) замість O(n)

Часті уточнювальні запитання:

  • Чому мутабельний ключ небезпечний? — Зміниться hashCode → об’єкт “загубиться” → logical leak
  • Record як ключ? — Ідеальний: автоматично final, equals, hashCode по всіх полях
  • Якщо ключ зобов’язаний бути мутабельним? — IdentityHashMap (порівняння за ==) або remove → mutate → put
  • equals і hashCode повинні збігатися? — Так, використовувати однаковий набір полів

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

  • «Мутабельний ключ в HashMap — нормально» — об’єкт стане недосяжним
  • «hashCode можна не кешувати» — для незмінних це оптимізація
  • «ArrayList як ключ — хороша ідея» — мутабельний, hashCode зміниться
  • «IdentityHashMap вирішує всі проблеми» — порівнює за ==, не по equals — специфічний use case

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

  • [[22. В чому перевага незмінних об’єктів для кешування]]
  • [[28. Що станеться, якщо змінити змінний ключ в HashMap]]
  • [[20. Що таке Record і як він допомагає створювати незмінні класи]]
  • [[4. Чому клас String є незмінним]]