Какие требования предъявляются к классу, чтобы его объекты можно было использовать в качестве ключа в HashMap или элемента в HashSet?

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

Ответ

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

Контракт между equals и hashCode:

  1. Согласованность: Если два объекта равны согласно equals(Object), то вызов hashCode() для каждого из них должен возвращать одно и то же целое число.
  2. Обратное не обязательно: Разные хэш-коды гарантируют, что объекты не равны. Но одинаковые хэш-коды (коллизия) не гарантируют равенства объектов.

Неизменяемость (Immutability) — сильная рекомендация: Поля, участвующие в расчетах equals и hashCode, не должны изменяться после того, как объект стал ключом. Если хэш-код изменится, HashMap не сможет найти ключ в исходном бакете, что приведет к потере данных.

Пример корректной реализации:

public final class Person { // final класс для предотвращения нарушения контракта в подклассах
    private final String name; // final поля для обеспечения неизменяемости
    private final int id;

    public Person(String name, int id) {
        this.name = name;
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Проверка на один и тот же объект
        if (o == null || getClass() != o.getClass()) return false; // Проверка типа
        Person person = (Person) o;
        return id == person.id && Objects.equals(name, person.name); // Сравнение значимых полей
    }

    @Override
    public int hashCode() {
        // Использование Objects.hash() для удобства и соблюдения контракта
        return Objects.hash(name, id);
    }
}

Почему это важно для HashMap:

  1. При добавлении пары ключ-значение вычисляется hashCode() ключа для определения номера бакета (ячейки массива).
  2. Если в бакете уже есть элементы (коллизия), для сравнения используется equals(Object), чтобы проверить, не равен ли новый ключ уже существующему. Без корректной реализации этих методов поиск, вставка и удаление элементов будут работать неправильно.