Коллекции в Java: Collection Framework и иерархия коллекций
Попробуйте написать Collection<String> names = new HashMap<String, String>(); — компилятор ответит incompatible types: HashMap<String,String> cannot be converted to Collection<String>. HashMap входит в Collection Framework, но интерфейс Collection не реализует. Чтобы такие сюрпризы не случались, сначала нужно увидеть иерархию коллекций целиком.
Collection Framework (Java Collections Framework) — это набор интерфейсов, реализаций и алгоритмов из пакета java.util для хранения и обработки групп объектов: списков, множеств, очередей и отображений «ключ–значение». В отличие от массива, размер коллекции меняется динамически, а готовые методы позволяют искать, сортировать и фильтровать элементы без ручного кода. Коллекции появились в J2SE 1.2 (1998), обобщённые типы в них — в Java 5.
Из чего состоит Collection Framework
Collection Framework в Java состоит из трёх частей: интерфейсов, которые описывают поведение коллекции (список, множество, очередь, отображение), реализаций — готовых классов вроде ArrayList и HashMap, и алгоритмов — статических методов для сортировки, поиска и перемешивания.
| Часть | Что это | Примеры | Где разбирается |
|---|---|---|---|
| Интерфейсы | Контракт: какие операции поддерживает коллекция | Collection, List, Set, Queue, Deque, Map | Интерфейс Collection |
| Реализации | Классы с конкретной структурой данных внутри | ArrayList, LinkedList, HashSet, TreeSet, ArrayDeque, HashMap | Уроки по каждому классу — см. карту модуля |
| Алгоритмы | Статические методы классов Collections и Arrays | Collections.sort(), binarySearch(), shuffle(), max() | Методы класса Collections |
Переменную принято объявлять типом интерфейса, а реализацию указывать только в new: List<String> names = new ArrayList<>();. Тогда реализацию можно заменить одной строкой, не трогая остальной код. Сами структуры данных (список, стек, очередь, множество) без привязки к Java описаны в уроке Структуры данных в Java.
Иерархия коллекций Java
Иерархия коллекций Java состоит из двух независимых веток. Первая начинается с интерфейса Collection (он расширяет Iterable) и делится на List, Set и Queue. Вторая — интерфейс Map для пар «ключ–значение»: он входит в Collection Framework, но Collection не наследует.

Та же иерархия в текстовом виде (стрелка → означает «расширяет / реализует», после тире указаны основные классы):
Iterable<T>
└── Collection<E>
├── List<E> — ArrayList, LinkedList, Vector → Stack
├── Set<E> — HashSet → LinkedHashSet
│ └── SortedSet<E> → NavigableSet<E> — TreeSet
└── Queue<E> — PriorityQueue
└── Deque<E> — ArrayDeque, LinkedList
Map<K,V> — HashMap → LinkedHashMap, Hashtable
└── SortedMap<K,V> → NavigableMap<K,V> — TreeMap Обратите внимание: LinkedList встречается в двух ветках — он реализует и List, и Deque. Vector, Stack и Hashtable — классы из Java 1.0, встроенные в иерархию задним числом; в новом коде их не используют (урок Legacy Classes).
Что изменилось в Java 21
В Java 21 (JEP 431) в иерархию добавлены интерфейсы SequencedCollection, SequencedSet и SequencedMap для коллекций с определённым порядком. Их расширяют List, Deque, SortedSet, SortedMap, а реализуют также LinkedHashSet и LinkedHashMap. Отсюда методы getFirst(), getLast(), removeFirst(), removeLast() и reversed() у обычного ArrayList. На Java 17 вызов list.getFirst() не скомпилируется — там пишут list.get(0).
Основные интерфейсы коллекций
Основных интерфейсов коллекций шесть: List (упорядоченный список с индексами), Set (множество без дубликатов), SortedSet/NavigableSet (отсортированное множество), Queue (очередь), Deque (двусторонняя очередь и стек) и Map (пары «ключ–значение»). Различаются они тем, допускают ли дубликаты и в каком порядке отдают элементы.
| Интерфейс | Что моделирует | Дубликаты | Порядок элементов | Урок |
|---|---|---|---|---|
Collection | Корень ветки: add(), remove(), contains(), size(), iterator() | Зависит от наследника | Зависит от наследника | Collection |
List | Последовательность с доступом по индексу | Да | Порядок вставки | List и ArrayList |
Set | Множество уникальных элементов | Нет (add() вернёт false) | Не гарантирован (HashSet) или порядок вставки (LinkedHashSet) | Set, HashSet |
SortedSet / NavigableSet | Отсортированное множество с поиском ближайших элементов | Нет | По возрастанию или по Comparator | SortedSet и TreeSet |
Queue | Очередь: добавление в хвост, извлечение из головы | Да | FIFO или по приоритету (PriorityQueue) | Queue и Deque |
Deque | Двусторонняя очередь; заменяет класс Stack | Да | FIFO или LIFO — с обоих концов | ArrayDeque |
Map | Отображение уникальных ключей на значения | Ключи — нет, значения — да | Зависит от реализации | Map |
Как сравниваются элементы в отсортированных коллекциях, объясняет урок Comparable и Comparator, а как их перебирать — урок Iterator.
Какую реализацию выбрать
Реализацию выбирают по трём вопросам: нужен ли доступ по индексу, важен ли порядок элементов и нужна ли сортировка. В большинстве задач хватает ArrayList, HashSet, HashMap и ArrayDeque. Остальные классы нужны, когда требуется особый порядок или быстрые вставки на концах списка.
| Класс | Внутри | Порядок | null | Основные операции |
|---|---|---|---|---|
ArrayList | Динамический массив | Вставки | Да | get(i) O(1), добавление в конец O(1) в среднем, вставка в середину O(n) |
LinkedList | Двусвязный список | Вставки | Да | get(i) O(n), добавление/удаление на концах O(1) |
ArrayDeque | Кольцевой массив | Вставки (с обоих концов) | Нет — NullPointerException | Операции на концах O(1) |
PriorityQueue | Двоичная куча | По приоритету при извлечении | Нет — NullPointerException | offer()/poll() O(log n), peek() O(1) |
HashSet | Хеш-таблица (внутри HashMap) | Не гарантирован | Один null | add()/contains() O(1) в среднем |
LinkedHashSet | Хеш-таблица + связный список | Вставки | Один null | O(1) в среднем |
TreeSet | Красно-чёрное дерево | Сортировка | Нет — NullPointerException при естественном порядке | O(log n) |
HashMap | Хеш-таблица | Не гарантирован | Один null-ключ, любые null-значения | get()/put() O(1) в среднем |
LinkedHashMap | Хеш-таблица + связный список | Вставки | Как у HashMap | O(1) в среднем |
TreeMap | Красно-чёрное дерево | Сортировка ключей | null-ключ — NullPointerException при естественном порядке | O(log n) |
Упорядоченные и отсортированные коллекции
Столбец «Порядок» в таблице описывает два разных свойства. Упорядоченная (ordered) коллекция перебирает элементы в предсказуемом порядке, который не зависит от их значений: по индексу или по порядку вставки. Отсортированная (sorted) коллекция сама расставляет элементы по их значениям — в естественном порядке (Comparable) или по правилу Comparator.
| Сочетание | Возможно? | Примеры |
|---|---|---|
| Неупорядоченная и неотсортированная | Да | HashSet, HashMap |
| Упорядоченная, но неотсортированная | Да | ArrayList, LinkedList, LinkedHashSet, LinkedHashMap |
| Упорядоченная и отсортированная | Да | TreeSet, TreeMap |
| Отсортированная, но неупорядоченная | Нет | — сортировка сама задаёт порядок, поэтому любая отсортированная коллекция упорядочена |
Set<String> linked = new LinkedHashSet<>(List.of("pear", "apple", "fig"));
Set<String> sorted = new TreeSet<>(List.of("pear", "apple", "fig"));
System.out.println(linked); // [pear, apple, fig] — порядок вставки
System.out.println(sorted); // [apple, fig, pear] — по алфавиту Если важно, в каком порядке элементы вернутся при обходе, «упорядоченной» коллекции может оказаться недостаточно: LinkedHashSet сохранит порядок добавления, но не отсортирует данные. PriorityQueue — особый случай: poll() всегда отдаёт наименьший элемент, но итератор обходит кучу без сортировки.
Почему LinkedList проигрывает ArrayList при доступе по индексу, разобрано в уроке LinkedList, а зачем элементам HashSet и ключам HashMap методы equals() и hashCode() — в уроке Set и HashSet.
Обобщённые коллекции
Все интерфейсы и классы Collection Framework обобщённые (с Java 5): тип элементов указывают в угловых скобках, например List<String>. Компилятор не даст положить в такую коллекцию объект другого типа, а при чтении не потребуется приведение типа. Коллекция без параметра (raw type) компилируется только с предупреждением и ломается уже во время выполнения.
import java.util.ArrayList;
import java.util.List;
public class RawListExample {
public static void main(String[] args) {
List raw = new ArrayList(); // raw type: компилятор выдаст только warning
raw.add("Java");
raw.add(42); // ошибки нет — тип элементов не проверяется
for (Object item : raw) {
String s = (String) item; // на втором элементе — исключение
System.out.println(s.toUpperCase());
}
}
} Вывод:
JAVA
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at RawListExample.main(RawListExample.java:11) С объявлением List<String> list = new ArrayList<>(); строка list.add(42) не скомпилируется — ошибка найдена до запуска. Примитивы (int, double) в коллекции не кладут: используется класс-обёртка (List<Integer>) и автоупаковка. Wildcard <? extends T> и <? super T> в параметрах методов объясняет урок принцип PECS.
Неизменяемые коллекции
Неизменяемая (unmodifiable) коллекция — это коллекция, у которой методы add(), remove() и set() бросают UnsupportedOperationException. В современном Java их создают фабричными методами List.of(), Set.of(), Map.of() (Java 9) и List.copyOf() (Java 10). Различаются способы деталями: поддержкой null и связью с исходными данными.
| Способ | С версии | Что можно менять | null-элементы | Связь с источником |
|---|---|---|---|---|
Arrays.asList(...) | 1.2 | Только set(); add()/remove() — UnsupportedOperationException | Да | Представление массива: изменения видны в обе стороны |
Collections.unmodifiableList(list) | 1.2 | Ничего | Да | Представление: изменения исходного списка видны |
List.of(...), Set.of(...), Map.of(...) | 9 | Ничего | Нет — NullPointerException (даже у contains(null)) | Нет |
List.copyOf(collection) | 10 | Ничего | Нет — NullPointerException | Нет, это копия |
stream.toList() | 16 | Ничего | Да | Нет |
Что выведет этот код?
import java.util.Arrays;
import java.util.List;
public class UnmodifiableExample {
public static void main(String[] args) {
List<String> fixed = Arrays.asList("a", "b");
fixed.set(0, "z");
System.out.println(fixed);
try {
fixed.add("c");
} catch (UnsupportedOperationException e) {
System.out.println("Arrays.asList: add() запрещён");
}
List<String> immutable = List.of("a", "b");
try {
immutable.set(0, "z");
} catch (UnsupportedOperationException e) {
System.out.println("List.of: set() запрещён");
}
}
} [z, b]
Arrays.asList: add() запрещён
List.of: set() запрещён Arrays.asList() возвращает список фиксированного размера, а не неизменяемый: заменить элемент можно, изменить размер — нет. Почему изменения такого списка попадают в исходный массив, показано в уроке Backed Collections. Если нужен обычный изменяемый список с начальными элементами, оберните фабрику: new ArrayList<>(List.of("a", "b")).
Потокобезопасные коллекции Java
Обычные ArrayList, HashSet и HashMap не потокобезопасны: при одновременной записи из нескольких потоков данные теряются или итерация падает с ConcurrentModificationException. Для многопоточного кода в Java есть три группы коллекций: унаследованные синхронизированные классы, обёртки Collections.synchronizedXxx() и классы пакета java.util.concurrent (Java 5).
| Коллекция | Как обеспечивается потокобезопасность | Когда использовать |
|---|---|---|
Vector, Hashtable | Каждый метод synchronized | Только в старом коде; замена — классы ниже |
Collections.synchronizedList(), synchronizedMap() | Обёртка с одной блокировкой на все методы; перебор нужно вручную заключать в synchronized (list) { ... } | Редкий доступ из нескольких потоков к готовой коллекции |
ConcurrentHashMap | Блокировка отдельных корзин и CAS-операции; чтение без блокировок. null-ключи и значения запрещены | Основной выбор для общей Map (кеши, счётчики) |
CopyOnWriteArrayList, CopyOnWriteArraySet | Каждая запись копирует внутренний массив; итератор работает со снимком и не бросает ConcurrentModificationException | Много чтений, редкие изменения (списки слушателей) |
ConcurrentLinkedQueue, ConcurrentLinkedDeque | Неблокирующие алгоритмы на CAS | Общая очередь без ожидания |
ArrayBlockingQueue, LinkedBlockingQueue (интерфейс BlockingQueue) | put() ждёт свободного места, take() — появления элемента | Схема «производитель–потребитель» |
ConcurrentSkipListMap, ConcurrentSkipListSet | Список с пропусками (skip list) без глобальной блокировки | Потокобезопасная замена TreeMap/TreeSet |
Пример: два потока по 10 000 раз увеличивают общий счётчик в ConcurrentHashMap. Метод merge() выполняет чтение и запись атомарно, поэтому ни одно увеличение не теряется:
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentCounter {
public static void main(String[] args) throws InterruptedException {
Map<String, Integer> counts = new ConcurrentHashMap<>();
Runnable task = () -> {
for (int i = 0; i < 10_000; i++) {
counts.merge("clicks", 1, Integer::sum);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counts.get("clicks")); // Вывод: 20000
}
} Если заменить ConcurrentHashMap на HashMap, результат станет непредсказуемым: обычно меньше 20000, потому что потоки перезаписывают изменения друг друга. Синхронизация потоков подробно разбирается в уроке Синхронизация потоков.
Уроки модуля по порядку
Модуль «Коллекции» идёт от общего к частному: сначала корневой интерфейс и структуры данных, затем каждая ветка иерархии — списки, множества, очереди, отображения, и в конце инструменты для работы с ними.
| Урок | Что в нём разбирается |
|---|---|
| Интерфейс Collection | Методы, общие для всех коллекций |
| Структуры данных | Список, стек, очередь, множество — без привязки к классам |
| List и ArrayList | Методы List, устройство ArrayList, toArray() |
| Set, HashSet, LinkedHashSet | Хеш-таблица, контракт equals() и hashCode() |
| SortedSet и TreeSet | Отсортированные множества, subSet(), headSet() |
| Comparable и Comparator | Как задать порядок сортировки объектов |
| NavigableSet | ceiling(), floor(), higher(), lower() |
| Queue и Deque | Методы очередей: offer(), poll(), peek(), push(), pop() |
| ArrayDeque | Очередь и стек на массиве |
| LinkedList | Двусвязный список, сравнение с ArrayList |
| PriorityQueue | Очередь с приоритетами |
| Принцип PECS | Wildcard extends и super в коллекциях |
| Iterator | Перебор коллекций, ConcurrentModificationException |
| Map | HashMap, TreeMap, LinkedHashMap, SortedMap, NavigableMap |
| Методы класса Collections | sort(), binarySearch(), shuffle(), frequency() |
| Backed Collections | Представления коллекций и Arrays.asList() |
| Legacy Classes | Vector, Stack, Hashtable, Properties, Enumeration |
Презентация с видео на Patreon →
Часто задаваемые вопросы
Чем Collection отличается от Collections?
Collection — интерфейс, корень ветки списков, множеств и очередей. Collections — служебный класс со статическими методами: sort(), reverse(), unmodifiableList(), synchronizedMap() и другими. Создать объект Collections нельзя, а Collection реализуют все списки, множества и очереди.
Почему Map не наследует интерфейс Collection?
Элемент Map — пара «ключ–значение», а методы Collection рассчитаны на одиночные элементы: add(E) для пары не имеет смысла. Поэтому Map — отдельная ветка иерархии, а с остальными коллекциями её связывают представления keySet(), values() и entrySet().
Можно ли хранить int в коллекции?
Напрямую нельзя: List<int> не скомпилируется, используется List<Integer> с автоупаковкой. Тут легко ошибиться: для List<Integer> вызов list.remove(1) удалит элемент с индексом 1, а не число 1. Чтобы удалить значение, пишут list.remove(Integer.valueOf(1)).
Какие коллекции использовать по умолчанию?
Для списка — ArrayList, для множества — HashSet, для отображения — HashMap, для очереди и стека — ArrayDeque вместо устаревшего Stack. Остальные реализации берут, когда нужен порядок вставки, сортировка или потокобезопасность. Сравнение классов — в разделе Какую реализацию выбрать.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии