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

Что произойдёт, если переопределить hashCode(), но не переопределить equals()?

Если переопределить только hashCode(), объекты с одинаковым хешем попадут в одну корзину, но HashMap всё равно будет считать их разными — потому что equals() по умолчанию сравни...

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

🟢 Junior Level

Если переопределить только hashCode(), объекты с одинаковым хешем попадут в одну корзину, но HashMap всё равно будет считать их разными — потому что equals() по умолчанию сравнивает ссылки на объекты (==).

Что случится:

public class User {
    private Long id;

    @Override
    public int hashCode() { return id != null ? id.intValue() : 0; }
    // equals() НЕ переопределён — используется Object.equals() (сравнение по ссылке)
}

Map<User, String> map = new HashMap<>();
map.put(new User(1L), "Alice");  // hashCode=1, бакет #1 → equals не вызван (бакет пуст) → OK
map.put(new User(1L), "Bob");    // hashCode=1, бакет #1 → equals(Alice, Bob) → false (Object.equals = сравнение ссылок!) → новый элемент
System.out.println(map.size()); // 2! Два "одинаковых" ключа

Главное правило: Переопределил hashCode — переопредели и equals!

🟡 Middle Level

Практические последствия

1. Дубликаты в HashMap:

  • Оба User(id=1) попадают в один бакет (hashCode = 100)
  • При проверке equals() сравнивает ссылки → разные объекты → разные ключи
  • HashMap допускает оба

2. Невозможность получения данных:

  • Положили new User(1L) → достать пытаемся по new User(1L)
  • hashCode правильный (тот же бакет)
  • equals() возвращает false (разные ссылки)
  • Результат: null

3. Повышенное потребление памяти:

  • HashSet содержит больше элементов, чем ожидалось. При большом потоке данных это приводит к повышенному потреблению памяти и потенциальному OutOfMemoryError.

Сравнение двух ситуаций

Ситуация Где ищутся? Результат
equals OK, hashCode NO Разные бакеты Не находит (ищет не там)
hashCode OK, equals NO Один бакет Не находит (не узнаёт)

Последствия для производительности

Одинаковый hashCode для всех ключей → все элементы в одном бакете → деградация до O(n)/O(log n). Плюс бессмысленные вызовы equals().

🔴 Senior Level

Internal Mechanics

При map.get(new User(1L)):

// 1. Находим бакет (правильный): index = (n-1) & hash = 100
// 2. Итерируем элементы в бакете 100
// 3. Для каждого: p.hash == hash (true) → p.key.equals(key)
// 4. Object.equals(): this == obj → false (разные объекты)
// 5. Возвращаем null

Элемент физически в правильном бакете, но не опознан.

Memory Leak в production

Set<Event> uniqueEvents = new HashSet<>();
// hashCode переопределён по eventId, equals — нет
for (Event e : stream) {
    uniqueEvents.add(e); // Каждый объект добавляется!
}
// Результат: OOM при обработке большого потока

Сравнение с Object.equals()

// Object.equals() — это просто:
public boolean equals(Object obj) { return this == obj; }

Это сравнивает ссылки, а не содержимое. Даже два new User(1L) — разные ссылки → разные объекты.

Edge Cases

  1. Singleton-подобные объекты: Если всегда используется один и тот же экземпляр, проблема не проявится (но это хрупкое решение)
  2. Десериализация: При десериализации всегда создаётся новый объект — проблема гарантирована

Production Diagnostics

Признаки проблемы:

  • Растущий размер коллекций без ожидаемого роста
  • containsKey() возвращает false для “существующих” ключей
  • Heap dump показывает множество “одинаковых” объектов в Map

Best Practices

  1. Всегда генерируйте пару — используйте IDE или Lombok @EqualsAndHashCode
  2. Records — автоматически генерируют оба метода
  3. Статический анализ — SonarQube/SpotBugs находит это нарушение

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

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

  • Без equals() объекты с одинаковым hashCode попадают в один бакет, но Object.equals = сравнение ссылок (==)
  • HashMap допускает дубликаты: два new User(1L) = разные ключи
  • get() вернёт null — бакет правильный, но equals не узнаёт равный объект
  • OOM при обработке потоков: HashSet добавляет каждый объект вместо дедупликации
  • Object.equals() = это просто this == obj, сравнение по ссылке

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

  • Чем отличается от ситуации “equals OK, hashCode NO”? — там ищут не в том бакете, тут — в правильном, но не узнают
  • Когда проблема не проявится? — если всегда используется один экземпляр (singleton)
  • Почему OOM? — uniqueEvents.add() добавляет каждый объект, Set растёт бесконечно
  • Десериализация усугубляет? — да, всегда создаётся новый объект — проблема гарантирована

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

  • «Это менее серьёзная ошибка чем отсутствие hashCode» — нет, обе критичны
  • «equals можно не переопределять для immutable объектов» — нет, даже immutable объекты сравниваются по ссылке
  • «В production это не встречается» — встречается при десериализации

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

  • [[7. Что такое контракт equals() и hashCode()]]
  • [[10. Что произойдёт, если переопределить equals() но не переопределить hashCode()]]