Вопрос «как устроен HashMap» задают почти всегда, от junior до senior. Разница только в глубине: junior называет корзины и equals/hashCode, senior рассказывает про деревья в корзинах, resize и почему HashMap не для многопоточности.
Иерархия
Collection:List(порядок, индексы, дубли),Set(без дублей),Queue/Deque(очереди).Mapстоит отдельно: пары ключ-значение, не наследуетCollection.
| Задача | Брать |
|---|---|
| список с доступом по индексу | ArrayList |
| стек, очередь, дек | ArrayDeque |
| множество без порядка | HashSet |
| порядок вставки | LinkedHashSet / LinkedHashMap |
| отсортированные ключи, поиск ближайшего | TreeSet / TreeMap |
| приоритеты | PriorityQueue (двоичная куча) |
ArrayList
Внутри массив Object[]. get(i) O(1), add в конец амортизированно O(1): когда массив заполнен, создаётся новый в 1.5 раза больше и старый копируется. Вставка и удаление в середине O(n) из-за сдвига. Если размер известен, задайте ёмкость: new ArrayList<>(n).
LinkedList двусвязный список: вставка по готовому узлу O(1), но get(i) O(n), а каждый узел это отдельный объект с двумя ссылками, кеш процессора страдает. На практике ArrayList или ArrayDeque почти всегда быстрее.
HashMap
Массив корзин (table), размер степень двойки, по умолчанию 16. Путь put(key, value):
hash = key.hashCode() ^ (h >>> 16): старшие биты подмешиваются к младшим.- Индекс корзины
(n - 1) & hash. - Корзина пуста: кладём узел. Иначе идём по цепочке и ищем ключ через
hash ==иequals. Нашли: заменяем значение, нет: добавляем в конец. - В цепочке стало 8 узлов, и таблица не меньше 64: цепочка превращается в красно-чёрное дерево (поиск O(log n) вместо O(n)). Если таблица меньше 64, вместо этого она расширяется.
- Размер превысил
capacity * loadFactor(0.75): таблица удваивается, узлы раскладываются заново.
Ключ null допустим (один, в корзине 0). Порядок обхода не гарантирован и меняется после resize. HashMap не потокобезопасен: при параллельной записи теряются данные. Для потоков ConcurrentHashMap (он, кстати, не принимает null ни в ключах, ни в значениях).
HashSet это HashMap, где элементы хранятся как ключи, а значение одна общая заглушка.
TreeMap и LinkedHashMap
TreeMap красно-чёрное дерево: операции O(log n), ключи отсортированы по compareTo или Comparator, есть floorKey, ceilingKey, headMap, subMap. Равенство ключей определяет компаратор, а не equals.
LinkedHashMap хеш-таблица плюс двусвязный список узлов: обход в порядке вставки. С флагом accessOrder = true и переопределённым removeEldestEntry это готовый LRU-кеш.
Итераторы и изменение коллекции
Итераторы ArrayList, HashMap и других обычных коллекций fail-fast: если коллекцию структурно изменили не через итератор, следующий next() бросит ConcurrentModificationException. Удалять при обходе нужно через iterator.remove() или list.removeIf(...).
for (String s : list) {
if (s.isEmpty()) list.remove(s); // CME
}
list.removeIf(String::isEmpty); // правильно
Неизменяемые коллекции
List.of,Set.of,Map.of(Java 9): неизменяемые, не принимаютnull,Set.ofиMap.ofпадают на дублях.Arrays.asList(arr): фиксированный размер,setможно,add/removeбросаютUnsupportedOperationException, а изменения пишутся в исходный массив.Collections.unmodifiableList(list): обёртка-представление, изменения исходного списка через неё видны.- Java 21 добавила
SequencedCollection:getFirst(),getLast(),reversed()у списков, деков иLinkedHashSet.
Частые ошибки
- Удаление в foreach и
ConcurrentModificationException. - Изменяемый объект как ключ HashMap.
LinkedList«потому что вставка O(1)».list.remove(1)наList<Integer>: удаляется элемент по индексу 1, а не значение 1.
Что спросят на собесе
- Как устроен HashMap: hash, корзины, коллизии, resize, деревья.
- Сложность операций ArrayList и LinkedList, что выбрать.
- Чем отличаются HashMap, LinkedHashMap и TreeMap.
- Что такое fail-fast итератор и как удалить элемент во время обхода.
- Как сделать LRU-кеш на стандартных коллекциях.