Что произойдёт, если переопределить hashCode(), но не переопределить equals()?
Если переопределить только hashCode(), объекты с одинаковым хешем попадут в одну корзину, но HashMap всё равно будет считать их разными — потому что equals() по умолчанию сравни...
🟢 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
- Singleton-подобные объекты: Если всегда используется один и тот же экземпляр, проблема не проявится (но это хрупкое решение)
- Десериализация: При десериализации всегда создаётся новый объект — проблема гарантирована
Production Diagnostics
Признаки проблемы:
- Растущий размер коллекций без ожидаемого роста
containsKey()возвращаетfalseдля “существующих” ключей- Heap dump показывает множество “одинаковых” объектов в Map
Best Practices
- Всегда генерируйте пару — используйте IDE или Lombok
@EqualsAndHashCode - Records — автоматически генерируют оба метода
- Статический анализ — 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()]]