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

Если два объекта равны по equals(), что можно сказать об их hashCode()?

Если два объекта равны по equals(), их hashCode() обязательно должен быть одинаковым. Это одно из главных правил Java.

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

🟢 Junior Level

Если два объекта равны по equals(), их hashCode() обязательно должен быть одинаковым. Это одно из главных правил Java.

Почему это важно? HashMap сначала ищет бакет по hashCode(), и только внутри бакета проверяет equals().

Зачем два уровня: hashCode быстрый (одна операция), но неточный (коллизии). equals точный (сравнение всех полей), но медленный. Сначала быстрый отсев — потом точная проверка. Если хеши разные — HashMap даже не вызовет equals() и не найдёт элемент.

Пример:

Person p1 = new Person("Ivan", 30);
Person p2 = new Person("Ivan", 30);

System.out.println(p1.equals(p2));  // true (если правильно реализован)
System.out.println(p1.hashCode() == p2.hashCode()); // ОБЯЗАН быть true!

Простое правило: Равные объекты = одинаковый hashCode. Всегда.

🟡 Middle Level

Почему это критически важно?

Поиск в HashMap работает так:

  1. Вычисляется hashCode() ключа
  2. Определяется бакет: index = (n-1) & hash
  3. Внутри бакета ищется элемент через equals()

Если hashCode разный при равных equals:

  • HashMap пойдёт в другой бакет
  • get() вернёт null, хотя элемент есть
  • HashSet допустит дубликаты
  • remove() не удалит объект

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

Проблема Описание
Потеря данных get() возвращает null для существующего ключа
Дубликаты HashSet содержит “одинаковые” объекты
Утечка памяти Невозможно удалить объект из карты

Реализация в JDK

  • String: одинаковые строки → одинаковый hashCode (формула на основе символов)
  • Integer: hashCode = само значение int
  • Long: hashCode = (int)(value ^ (value >>> 32))
  • Object (по умолчанию): identity hash code — число, которое JVM связывает с объектом при первом вызове System.identityHashCode(). Оно НЕ равно адресу в памяти (хотя может быть связано) и НЕ меняется при GC-перемещении.

Как проверить

Статические анализаторы (SonarQube, SpotBugs) автоматически находят нарушение этого контракта. IntelliJ IDEA предупреждает при генерации одного метода без другого.

Когда одинакового hashCode недостаточно

Одинаковый hashCode для равных объектов — необходимое, но недостаточное условие. hashCode должен быть консистентным: если поля не менялись, повторный вызов hashCode() должен вернуть то же число. Если hashCode зависит от текущего времени или рандома — контракт нарушен.

🔴 Senior Level

Формальное требование

Из Javadoc Object.hashCode():

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

Это обязательство, а не рекомендация. Его нарушение ломает весь Java Collections Framework.

Internal Mechanics нарушения

При map.put(objA, "value"):

hashA = hash(objA.hashCode()) = 100 → бакет 100

При map.get(objB) где objB.equals(objA) == true:

hashB = hash(objB.hashCode()) = 200 → бакет 200 → пустой → null

Элемент физически в бакете 100, но HashMap ищет в бакете 200.

Heisenbug-эффект

Ошибка может не проявляться, если:

  • Объекты не используются в хеш-коллекциях
  • Разные hashCode случайно попали в один бакет (индекс совпал через (n-1) & hash)
  • При следующем resize индексы изменятся, и код упадёт

Это создаёт недетерминированный баг, который сложно воспроизвести.

Memory and GC Impact

“Потерянные” элементы в HashMap продолжают занимать память, но недоступны для удаления. При массовом нарушении — утечка памяти в Old Gen.

Record и Lombok

// Record (Java 14+) — контракт соблюдён автоматически
public record Key(String id) {}

// Lombok — оба метода генерируются из одних полей
@EqualsAndHashCode
class Key { String id; }

Production Diagnostics

Если в production get() возвращает null для “существующего” ключа:

  1. Проверьте, переопределены ли оба метода
  2. Проверьте, не изменились ли mutable поля ключа
  3. Проверьте, не нарушена ли симметричность при наследовании

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

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

  • Если equals = true, то hashCode ОБЯЗАН быть одинаковым — обязательство из Javadoc
  • HashMap ищет бакет по hashCode, внутри — по equals; разные hashCode = элемент не найден
  • Нарушение = потеря данных в Map, дубликаты в Set, невозможность удаления
  • Heisenbug-эффект: ошибка может не проявляться в малых тестах, но упасть в production
  • Object.hashCode() по умолчанию = identity hash code, НЕ адрес в памяти
  • Identity hash code не меняется при GC-перемещении

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

  • Может ли hashCode совпасть случайно при разных equals? — да, это коллизия, нормальная ситуация
  • Почему ошибка недетерминированная? — разные hashCode могут случайно попасть в один бакет при малой карте
  • Что такое identity hash code? — число, которое JVM связывает с объектом при первом вызове System.identityHashCode()
  • Как предотвратить? — использовать Records, Lombok @EqualsAndHashCode, IDE-генерацию

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

  • «hashCode должен быть одинаковым только для равных объектов» — он МОЖЕТ совпадать и для неравных (коллизия)
  • «Это редкая ошибка» — один из самых коварных багов: проходит тесты, падает в production
  • «Можно игнорировать, если объекты не в Map» — да, но это хрупкое решение

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

  • [[7. Что такое контракт equals() и hashCode()]]
  • [[9. Если два объекта имеют одинаковый hashCode(), обязательно ли они равны по equals()]]
  • [[10. Что произойдёт, если переопределить equals() но не переопределить hashCode()]]