Коллекции глубже
Итераторы, неизменяемые коллекции, деки и очереди, ConcurrentModificationException, PriorityQueue, NavigableSet, sequenced-коллекции и LinkedHashMap.
8 вопросов
MiddleТеорияОчень частоЧем HashMap, LinkedHashMap и TreeMap различаются по порядку и стоимости?
Чем HashMap, LinkedHashMap и TreeMap различаются по порядку и стоимости?
HashMap — это хеш-таблица: в среднем O(1) на get/put и никаких гарантий порядка. LinkedHashMap расширяет её двусвязным списком, продетым сквозь записи, и сохраняет порядок вставки — или порядок обращений, что превращает её в LRU-кэш через removeEldestEntry — по-прежнему за O(1) и ценой небольшой памяти. TreeMap — красно-чёрное дерево: ключи упорядочены через Comparable/Comparator, операции за O(log n), ключ null запрещён.
Типичные ошибки
- ✗Полагать, что порядок обхода
HashMapстабилен или отражает порядок вставки - ✗Думать, что сортировка в
TreeMapбесплатна, а не стоит O(log n) на операцию - ✗Забывать, что именно режим порядка обращений делает
LinkedHashMapпригодной как LRU-кэш
Уточняющие вопросы
- →Как
removeEldestEntryпревращаетLinkedHashMapв ограниченный LRU-кэш? - →Почему
TreeMapотвергает ключnull, аHashMapдопускает ровно один?
JuniorТеорияЧастоПочему ArrayDeque предпочтительнее LinkedList в роли стека или очереди?
Почему ArrayDeque предпочтительнее LinkedList в роли стека или очереди?
ArrayDeque реализует Deque поверх растущего кольцевого массива, поэтому добавления и удаления с обоих концов — амортизированные O(1) при непрерывном, дружественном к кэшу хранении. LinkedList тоже реализует Deque, но каждому элементу нужен отдельный узел с двумя ссылками, что стоит памяти, локальности и нагрузки на сборщик мусора. ArrayDeque — рекомендуемый стек (вместо устаревшего Stack) и очередь; он лишь запрещает элементы null.
Типичные ошибки
- ✗Брать устаревший класс
Stack, который синхронизирован и наследуется отVector - ✗Полагать, что сращивание узлов за O(1) делает
LinkedListболее быстрой очередью на практике - ✗Забывать, что
ArrayDequeотвергает элементnullсNullPointerException
Уточняющие вопросы
- →Почему
ArrayDequeзапрещает элементыnull, аLinkedListих принимает? - →Как
ArrayDequeнаращивает свой кольцевой массив и чего стоит этот рост?
JuniorТеорияЧастоЧем List.of() отличается от Collections.unmodifiableList()?
Чем List.of() отличается от Collections.unmodifiableList()?
List.of() строит по-настоящему неизменяемый список: он копирует аргументы в собственное хранилище, отвергает элементы null и не отдаёт наружу ссылку на какую-либо исходную коллекцию, поэтому изменить его нельзя ничем. Collections.unmodifiableList(list) возвращает лишь доступное для чтения представление — его собственные мутаторы бросают UnsupportedOperationException, но запись через исходную ссылку list по-прежнему видна через это представление.
Типичные ошибки
- ✗Думать, что
Collections.unmodifiableListзамораживает исходный список, а не оборачивает его в представление - ✗Передавать элемент
nullвList.of(), что приводит кNullPointerException - ✗Полагать, что неизменяемый список неизменяем вглубь, хотя его элементы всё ещё могут быть изменяемыми
Уточняющие вопросы
- →Почему
List.of()отвергает элементыnull, аArrays.asList()их принимает? - →Что гарантирует
List.copyOf(list), чего не даётunmodifiableList(list)?
MiddleТеорияЧастоКакой порядок на самом деле гарантирует PriorityQueue?
Какой порядок на самом деле гарантирует PriorityQueue?
Упорядочена только голова. PriorityQueue — это двоичная куча, поэтому peek/poll всегда возвращают наименьший элемент по естественному порядку или по переданному Comparator, но iterator() и toString() обходят внутренний массив в порядке кучи, а он не отсортирован. offer/poll стоят O(log n), peek — O(1), а у элементов с равным приоритетом нет определённого взаимного порядка — очередь не стабильна и не FIFO внутри приоритета.
Типичные ошибки
- ✗Ожидать, что
for (T t : pq)илиtoString()выдадут элементы в порядке приоритета - ✗Полагать, что элементы равного приоритета покидают очередь в порядке вставки
- ✗Думать, что
pollстоит O(1), а не O(log n) на просеивание кучи вниз
Уточняющие вопросы
- →Как сделать
PriorityQueueстабильной для элементов равного приоритета? - →Почему
remove(Object)уPriorityQueueстоит O(n), аpoll— лишь O(log n)?
JuniorТеорияИногдаЧто умеет ListIterator, чего не может обычный Iterator?
Что умеет ListIterator, чего не может обычный Iterator?
Iterator обходит любую Collection только вперёд, с hasNext, next и remove. ListIterator, доступный только из List, ещё и идёт назад через hasPrevious/previous, сообщает позиции через nextIndex/previousIndex, заменяет последний возвращённый элемент через set и вставляет у курсора через add. Оба fail-fast при структурном изменении вне итератора.
Типичные ошибки
- ✗Думать, что
ListIteratorдоступен изSetилиMap, а не только изList - ✗Полагать, что
Iteratorумеет идти назад или вызыватьset/add, какListIterator - ✗Забывать, что
setзаменяет элемент, последним возвращённыйnext/previous, а не произвольный
Уточняющие вопросы
- →Когда использовать
ListIterator.setвместоList.set(index, e)? - →Почему
addуListIteratorвставляет перед элементом, который вернул быnext?
MiddleДебаггингИногдаИсправьте цикл, который удаляет элементы из списка во время обхода
Исправьте цикл, который удаляет элементы из списка во время обхода
Цикл for-each работает на собственном fail-fast Iterator списка. names.remove(name) меняет modCount списка за спиной итератора, поэтому следующий next() обнаруживает modCount != expectedModCount и бросает ConcurrentModificationException — второй поток тут ни при чём. Исправление: менять через сам итератор (it.remove() после it.next()) или просто вызвать names.removeIf(n -> n.length() == 3); обход копии тоже работает.
Типичные ошибки
- ✗Считать, что
ConcurrentModificationExceptionбывает только при нескольких потоках - ✗Вызывать
list.remove(...)внутри for-each вместоIterator.remove() - ✗Полагать, что исключение гарантировано — удаление предпоследнего элемента может проскочить
Уточняющие вопросы
- →Почему удаление предпоследнего элемента иногда вовсе не приводит к исключению?
- →Как итераторы
CopyOnWriteArrayListиConcurrentHashMapизбегают этого падения?
MiddleТеорияРедкоЧто добавляют SequencedCollection, SequencedSet и SequencedMap?
Что добавляют SequencedCollection, SequencedSet и SequencedMap?
Java 21 даёт каждой коллекции с определённым порядком обхода общий супертип. SequencedCollection объявляет getFirst/getLast, addFirst/addLast, removeFirst/removeLast и reversed(); его реализуют List, Deque и LinkedHashSet, поэтому list.getFirst() заменяет list.get(0). SequencedSet сужает reversed() до множества, а SequencedMap добавляет firstEntry/lastEntry и putFirst/putLast. reversed() — живое представление, а не копия.
Типичные ошибки
- ✗Ждать, что
HashSetилиHashMapобретут порядок обхода — его нет, и они остаются в стороне - ✗Считать
reversed()копией, а не живым представлением поверх исходной коллекции - ✗Думать, что у
Deque.addFirstиListуже был общий API первого/последнего до Java 21
Уточняющие вопросы
- →Почему
LinkedHashSetможет реализоватьSequencedCollection, аHashSet— нет? - →Что произойдёт с представлением
reversed(), если добавить элемент в коллекцию под ним?