Коллекции
Фреймворк коллекций — типы List/Set/Map, ArrayList и LinkedList, компараторы и копирование.
14 вопросов
JuniorТеорияОчень частоЧем ArrayList и LinkedList различаются по внутренней структуре?
Чем ArrayList и LinkedList различаются по внутренней структуре?
ArrayList опирается на динамический массив, поэтому произвольный доступ по индексу — O(1), но вставка или удаление в середине сдвигает последующие элементы и стоит O(n). LinkedList — двусвязный список узлов, поэтому добавление или удаление с концов — O(1), но добраться до индекса значит пройти цепочку, что даёт O(n) доступ. ArrayList берут для частого чтения по индексу, LinkedList — для частых правок на концах.
Типичные ошибки
- ✗Считать
LinkedList.get(i)за O(1) — он идёт по цепочке, поэтому циклы по индексу дают O(n²) - ✗Забывать, что
ArrayListиногда перевыделяется и копируется, делая добавление амортизированным, а не плоским - ✗Выбирать
LinkedListдля общего применения, когдаArrayListбыстрее на большинстве реальных нагрузок
Уточняющие вопросы
- →Почему удаление из середины
LinkedListвсё равно стоит O(n), несмотря на O(1) отсоединение узла? - →Что такое амортизированный O(1) для добавления в
ArrayList, и откуда берётся всплеск стоимости n?
JuniorТеорияОчень частоЧто такое Java Collections framework, и что он предоставляет?
Что такое Java Collections framework, и что он предоставляет?
Collections framework — это единая архитектура для хранения групп объектов и работы с ними. Он связывает базовые интерфейсы — Collection, List, Set, Map, Queue — с готовыми реализациями вроде ArrayList и HashMap, плюс переиспользуемые алгоритмы в утилитном классе Collections. Это даёт единый, согласованный API и позволяет менять реализацию, не переписывая вызывающий код.
Типичные ошибки
- ✗Путать фреймворк в целом с единственным утилитным классом
Collectionsсо статическими методами - ✗Считать, что
MapнаследуетCollection— это отдельный корневой интерфейс иерархии - ✗Думать, что фреймворк поставляет только интерфейсы, упуская конкретные реализации и алгоритмы
Уточняющие вопросы
- →Почему
Mapне является подтипом корневого интерфейсаCollection? - →Как программирование на уровне интерфейса упрощает замену одной реализации другой?
JuniorТеорияОчень частоВ чём разница между List и Set в Java, и когда что выбирать?
В чём разница между List и Set в Java, и когда что выбирать?
List — упорядоченная позиционная последовательность: хранит порядок вставки, даёт доступ к элементам по индексу и допускает дубликаты. Set моделирует математическое множество: отвергает повторяющиеся элементы, так что каждое значение встречается не более одного раза, и обычно неупорядочен (HashSet) — хотя TreeSet хранит элементы отсортированными, а LinkedHashSet сохраняет порядок вставки. List берут для последовательностей, Set — для уникальности.
Типичные ошибки
- ✗Считать всякий
Setнеупорядоченным —TreeSetотсортирован, аLinkedHashSetхранит порядок вставки - ✗Ожидать
get(int index)уSet, у которого нет позиционного доступа - ✗Забывать, что
Setмолча отбрасывает повторное добавление, а не бросает исключение
Уточняющие вопросы
- →Какие два метода решают, считает ли
HashSetдва элемента дубликатами? - →Когда вы предпочтёте
LinkedHashSetобычномуHashSet?
JuniorТеорияЧастоВ чём разница между Collection и Collections в Java?
В чём разница между Collection и Collections в Java?
Collection (единственное число) — корневой интерфейс иерархии, который расширяют List, Set и Queue; он объявляет операции вроде add, remove и size. Collections (множественное число) — утилитный класс, содержащий только статические методы вроде sort, reverse, shuffle, synchronizedList и unmodifiableList. То есть одно — интерфейс, который реализуют, а другое — набор инструментов, который вызывают.
Типичные ошибки
- ✗Вызывать методы
Collectionsу экземпляра вместо статического вызова через имя класса - ✗Путать эти два имени между собой, ведь они отличаются лишь конечной буквой
s - ✗Считать
Collectionsинтерфейсом для реализации, а не финальным утилитным классом
Уточняющие вопросы
- →Что возвращает
Collections.unmodifiableList, и что произойдёт при записи в него? - →Почему методы класса
Collectionsобъявлены статическими?
MiddleТеорияЧастоВ чём разница между Comparable и Comparator в Java?
В чём разница между Comparable и Comparator в Java?
Comparable задаёт единственный естественный порядок типа: класс реализует compareTo(other) у себя, так что Collections.sort знает порядок по умолчанию. Comparator — отдельный объект, задающий один из многих внешних порядков через compare(a, b), позволяя сортировать тот же тип по разным ключам, не трогая его исходник. Comparable берут для единственного очевидного порядка, Comparator — для альтернативных или сторонних.
Типичные ошибки
- ✗Возвращать boolean из
compareTo/compareвместо отрицательного, нуля или положительного int - ✗Делать естественный порядок несогласованным с
equals, ломая поведениеTreeSet/TreeMap - ✗Запихивать альтернативные порядки в
compareToвместо отдельных объектовComparator
Уточняющие вопросы
- →Почему естественный порядок типа должен быть согласован с его методом
equals? - →Как
Comparator.comparingиthenComparingстроят сортировку по нескольким ключам?
MiddleТеорияЧастоВ чём разница между HashMap и Hashtable в Java?
В чём разница между HashMap и Hashtable в Java?
HashMap несинхронизирован, допускает один null-ключ и любое число null-значений и работает быстрее — стандартный выбор для однопоточного кода. Hashtable — устаревший класс, чьи методы synchronized, поэтому он блокирует всю таблицу на вызов, и он запрещает null-ключи и значения, бросая NullPointerException. Для конкурентных словарей сегодня предпочитают ConcurrentHashMap, который блокирует более мелкие сегменты, а не всю структуру.
Типичные ошибки
- ✗Класть
null-ключ вHashtable, а затем удивлятьсяNullPointerException - ✗Использовать
Hashtableдля потокобезопасности в новом коде вместоConcurrentHashMap - ✗Считать, что поэлементная блокировка
Hashtableделает составную get-затем-put последовательность атомарной
Уточняющие вопросы
- →Почему
ConcurrentHashMapлучше масштабируется при конкуренции, чемsynchronizedHashtable? - →Как
HashMapотличает отсутствующий ключ от ключа, сопоставленного со значениемnull?
MiddleТеорияЧастоКак HashMap хранит записи, расширяется и преобразует в дерево?
Как HashMap хранит записи, расширяется и преобразует в дерево?
HashMap держит массив бакетов, индексируемый по хешу ключа. Сталкивающиеся ключи делят бакет как связный список. capacity — размер массива бакетов, а loadFactor (по умолчанию 0.75) — порог заполнения: когда size > capacity * loadFactor, map удваивает capacity и перехеширует. С Java 8 бакет с более чем 8 записями (в достаточно большой таблице) превращает список в красно-чёрное дерево, снижая худший случай поиска с O(n) до O(log n).
Типичные ошибки
- ✗Думать, что коллизии пробируются в другие бакеты, а не образуют цепочку в одном
- ✗Путать
loadFactor(порог расширения) с лимитом ёмкости на бакет - ✗Считать, что treeification превращает в дерево всю map, а не один бакет
Уточняющие вопросы
- →Почему treeification требует ещё и достаточно большой таблицы, а не только длинного бакета?
- →Как плохой
hashCodeсводит на нет среднююO(1)и форсирует treeification?
MiddleТеорияЧастоВ чём разница между HashSet и TreeSet в Java, и когда что брать?
В чём разница между HashSet и TreeSet в Java, и когда что брать?
HashSet хранит элементы через хеширование, давая в среднем O(1) на add, remove и contains, но без порядка — порядок обхода не определён. TreeSet опирается на красно-чёрное дерево, хранящее элементы отсортированными по естественному порядку Comparable или заданному Comparator, что стоит O(log n) на операцию, но даёт диапазонные запросы вроде first, ceiling и headSet. HashSet берут ради скорости, TreeSet — когда нужен отсортированный порядок.
Типичные ошибки
- ✗Ожидать от
HashSetпредсказуемого порядка обхода, включая порядок вставки - ✗Класть в
TreeSetэлементы без естественного порядка, не передавComparator - ✗Забывать, что
HashSetопирается наhashCode/equals, аTreeSet— на сравнение
Уточняющие вопросы
- →Почему
TreeSetбросаетClassCastExceptionдля взаимно несравнимых элементов? - →Когда
LinkedHashSetподойдёт лучше, чемHashSetилиTreeSet?
MiddleТеорияЧастоВ чём разница между ArrayList и Vector в Java?
В чём разница между ArrayList и Vector в Java?
И ArrayList, и Vector — динамические массивы с одинаковым O(1) доступом по индексу, но Vector — устаревший класс, чьи методы synchronized, так что каждый вызов берёт блокировку даже в однопоточном коде, теряя производительность. ArrayList несинхронизирован и сегодня является выбором по умолчанию. Vector к тому же удваивает ёмкость при росте, а ArrayList — наполовину. Для потокобезопасности предпочитают synchronizedList или CopyOnWriteArrayList.
Типичные ошибки
- ✗Считать поэлементную блокировку
Vectorдостаточной для составных операций вроде проверь-затем-добавь - ✗Брать
Vectorв новом коде ради потокобезопасности вместо современных конкурентных коллекций - ✗Полагать, что
VectorиArrayListнаращивают ёмкость с одинаковым коэффициентом
Уточняющие вопросы
- →Почему синхронизация методов
Vectorне делает последовательность проверь-и-действуй атомарной? - →Как
CopyOnWriteArrayListдостигает потокобезопасности иначе, чемVector?
SeniorПроизводительностьИногдаЧем отличается стоимость get, add и contains у реализаций List, Set и Map?
Чем отличается стоимость get, add и contains у реализаций List, Set и Map?
ArrayList даёт get по индексу за O(1) и амортизированное O(1) на добавление в конец, но contains сканирует за O(n). LinkedList даёт O(1) только на концах; get по индексу и contains — O(n). HashMap и HashSet в среднем дают O(1) на все три операции, деградируя до O(log n) в бакете-дереве и до O(n) при вырожденном hashCode. TreeMap и TreeSet — O(log n) на любую операцию в обмен на отсортированный порядок.
Типичные ошибки
- ✗Считать, что
LinkedListиндексируется заO(1), раз это связная структура - ✗Называть
O(1)худшим случаемHashMap, игнорируя коллизии и плохойhashCode - ✗Забывать, что
containsу любогоList— линейный проход, а не поиск по хешу
Уточняющие вопросы
- →Почему добавление в конец
ArrayList— амортизированноеO(1), а не простоO(1)? - →Когда
LinkedListреально выигрывает уArrayList, несмотря на худшую локальность кеша?
SeniorТеорияИногдаВ чём разница между fail-fast и fail-safe итераторами в Java?
В чём разница между fail-fast и fail-safe итераторами в Java?
Fail-fast итератор бросает ConcurrentModificationException, обнаружив структурное изменение коллекции в ходе обхода — он отслеживает счётчик модификаций и сверяет его на каждом шаге. ArrayList и HashMap ведут себя так. Fail-safe итератор вместо этого работает над копией или снимком, поэтому параллельные правки не мешают и он не бросает исключение — так делают CopyOnWriteArrayList и ConcurrentHashMap, ценой того, что поздние изменения не видны.
Типичные ошибки
- ✗Удалять через
removeколлекции в цикле вместо собственногоremoveитератора - ✗Считать fail-fast обнаружение гарантией потокобезопасности, а не проверкой по мере сил
- ✗Ожидать, что снимочный fail-safe итератор отразит правки, сделанные после начала обхода
Уточняющие вопросы
- →Как поле
modCountлежит в основе fail-fast обнаружения, и почему оно лишь по мере сил? - →Какие компромиссы по памяти и согласованности делает
CopyOnWriteArrayListради fail-safe?
SeniorТеорияИногдаВ чём разница между поверхностной и глубокой копией в Java?
В чём разница между поверхностной и глубокой копией в Java?
Поверхностная копия дублирует внешний контейнер, но разделяет те же ссылки на вложенные элементы, так что копия и оригинал указывают на одни внутренние объекты — изменение элемента у одного меняет его у другого. Глубокая копия рекурсивно клонирует весь граф объектов, давая копии собственные независимые внутренние объекты, так что изменения не протекают. clone и конструкторы копирования по умолчанию поверхностны; глубокая требует явного рекурсивного дублирования.
Типичные ошибки
- ✗Считать, что
cloneколлекции или конструктор копирования глубоко копирует содержащиеся элементы - ✗Думать, что глубокая копия графа из неизменяемых элементов отличается на практике от поверхностной
- ✗Забывать, что циклические ссылки могут заставить наивную рекурсивную глубокую копию зациклиться
Уточняющие вопросы
- →Почему поверхностной копии достаточно, когда каждый вложенный элемент неизменяем?
- →Каковы компромиссы глубокой копии через сериализацию против рукописного рекурсивного клонирования?
MiddleКодРедкоСтек с push, pop и getMax — все за O(1)
Стек с push, pop и getMax — все за O(1)
Держите два стека. Основной хранит значения; второй стек max хранит на каждом уровне максимум всего, что было добавлено к этому моменту. В push также кладите max(value, maxStack.peek()) на стек максимумов. В pop снимайте оба. getMax возвращает maxStack.peek(). Поскольку максимум пересчитан и сохранён на каждый элемент, он остаётся корректным после pop, а все три операции — O(1).
Типичные ошибки
- ✗Кешировать единственный максимум, который становится неверным после его удаления
- ✗Брать сортированную структуру, чьи операции на деле не
O(1) - ✗Сканировать элементы в
getMax, делая егоO(n)
Уточняющие вопросы
- →Почему единственный кешированный максимум ломается после
pop, а парный стек нет? - →Как расширить это, чтобы также возвращать минимум за
O(1)?
MiddleКодРедкоМинимум в скользящем окне с быстрыми add и getMin
Минимум в скользящем окне с быстрыми add и getMin
Держите TreeMap<value,count> плюс FIFO-очередь со значениями окна в порядке вставки. В add, если окно полно, извлеките самое старое из очереди и уменьшите (или удалите) его счётчик в map; затем увеличьте счётчик нового значения и поставьте его в очередь. getMin возвращает map.firstKey(). Каждая операция O(log w); монотонный deque даёт амортизированную O(1).
Типичные ошибки
- ✗Пересканировать всё окно для минимума на каждый
getMin, что даётO(w) - ✗Использовать heap и считать, что его голова — это и самый старый элемент для удаления
- ✗Хранить только один текущий минимум, что ломается, когда он вытесняется
Уточняющие вопросы
- →Почему
TreeMapиз значения в счётчик корректно обрабатывает дубликаты значений? - →Как монотонный deque достигает амортизированной
O(1)для этой задачи?