Каков контракт метода `equals()` в Java и как его правильно переопределить?

«Каков контракт метода `equals()` в Java и как его правильно переопределить?» — вопрос из категории Java Core, который задают на 25% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Контракт метода equals(Object obj), определенный в классе Object, состоит из пяти обязательных свойств. Их нарушение может привести к непредсказуемому поведению коллекций (HashMap, HashSet).

Контракт equals():

  1. Рефлексивность (Reflexive): x.equals(x) всегда должно возвращать true.
  2. Симметричность (Symmetric): Если x.equals(y) возвращает true, то y.equals(x) также должно возвращать true.
  3. Транзитивность (Transitive): Если x.equals(y) и y.equals(z) возвращают true, то x.equals(z) также должно возвращать true.
  4. Консистентность (Consistent): Многократные вызовы x.equals(y) должны стабильно возвращать одно и то же значение, при условии, что поля, используемые в сравнении, не изменялись.
  5. Сравнение с null: x.equals(null) всегда должно возвращать false.

Правила переопределения:

  1. Всегда переопределяйте hashCode() вместе с equals(). Это требование контракта Object: равные объекты должны иметь равные хэш-коды.
  2. Используйте instanceof для проверки типа, если класс не final и вы допускаете сравнение с подклассами.
  3. Используйте == для сравнения примитивных полей и Objects.equals() для сравнения ссылочных полей (корректно обрабатывает null).

Шаблон корректной реализации:

import java.util.Objects;

public final class Person { // final класс
    private final String name;
    private final int age;

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

    @Override
    public boolean equals(Object o) {
        // 1. Проверка ссылки на себя
        if (this == o) return true;
        // 2. Проверка на null и совместимость типов (instanceof для не-final классов)
        if (o == null || getClass() != o.getClass()) return false;
        // 3. Приведение типа
        Person person = (Person) o;
        // 4. Сравнение значимых полей
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        // Генерация хэш-кода на основе тех же полей, что и в equals()
        return Objects.hash(name, age);
    }
}

Почему getClass() вместо instanceof в примере? Для final-класса Person использование getClass() гарантирует строгое равенство: объект Person будет равен только другому объекту Person, но не объекту его подкласса (которого не может быть). Если класс не final и логика равенства должна работать с наследниками, используйте instanceof и сравнивайте все значимые поля, доступные в базовом классе.