Що станеться, якщо перевизначити 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()]]