Почему использование изменяемых объектов в качестве ключей в Java Map может привести к ошибкам?

«Почему использование изменяемых объектов в качестве ключей в Java Map может привести к ошибкам?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Использование изменяемых объектов в качестве ключей Map нарушает контракт между hashCode() и equals(), на котором основана работа структур данных, таких как HashMap.

Почему это происходит:

  1. Хэш-код изменяется: При добавлении объекта-ключа в Map вычисляется его хэш-код (hashCode()), который определяет «корзину» (bucket) для хранения пары ключ-значение.
  2. Поиск происходит по новому хэш-коду: Если после добавления изменить состояние ключа так, что его hashCode() изменится, последующий поиск по этому ключу (map.get(key)) будет выполнен в другой «корзине». В результате элемент не будет найден, хотя физически он присутствует в коллекции.
  3. Нарушается уникальность: Это также может привести к дублированию ключей в разных «корзинах», что противоречит логике Map.

Пример проблемы:

import java.util.HashMap;
import java.util.Map;

class MutableKey {
    private String value;
    public MutableKey(String value) { this.value = value; }
    public void setValue(String value) { this.value = value; }
    @Override
    public int hashCode() { return value.hashCode(); }
    @Override
    public boolean equals(Object obj) { /* ... */ }
}

public class Main {
    public static void main(String[] args) {
        Map<MutableKey, String> map = new HashMap<>();
        MutableKey key = new MutableKey("initial");
        map.put(key, "My Value");

        // Изменяем состояние ключа, что ведет к изменению hashCode()
        key.setValue("modified");

        // Попытка найти значение по измененному ключу
        System.out.println(map.get(key)); // Вывод: null
        // Значение "потерялось", хотя map не пуст
    }
}

Решение и best practices:

  • Используйте неизменяемые объекты (String, Integer, собственные классы с final полями) в качестве ключей.
  • Если класс ключа должен быть изменяемым, гарантируйте, что его поля, участвующие в hashCode()/equals(), не изменяются после того, как объект был использован как ключ.
  • Корректно переопределяйте методы hashCode() и equals().