HashMap
HashMap<K, V> хранит пары «ключ — значение» и реализует Map<K, V>.
Ключи уникальны по контракту equals, значения могут повторяться. Один ключ и
несколько значений могут быть null. Порядок обхода не гарантирован.
Как устроена
Внутри находится массив бакетов. Бакет бывает пустым, цепочкой узлов или, при большом числе коллизий, красно-чёрным деревом.
Путь операции get(key):
- Получить
hashCodeключа; дляnullиспользуется ноль. - Смешать старшие и младшие биты:
h ^ (h >>> 16). - Найти индекс:
(table.length - 1) & hash. - Сравнить hash, затем ключи через
equals.
Длина таблицы — степень двойки, поэтому битовая маска заменяет деление.
record UserId(long value) {}
Map<UserId, String> names = new HashMap<>();
names.put(new UserId(7), "Ира");
String name = names.get(new UserId(7)); // ИраЁмкость, load factor и resize
По умолчанию load factor равен 0.75. Порог расширения:
capacity * loadFactor. При превышении порога таблица в Java 8+ удваивается.
Узел остаётся на прежнем индексе или переезжает на oldIndex + oldCapacity:
это определяется одним битом hash, полный перерасчёт остатка не нужен.
При ёмкости не меньше 64 цепочка превращается в дерево после добавления
девятого узла в бакет (TREEIFY_THRESHOLD = 8). При меньшей таблице сначала
выполняется расширение. При сокращении до шести узлов дерево снова может стать
списком.
| Операция | Обычно | Худший случай |
|---|---|---|
get, put, remove | O(1) | O(n), для дерева O(log n) |
containsValue | O(n) | O(n) |
| обход | O(capacity + size) | O(capacity + size) |
Важные ограничения
- Ключ не должен менять поля, участвующие в
equalsиhashCode, пока он в map. HashMapне потокобезопасен; для общей изменяемой map см. ConcurrentHashMap.- Начальная ёмкость влияет на resize и стоимость обхода.
- Коллизия допустима: разные ключи могут иметь одинаковый hash.
putс равным ключом заменяет значение и возвращает предыдущее.
Подробнее о контракте ключа: Equals и hashCode. Оценки сложности: Big O.
Вопросы для собеседования
- Почему ёмкость — степень двойки? Чтобы вычислять индекс маской и распределять узлы при resize по одному дополнительному биту.
- Когда бакет становится деревом? После превышения порога цепочки при ёмкости таблицы не меньше 64.
- Что произойдёт при изменении ключа? Поиск пойдёт в другой бакет или не найдёт равный ключ; запись фактически станет недоступной.
- Допускает ли map коллизии? Да, hash выбирает бакет, а равенство ключа проверяется отдельно.
- Почему средняя сложность
get—O(1)? При хорошем распределении в бакете остаётся мало элементов. - Гарантирует ли
HashMapпорядок? Нет; для порядка вставки нуженLinkedHashMap, для сортировки —TreeMap.