Вопрос 7 · Раздел 10

Что такое контракт equals() и hashCode()?

Метод equals() должен вести себя как математическое понятие 'равенства':

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Контракт equals() и hashCode() — это набор правил, которые должны соблюдать методы equals() и hashCode() в Java. Если их нарушить, коллекции вроде HashMap и HashSet будут работать неправильно.

Два простых правила:

  1. Если два объекта равны по equals(), их hashCode() обязан быть одинаковым
  2. Если hashCode() одинаковый — объекты не обязательно равны (это называется коллизия)

Пример правильного контракта:

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person p = (Person) o;
        return age == p.age && name.equals(p.name);
    }

    @Override
    public int hashCode() {
        return 31 * name.hashCode() + age;
    }
}

Главное правило: если объект будет использоваться как ключ в HashMap/HashSet — переопределяйте оба метода. Исключение: если вы НАМЕРЕННО хотите identity-поведение (например, для weak references).

🟡 Middle Level

Контракт equals()

Метод equals() должен вести себя как математическое понятие ‘равенства’:

  • Рефлексивность: объект равен самому себе: a.equals(a) = true
  • Симметричность: если A равен B, то B равен A
  • Транзитивность: если A=B и B=C, то A=C
Свойство Описание
Рефлексивность x.equals(x) всегда true
Симметричность Если x.equals(y), то y.equals(x)
Транзитивность Если x.equals(y) и y.equals(z), то x.equals(z)
Консистентность Повторные вызовы дают один результат
Сравнение с null x.equals(null) всегда false

Контракт hashCode()

  1. Консистентность: значение не меняется, если не меняются поля из equals()
  2. Обязательная связь: x.equals(y)x.hashCode() == y.hashCode()
  3. Необязательная связь: одинаковый hashCode ≠ равные объекты

Как правильно реализовывать

Алгоритм Джошуа Блоха:

int result = 17;
result = 31 * result + field1;
result = 31 * result + (field2 != null ? field2.hashCode() : 0);
return result;

Почему 17 и 31: 17 — начальное ‘зерно’ (prime seed), чтобы хеш первых полей не был нулевым. 31 — нечётное простое число: умножение на 31 = сдвиг влево на 5 минус 1 (31 * i = (i << 5) - i), что эффективно на CPU. Переполнение int — не баг, а норма: хеш ВСЕГДА wrap-around, это заложено в математику.

Типичные ошибки

  1. Переопределить equals, забыть hashCode — дубликаты в HashSet
  2. Использовать mutable поля — ключ “ломается” после изменения
  3. Классический баг JDK: Timestamp расширяет Date. date.equals(timestamp) может вернуть true, но timestamp.equals(date) — false, потому что Timestamp проверяет наносекунды, которых нет у Date. Нарушена симметричность!

Когда НЕ использовать кастомные equals/hashCode

Не используйте equals/hashCode для объектов с business-идентичностью, которая может меняться. Например, entity с auto-generated ID: до сохранения id=null, после — id=42. Два объекта с id=null ‘равны’, но после сохранения — уже нет. Это нарушает консистентность в HashMap.

🔴 Senior Level

Internal Implementation в HashMap

HashMap использует оба метода для O(1) поиска:

  1. hashCode() → находит бакет: index = (n-1) & hash(key)
  2. equals() → находит ключ внутри бакета

Последствия нарушения:

  • Равные объекты с разными hashCode попадают в разные бакеты → get() вернёт null
  • HashSet допустит дубликаты → нарушение бизнес-логики
  • remove() не найдёт объект → утечка памяти

Нарушение симметричности при наследовании

class Point2D { int x, y; }
class Point3D extends Point2D { int z; }

Если Point3D.equals() проверяет все 3 поля, а Point2D.equals() только 2:

  • new Point2D(1,1).equals(new Point3D(1,1,0))true (Point2D не видит z)
  • new Point3D(1,1,0).equals(new Point2D(1,1))false (Point3D не instanceof Point3D)
  • Нарушение симметричности!

Решение: использовать getClass() вместо instanceof, или композицию вместо наследования.

Edge Cases

  1. URL.equals() — делает DNS-запрос, нарушает консистентность при сбое сети
  2. Float.NaNFloat.NaN.equals(Float.NaN) = true, но Float.NaN == Float.NaN = false
  3. Массивыequals() сравнивает ссылки, не содержимое; используйте Arrays.equals()

Performance Implications

  • Плохой hashCode → коллизии → деградация HashMap до O(log n) или O(n)
  • Тяжёлый equals() (сравнение больших строк/байтовых массивов) → большая константа в O(1)
  • Кэширование hashCode (как в String) — критично для производительности

Java 14+: Records

public record Person(String name, int age) {}

Records автоматически генерируют equals() и hashCode() на основе всех компонентов, гарантируя соблюдение контракта.

Security

Нарушение контракта может приводить к DoS-атам через Hash Flooding. Качественный hashCode() — это не только корректность, но и безопасность.


🎯 Шпаргалка для интервью

Обязательно знать:

  • equals() — рефлексивность, симметричность, транзитивность, консистентность, null = false
  • hashCode() — если equals = true, то hashCode ОБЯЗАН совпадать
  • Обратное неверно: одинаковый hashCode ≠ равные объекты (коллизия)
  • Алгоритм Блоха: начальное 17, множитель 31 (=(i«5)-i), включать все поля из equals
  • 31 — простое нечётное, JVM оптимизирует умножение через сдвиг
  • Переполнение int — норма, хеш всегда wrap-around
  • Records (Java 14+) автоматически генерируют оба метода

Частые уточняющие вопросы:

  • Что будет если переопределить только equals? — равные объекты попадут в разные бакеты, get вернёт null
  • Почему 31, а не 17 или 37? — 31 = (i«5)-i, одна операция вместо умножения
  • Что такое нарушение симметричности? — A.equals(B) = true, но B.equals(A) = false (классический баг: Timestamp vs Date)
  • Когда НЕ переопределять? — если нужна identity-семантика (сравнение по ссылке, а не содержимому)

Красные флаги (НЕ говорить):

  • «hashCode должен быть уникальным» — нет, коллизии допустимы
  • «Можно переопределить только один метод» — нет, всегда пара
  • «URL хороший ключ» — нет, URL.equals() делает DNS-запрос, нарушает консистентность

Связанные темы:

  • [[3. Как HashMap определяет, в какой bucket положить элемент]]
  • [[8. Если два объекта равны по equals(), что можно сказать об их hashCode()]]
  • [[9. Если два объекта имеют одинаковый hashCode(), обязательно ли они равны по equals()]]
  • [[10. Что произойдёт, если переопределить equals() но не переопределить hashCode()]]