Встречается на 56% собеседований по Java

Расскажи про Hash Code & Equals Contract

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

Контракт equals() и hashCode()

Контракт определяет обязательные правила взаимодействия методов:

  • Согласованность: если x.equals(y) возвращает true, то x.hashCode() == y.hashCode() обязательно.
  • Обратное не требуется: если x.hashCode() == y.hashCode(), это не гарантирует x.equals(y). Разные объекты могут иметь одинаковый хеш-код, что приводит к коллизиям.
  • Равные объекты — одинаковый хеш-код: это обеспечивает корректный поиск в хеш-таблицах: объект с одинаковым содержимым будет найден в той же «корзине».

Требования к equals()

Метод equals() по умолчанию сравнивает ссылки, поэтому для сравнения по содержимому его переопределяют. При переопределении необходимо соблюдать пять свойств:

  • Рефлексивность: x.equals(x) всегда true для ненулевого x.
  • Симметричность: x.equals(y) равно y.equals(x).
  • Транзитивность: если x.equals(y) и y.equals(z), то x.equals(z).
  • Консистентность: при неизменных полях результат equals() не меняется.
  • Сравнение с null: x.equals(null) всегда false.

Пример реализации

java
public class Person {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
               Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

Здесь equals() сравнивает name и age, а hashCode() использует те же поля через Objects.hash(). Это гарантирует, что равные объекты имеют одинаковый хеш-код.

Подводные камни

  • Нарушение контракта: если переопределить только equals() без hashCode(), объекты могут «потеряться» в HashMap или HashSet — поиск по ключу не найдёт их, даже если они равны.
  • Использование изменяемых полей: если поля, участвующие в hashCode(), изменяются после добавления объекта в коллекцию, хеш-код меняется, и объект становится недоступным для поиска. Лучше использовать неизменяемые поля.
  • Разные типы: в equals() часто используют getClass() != o.getClass() для строгой типизации, но это нарушает симметричность при наследовании. Альтернатива — instanceof с проверкой полей, но это требует аккуратности.

Коротко

  • Контракт: равные объекты обязаны иметь одинаковый hashCode(), но одинаковый хеш-код не гарантирует равенства.
  • Переопределяя equals(), соблюдай рефлексивность, симметричность, транзитивность, консистентность и сравнение с null.
  • Всегда переопределяй hashCode() вместе с equals(), используя те же значимые поля.
  • Для генерации хеш-кода удобно использовать Objects.hash() или Objects.hashCode() для отдельных полей.
как отвечать на вопрос
пример собеседования
фреймворки на собеседовании
типичные вопросы junior
интервью вопросы и ответы