Коллекция в Java — это объект, который хранит группу других объектов (элементов) и дает единый набор методов работы с ними: добавить, удалить, найти, перебрать. Набор таких интерфейсов и классов из пакета java.util называют Java Collections Framework (JCF); он появился в JDK 1.2 и остается основой стандартной библиотеки в Java 25 LTS.
Содержание
Разберу иерархию интерфейсов, ключевые реализации, сравню их по сложности операций и покажу, как безопасно обходить коллекцию и зачем нужны обобщения (generics) — все ради выбора структуры под задачу.
Иерархия интерфейсов
В основе JCF лежит интерфейс Iterable<E> — все, что его реализует, можно перебрать циклом for-each. От него наследуется Collection<E> — контракт «группа элементов» с методами add, remove, size, contains, iterator. От него расходятся три ветви:
- List — упорядоченный список с доступом по индексу, дубликаты разрешены.
- Set — множество без дубликатов, индекса нет.
- Queue и Deque — очереди, доступ к краям, а не по индексу.
Map<K, V> стоит отдельно: это отображение «ключ -> значение», и оно не наследует Collection. У карты нет метода add(E), вместо него put(K, V). Map — часть фреймворка, но не подтип коллекции; это частая путаница у новичков.
Минимальный рабочий пример
Пример ниже показывает разницу List и Set.
import java.util.*;
public class CollectionsDemo {
public static void main(String[] args) {
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("Python");
languages.add("Java"); // дубликат разрешен
System.out.println(languages); // [Java, Python, Java]
Set<String> unique = new HashSet<>(languages);
System.out.println(unique.size()); // 2 (дубликат отброшен)
}
}
Список сохранил оба «Java», а HashSet схлопнул дубликаты до двух элементов. Порядок в HashSet не гарантирован, поэтому я печатаю только size(), а не сам набор.
List: ArrayList или LinkedList
List — самый ходовой интерфейс. Две основные реализации решают одну задачу по-разному.
ArrayList — массив с автоматическим ростом. get(i) читает ячейку массива — O(1). Но вставка или удаление в середине сдвигает все последующие элементы — O(n).
LinkedList — двусвязный список узлов. Вставка и удаление в известной позиции — O(1) (перецепить ссылки), но get(i) требует пройти список от края — O(n). Также реализует Deque.
Правило: чаще читаете по индексу и добавляете в конец — берите ArrayList (он экономнее по памяти). LinkedList выигрывает лишь при частых вставках и удалениях с краев без обращений по индексу.
Set: убрать дубликаты
Set хранит уникальные элементы. Уникальность для HashSet задают методы hashCode() и equals() — для объектов своих классов их нужно переопределить, иначе дубликаты не распознаются. Три реализации отличаются порядком обхода:
- HashSet — порядок не определен, операции O(1).
- LinkedHashSet — сохраняет порядок добавления.
- TreeSet — элементы отсортированы (реализует
SortedSet), операции O(log n).
Set<String> tree = new TreeSet<>(List.of("banana", "apple", "cherry"));
tree.add("apple"); // повтор игнорируется
System.out.println(tree); // [apple, banana, cherry]
TreeSet вывел элементы в алфавитном порядке независимо от вставки и отбросил повтор. Уточнение: TreeSet не принимает null (бросит NullPointerException), а HashSet допускает один null.
Map: пары ключ-значение
Map связывает уникальный ключ со значением. Повторный put с тем же ключом перезаписывает значение, а не добавляет пару.
- HashMap — порядок ключей не определен, доступ по ключу O(1); один
null-ключ допустим. - LinkedHashMap — сохраняет порядок вставки ключей.
- TreeMap — ключи отсортированы (красно-черное дерево, реализует
SortedMap), доступ O(log n);null-ключ не допускается.
Map<String, Integer> count = new TreeMap<>();
for (String w : new String[]{"java", "map", "java", "set", "java"}) {
count.merge(w, 1, Integer::sum); // +1 или инициализация единицей
}
System.out.println(count); // {java=3, map=1, set=1}
Метод merge удобен для подсчета: нет ключа — кладет 1, иначе прибавляет к текущему. TreeMap выдал ключи по алфавиту; с HashMap порядок был бы не определен.
Queue и Deque
Queue — очередь с дисциплиной FIFO (первым пришел — первым вышел). Deque (double-ended queue, «дэк») — двусторонняя очередь: добавлять и извлекать можно с обоих концов, поэтому она заменяет и очередь, и стек. Для стека и очереди рекомендуют ArrayDeque (push/pop/peek для стека, offer/poll для очереди), а не устаревший Stack. PriorityQueue выдает элементы по приоритету, а не по порядку прихода. Граница: ArrayDeque не хранит null.
Обобщения (generics)
Угловые скобки <E> в List<String> — это параметр типа: он говорит компилятору, какого типа элементы в коллекции, и переносит проверку типов на этап компиляции. Без параметра (сырой тип, raw type) ошибка всплывет во время работы программы:
List raw = new ArrayList(); // сырой тип, без параметра
raw.add("text");
raw.add(42);
String s = (String) raw.get(1); // берем Integer, приводим к String
Результат:
Exception in thread "main" java.lang.ClassCastException:
class java.lang.Integer cannot be cast to class java.lang.String
С обобщением та же ошибка не доживет до запуска: в List<String> typed компилятор откажет добавить typed.add(42) с ошибкой incompatible types. Ромбовидный оператор <> справа позволяет не повторять тип — компилятор выводит его из левой части. В этом и смысл generics: безопасность типов без ручных приведений.
Обход коллекции
Перебрать коллекцию можно циклом for-each, явным Iterator или forEach с лямбдой. Типичная ловушка — удалять элементы внутри for-each. Так делать нельзя:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
for (Integer n : numbers) {
if (n % 2 == 0) {
numbers.remove(n); // изменяем список во время обхода
}
}
Результат:
Exception in thread "main" java.util.ConcurrentModificationException
Правильно — удалять через removeIf или сам итератор:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
numbers.removeIf(n -> n % 2 == 0);
System.out.println(numbers); // [1, 3, 5]
removeIf убрал четные числа и не бросил исключение. Тот же результат дает Iterator с it.remove() вместо list.remove().
Как выбрать реализацию: таблица
«Доступ» — по индексу для List и по ключу для Map (у Set индекса нет); сложности для среднего случая.
| Реализация | Интерфейс | Доступ | Вставка/удаление | Порядок обхода | null |
|---|---|---|---|---|---|
| ArrayList | List | O(1) по индексу | O(n) в середину, O(1) в конец (амортизированно) | порядок вставки | да |
| LinkedList | List, Deque | O(n) по индексу | O(1) в известной позиции у края | порядок вставки | да |
| HashSet | Set | — | O(1) в среднем | не определен | один null |
| LinkedHashSet | Set | — | O(1) в среднем | порядок вставки | один null |
| TreeSet | SortedSet | — | O(log n) | по сортировке | нет |
| HashMap | Map | O(1) по ключу | O(1) в среднем | не определен | один null-ключ |
| LinkedHashMap | Map | O(1) по ключу | O(1) в среднем | порядок вставки | один null-ключ |
| TreeMap | SortedMap | O(log n) по ключу | O(log n) | по сортировке ключей | нет null-ключа |
Оговорка: O(1) у HashMap/HashSet — это среднее при хорошем распределении хешей. При плохом hashCode() элементы скапливаются в одной корзине и операции деградируют, поэтому корректный hashCode() важен и для скорости.
Выводы
- JCF строится на
Collection(List, Set, Queue) и отдельно стоящемMap; выбирают сначала интерфейс под задачу, потом реализацию под нагрузку. - ArrayList быстр на доступе по индексу, LinkedList — на вставках у краев; по умолчанию берут ArrayList.
- Set убирает дубликаты, Map хранит пары ключ-значение; у обоих есть варианты без порядка (Hash), с порядком вставки (LinkedHash) и отсортированные (Tree).
- Обобщения переносят проверку типов на компиляцию; сырые типы оставляют ошибку до времени выполнения.
- Удалять элементы во время обхода нужно через
removeIfилиIterator.remove(), иначе получитеConcurrentModificationException.
Где применяется / связь с практикой
Коллекции — ежедневный инструмент Java-разработчика: кэш на HashMap, список товаров на ArrayList, уникальные теги на HashSet, очередь задач на ArrayDeque. Умение выбрать структуру по сложности операций влияет на скорость и память сервиса, и его проверяют почти на каждом собеседовании на джуна.
Освойте тему на практике
Разобрать коллекции на практике, вместе с обобщениями и потоками (Stream API), помогает системный курс с обратной связью от инженеров: курс «Java-разработчик Basic». Посмотреть формат подачи можно на открытых уроках Otus.
FAQ
Чем отличается Collection от Collections?
Collection (без s) — интерфейс, вершина иерархии. Collections (с s) — служебный класс со статическими методами: Collections.sort(list), Collections.max(coll). Имена похожи, но это разные сущности.
Как получить неизменяемую коллекцию?
Для фиксированных наборов есть фабрики List.of(...), Set.of(...), Map.of(...) — попытка add к ним бросит UnsupportedOperationException. Обернуть существующую можно через Collections.unmodifiableList(list).
Что выбрать для многопоточного доступа?
Обычные HashMap/ArrayList не потокобезопасны. Для конкурентного доступа берут ConcurrentHashMap и CopyOnWriteArrayList из пакета java.util.concurrent — они масштабируются лучше, чем обертки Collections.synchronizedMap.



