Что, кроме значений полей, нужно учитывать при переопределении equals() в Java?

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

Ответ

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

  1. Сравнение ссылок (==): Проверить, не является ли переданный объект той же самой ссылкой. Это оптимизация.
  2. Проверка на null: Убедиться, что переданный объект не null.
  3. Проверка типа (getClass() или instanceof): Определить, можно ли сравнивать объекты. Выбор зависит от требований к наследованию.
    • getClass() == o.getClass() гарантирует строгое равенство классов (не работает с наследниками).
    • o instanceof MyClass позволяет считать равными объекты родительского и дочернего классов (логика должна быть корректной).

Пример корректной реализации для final-класса:

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

Контракт equals() требует соблюдения свойств:

  • Рефлексивность: x.equals(x) — всегда true.
  • Симметричность: Если x.equals(y) == true, то и y.equals(x) == true.
  • Транзитивность: Если x.equals(y) и y.equals(z), то x.equals(z).
  • Согласованность: Многократный вывоз должен возвращать одинаковый результат.
  • Сравнение с null: x.equals(null) — всегда false.