Вопрос 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 является иммутабельным]]