Что такое коллизия в HashMap?
Это происходит потому, что количество возможных ключей бесконечно, а количество корзин ограничено. Представьте парковку на 16 мест: если приедут 20 машин, хотя бы на 4 места пре...
🟢 Junior Level
Коллизия — это когда два разных ключа попадают в одну и ту же корзину (бакет) в HashMap.
Это НЕ ошибка! Это ожидаемое поведение. Как в школе два ученика могут иметь одну фамилию — это не значит, что они один человек. HashMap рассчитана на коллизии и умеет их обрабатывать.
Это происходит потому, что количество возможных ключей бесконечно, а количество корзин ограничено. Представьте парковку на 16 мест: если приедут 20 машин, хотя бы на 4 места претендуют по 2 машины.
Пример коллизии:
HashMap<String, String> map = new HashMap<>();
map.put("Aa", "value1"); // hashCode = 2112
map.put("BB", "value2"); // hashCode = 2112 (такой же!)
// Почему: формула String.hashCode(): s[0]*31^(n-1) + s[1]*31^(n-2)
// "Aa": 65*31 + 97 = 2112
// "BB": 66*31 + 66 = 2112
// Разные символы, но математически дают тот же результат
// Оба попали в одну корзину — это коллизия
Хорошая новость: HashMap умеет обрабатывать коллизии — она хранит несколько элементов в одной корзине в виде цепочки.
🟡 Middle Level
Почему коллизии неизбежны
- Принцип Дирихле: Хеш-кодов 2^32 (~4.3 млрд), а возможных объектов — бесконечно
- Парадокс дней рождения: В таблице на 365 бакетов вероятность коллизии > 50% уже при 23 элементах
- Сжатие диапазона: Даже разные hashCode могут совпасть после
& (n-1)
Типы коллизий
| Тип коллизии | Причина | Решение |
|---|---|---|
| Коллизия hashCode | Два объекта имеют одинаковый hashCode() (редко при хорошей функции) | Treeification при 8 элементах (Java 8+) |
| Коллизия индекса | hashCode РАЗНЫЕ, но (n-1) & hash одинаковый (гораздо чаще) |
Resize (удвоение таблицы) |
Пример коллизии индекса: hashCode = 100 и hashCode = 116 при n=16 оба дают индекс 4, потому что 100 & 15 = 4 и 116 & 15 = 4.
Стратегии разрешения
- Separate Chaining (метод цепочек) — используется в HashMap, LinkedHashMap, ConcurrentHashMap
- Open Addressing (открытая адресация) — используется в ThreadLocalMap, IdentityHashMap
Эволюция
| Версия | Структура | Сложность |
|---|---|---|
| Java 7 | Связный список | O(n) |
| Java 8+ | Список → Дерево | O(log n) |
Типичные ошибки
- Возврат константы из hashCode() — все элементы в одном бакете
- Использование только одного поля в hashCode() — множество коллизий при разных объектах
🔴 Senior Level
Internal Mechanics
Коллизия определяется на уровне бакета:
// hash(k1) & (n-1) == hash(k2) & (n-1)
// Но k1.equals(k2) == false
HashMap использует двухуровневую фильтрацию:
- Быстрый отсев — сравнение по хешу (
p.hash == hash), отсекает 99.9% - Точная проверка — вызов
equals()для оставшихся кандидатов
Treeification Details
При 8 элементах в бакете:
- Если capacity < 64: вызывается
resize()(разнесёт элементы) - Если capacity >= 64: список → красно-чёрное дерево
Почему 8? Вероятность цепочки из 8 элементов при loadFactor 0.75 и хорошем hashCode: 0.00000006 (распределение Пуассона). Это маркер аномалии — либо плохой hashCode, либо атака.
CPU и Cache Impact
Коллизии влияют на производительность сильнее, чем кажется:
- Каждый
equals()— это вызов метода + сравнение полей - Ноды разбросаны по Heap → Cache Miss при каждом переходе
- При treeification добавляется overhead балансировки дерева
Hash Flooding Attack
Целенаправленная DoS-атака:
- Злоумышленник генерирует ключи с одинаковым хешем
- Все элементы попадают в один бакет
- Java 7: O(n²) при вставке n элементов → 100% CPU
- Java 8+: O(n log n) — атака смягчена, но не устранена полностью
Memory Implications
Каждая коллизия = дополнительный объект Node в Heap. При массовых коллизиях:
- Растёт давление на Young Gen GC
- При treeification: TreeNode ~ 2x размер Node
- В Old Gen накапливаются долгоживущие ноды
Production Monitoring
Для выявления проблем:
- Мониторинг времени get/put — рост указывает на коллизии
- Анализ распределения бакетов через JFR (Java Flight Recorder)
- Проверка качества hashCode() через unit-тесты на распределение
🎯 Шпаргалка для интервью
Обязательно знать:
- Коллизия = два разных ключа попали в один бакет — это нормально, не ошибка
- Принцип Дирихле: ключей бесконечно много, бакетов ограниченно — коллизии неизбежны
- Java 7: O(n) при коллизиях, Java 8+: O(log n) благодаря treeification
- Парадокс дней рождения: при 5 элементах и 16 бакетах вероятность коллизии > 50%
- Классический пример: “Aa” и “BB” имеют одинаковый hashCode = 2112
- Два типа коллизий: одинаковый hashCode И разные hashCode, но одинаковый индекс после
(n-1) & hash - Порог 8 выбран на основе распределения Пуассона — вероятность ≈ 0.00000006
Частые уточняющие вопросы:
- Почему коллизии неизбежны? — hashCode = 32 бита (~4.3 млрд), а возможных объектов бесконечно
- Чем Separate Chaining отличается от Open Addressing? — chaining = списки в бакетах, open addressing = поиск следующего свободного
- Что такое Hash Flooding Attack? — злоумышленник подбирает ключи с одинаковым хешем → DoS
- Почему вероятность 8 коллизий так мала? — распределение Пуассона при loadFactor 0.75
Красные флаги (НЕ говорить):
- «Коллизия — это баг» — нет, ожидаемое поведение
- «HashMap не поддерживает коллизии» — поддерживает через цепочки/деревья
- «hashCode всегда уникален» — нет, 32 бита не могут покрыть бесконечное множество
Связанные темы:
- [[3. Как HashMap определяет, в какой bucket положить элемент]]
- [[5. Как HashMap обрабатывает коллизии]]
- [[6. Что происходит при достижении 8 элементов в одном bucket]]
- [[9. Если два объекта имеют одинаковый hashCode(), обязательно ли они равны по equals()]]