Каков контракт между методами equals() и hashCode() в Java?

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

Ответ

Контракт (соглашение), определенный в документации Java, гласит:

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

Зачем это нужно? Этот контракт критически важен для корректной работы hash-базированных коллекций: HashMap, HashSet, Hashtable. Они используют хэш-код для быстрого определения «корзины» (bucket), в которой может находиться объект.

Нарушение контракта приводит к ошибкам:

class BrokenPerson {
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BrokenPerson that = (BrokenPerson) o;
        return age == that.age && Objects.equals(name, that.name);
    }
    // НЕТ переопределения hashCode()! Используется реализация Object.hashCode()
}

// Проблема в использовании:
Set<BrokenPerson> set = new HashSet<>();
BrokenPerson p1 = new BrokenPerson("Alice", 30);
BrokenPerson p2 = new BrokenPerson("Alice", 30);

System.out.println(p1.equals(p2)); // true (логически равны)
set.add(p1);
System.out.println(set.contains(p2)); // Может вернуть FALSE! Объекты попали в разные корзины.

Правильная реализация: Всегда переопределяйте hashCode(), если переопределяете equals(). Используйте одни и те же значимые поля в обоих методах. Удобно делать это с помощью Objects.hash().

class CorrectPerson {
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CorrectPerson that = (CorrectPerson) o;
        return age == that.age && Objects.equals(name, that.name); // Поля для equals
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // Те же поля, что и в equals()!
    }
}