Массив в Java фиксированного размера: создал на 10 элементов — больше не положишь. В реальном коде заранее размер обычно неизвестен: пользователей может быть три, а может три тысячи. Для этого есть коллекции — набор готовых структур данных, которые сами растут, ищут и хранят элементы. Разберём три главных вида и поймём, когда какой брать.
Три коллекции получают одни и те же три добавления. List кладёт всё подряд и помнит порядок; Set видит повтор и молча его отбрасывает; Map по тому же ключу заменяет значение. Поэтому в конце размеры разные: 3, 2 и 2.
Зачем нужны коллекции
Почти любая программа что-то накапливает: список заказов, набор уникальных тегов, соответствие «логин → пользователь». Писать такие структуры руками — долго и легко ошибиться. В стандартной библиотеке Java они уже есть, отлажены годами и работают быстро.
Все коллекции живут в пакете java.util. В основе — три интерфейса: List (список), Set (множество) и Map (словарь). Интерфейс описывает, что коллекция умеет, а конкретные классы (ArrayList, HashMap и т. д.) — как это сделано внутри.
Короткая формула: упорядоченная последовательность с возможными повторами — List; набор уникальных значений — Set; пары «ключ → значение» — Map.
Вот вся семья на одной картинке: сверху интерфейсы — то, что коллекция умеет, снизу классы — как это сделано внутри. Map стоит особняком: он не наследует Collection, потому что хранит не элементы, а пары.
Интерфейсы сверху, реализации снизу. По умолчанию берут ArrayList, HashSet и HashMap — остальные нужны, когда важен порядок обхода или сортировка.
List — упорядоченный список
List хранит элементы в том порядке, в каком вы их добавили, и допускает дубликаты. К любому элементу можно обратиться по индексу (как в массиве), начиная с нуля.
живой пример
import java.util.ArrayList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
List<String> cities = new ArrayList<>();
cities.add("Москва");
cities.add("Казань");
cities.add("Москва"); // дубликаты разрешены
System.out.println(cities.get(0)); // Москва
System.out.println(cities.size()); // 3
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Здесь List<String> — это «список строк». Угловые скобки — обобщённый тип (generics), он фиксирует, что внутри лежат именно String; подробнее — в отдельной статье про дженерики.
Реализации две. ArrayList внутри обычный массив, который сам расширяется: быстрый доступ по индексу, быстрое добавление в конец, и это выбор по умолчанию. LinkedList связный список, где каждый элемент знает соседей. Вставка в начало у него дешёвая, а привычное «зато быстрее вставляет в середину» миф: до места сначала надо дойти по цепочке ссылок, а ArrayList сдвигает хвост массива одной операцией копирования и почти всегда оказывается быстрее. LinkedList берут, только когда нужна очередь с обоих концов, и даже тогда обычно лучше ArrayDeque.
Короткая формула: сомневаешься — бери ArrayList.
Вставка в середину по шагам: ArrayList попадает на место сразу и сдвигает хвост, а LinkedList сначала идёт по ссылкам, и этот путь тем длиннее, чем больше список.
Как ArrayList растёт, стоит знать, потому что от этого зависят две его цены. Внутри лежит обычный массив, и при первом add под него выделяют десять ячеек. Когда место кончается, создаётся новый массив в полтора раза больше, и все элементы копируются в него: на миллионе элементов таких переездов около тридцати, и каждый копирует всё, что накопилось. Если размер известен заранее, его называют сразу, new ArrayList<>(100_000), и переездов не будет. Вторая цена в удалении и вставке не в конец: remove(0) сдвигает весь хвост массива на одну ячейку влево, и цикл, который так выкидывает элементы из начала большого списка, работает квадратично. Для очереди, где берут с начала, есть ArrayDeque, о ней ниже.
Set — множество уникальных значений
Set хранит только уникальные элементы: повторное добавление того же значения ничего не меняет. Это удобно, когда нужно убрать дубликаты или проверить «а есть ли уже такой». И проверка эта почти бесплатна: list.contains(x) идёт по списку до конца, а set.contains(x) считает хеш и заглядывает в одну ячейку — на тысяче элементов разница уже видна глазом.
живой пример
import java.util.HashSet;
import java.util.Set;
public class SetDemo {
public static void main(String[] args) {
Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("backend");
tags.add("java"); // повтор проигнорирован
System.out.println(tags.size()); // 2
System.out.println(tags.contains("java")); // true
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Реализации различаются одним: что происходит с порядком. HashSet порядок не гарантирует и поэтому самый быстрый, его берут по умолчанию. TreeSet держит элементы отсортированными, строки по алфавиту, числа по возрастанию, и нужен, когда обход должен идти в этом порядке. LinkedHashSet помнит порядок добавления.
Map — словарь «ключ → значение»
Map хранит пары: по ключу мгновенно достаётся значение. Это как телефонная книжка: по имени находите номер. Ключи уникальны, значения могут повторяться.
живой пример
import java.util.HashMap;
import java.util.Map;
public class MapDemo {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<>();
ages.put("Анна", 30);
ages.put("Иван", 25);
ages.put("Анна", 31); // тот же ключ — значение перезаписано
System.out.println(ages.get("Анна")); // 31
System.out.println(ages.getOrDefault("Пётр", 0)); // 0 — ключа нет
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Тот же выбор по порядку. HashMap порядок не гарантирует, это выбор по умолчанию. TreeMap держит ключи отсортированными. LinkedHashMap помнит порядок добавления, а если создать его третьим конструктором с accessOrder = true, порядок станет порядком обращений: только что прочитанный ключ уезжает в конец, а самый давно не нужный оказывается первым. На этом делают кэш с вытеснением, переопределив removeEldestEntry.
getOrDefault удобен, чтобы не получить null, когда ключа нет, — про обработку отсутствующих значений и Optional есть отдельные статьи. Сам null при этом не запрещён: HashMap спокойно принимает один null-ключ и сколько угодно null-значений. А TreeMap на null-ключе падает с NullPointerException — ему надо сравнивать ключи между собой, а null сравнивать не с чем.
Queue и Deque: очередь и стек
На схеме иерархии выше есть ветка, которую легко пропустить: Queue и Deque. Они нужны, когда порядок обработки важнее доступа по индексу. Обработать задачи в порядке поступления, откатить последние действия, обойти дерево в ширину — всё это очередь или стек, и ArrayList для них плохой выбор: брать с начала списка дорого, о чём сказано выше.
Queue это очередь «первым пришёл, первым вышел»: offer кладёт в конец, poll забирает с начала (и возвращает null, если пусто), peek подглядывает без изъятия. Deque двусторонняя: класть и брать можно с обоих концов, и та же структура работает как стек через push и pop. Реализация по умолчанию для обоих — ArrayDeque: кольцевой массив, быстрее LinkedList и без лишних объектов на элемент, но null в него класть нельзя. Отдельно стоит PriorityQueue: она отдаёт не первый добавленный, а наименьший по естественному порядку или компаратору, и на ней строят планировщики и обход «сначала самое срочное».
живой пример
import java.util.*;
public class QueueDemo {
public static void main(String[] args) {
Queue<String> tasks = new ArrayDeque<>();
tasks.offer("собрать");
tasks.offer("протестировать");
tasks.offer("выкатить");
System.out.println(tasks.poll() + " → " + tasks.peek() + ", осталось " + tasks.size());
Deque<String> undo = new ArrayDeque<>();
undo.push("ввод текста");
undo.push("выделение жирным");
System.out.println("отменяем: " + undo.pop() + ", следующим будет: " + undo.peek());
Queue<Integer> urgent = new PriorityQueue<>(List.of(5, 1, 3));
System.out.println("самое срочное: " + urgent.poll() + ", в очереди: " + urgent);
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Последняя строка показывает ловушку PriorityQueue: poll честно отдал единицу, а печать очереди даёт [3, 5] не потому, что она отсортирована, а потому, что так лёг внутренний массив; обход PriorityQueue итератором порядка не гарантирует, порядок даёт только poll. И для многопоточной работы эти классы не годятся: очереди, из которых один поток берёт, а другой кладёт, живут в java.util.concurrent и разобраны в фазе про многопоточность.
Как перебирать коллекции
Самый частый способ — цикл for-each: читается просто, индексы не нужны.
живой пример
import java.util.List;
public class ForEachDemo {
public static void main(String[] args) {
List<String> cities = List.of("Москва", "Казань", "Сочи");
for (String city : cities) { // «для каждого city из cities»
System.out.println(city);
}
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Для Map перебирают пары через entrySet():
живой пример
import java.util.Map;
public class EntriesDemo {
public static void main(String[] args) {
Map<String, Integer> ages = Map.of("Анна", 30, "Иван", 25);
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Под капотом for-each использует итератор (Iterator) — объект, который «проходит» по коллекции элемент за элементом. Напрямую он нужен в одном важном случае: если надо удалять элементы прямо во время обхода. Удалять через cities.remove(...) внутри for-each нельзя — будет ошибка ConcurrentModificationException. Правильно так:
живой пример
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IteratorDemo {
public static void main(String[] args) {
List<String> cities = new ArrayList<>(List.of("Москва", "Казань", "Сочи"));
Iterator<String> it = cities.iterator();
while (it.hasNext()) {
String city = it.next();
if (city.startsWith("К")) {
it.remove(); // безопасное удаление через итератор
}
}
System.out.println(cities); // [Москва, Сочи]
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Почему важны equals и hashCode
Положили свой объект в HashSet, тут же спросили contains с таким же по полям объектом и получили false. Так ведёт себя любой класс без двух методов: equals (равны ли два объекта по смыслу) и hashCode (число-«отпечаток», по которому коллекция находит нужную ячейку). HashSet и HashMap сравнивают элементы и ключи только через них, а не по ссылке в памяти; у String и чисел оба метода уже реализованы, поэтому примеры выше работают. Запись record получает их даром:
живой пример
import java.util.HashSet;
import java.util.Set;
public class PointDemo {
record Point(int x, int y) {} // record сам генерирует equals и hashCode
public static void main(String[] args) {
Set<Point> points = new HashSet<>();
points.add(new Point(1, 2));
System.out.println(points.contains(new Point(1, 2))); // true
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Если бы Point был обычным class без переопределённых equals/hashCode, результат был бы false: два разных объекта с одинаковыми координатами считались бы разными. Правило: если объект кладётся в Set или становится ключом Map, у него должны быть согласованные equals и hashCode. Самый простой способ получить их даром — сделать тип record; как он устроен — в статьях про ООП и современные возможности Java.
EnumMap и EnumSet: коллекции для enum-ключей
Когда ключ — это enum, обычная HashMap избыточна: хешировать нечего, множество ключей известно заранее. EnumMap хранит значения в обычном массиве по порядковому номеру константы — это быстрее HashMap, компактнее по памяти, а обход всегда идёт в порядке объявления констант. EnumSet устроен ещё экономнее: множество enum-значений упаковано в битовую маску. Правило простое: ключ или элемент — enum, значит EnumMap/EnumSet, а не HashMap/HashSet.
Вот обе коллекции на статусах заказа. Запустите и посмотрите на порядок вывода: он не случайный, а такой, в каком объявлены константы.
живой пример
import java.util.*;
public class EnumDemo {
enum Status { PENDING_PAYMENT, PAID, SHIPPED, DELIVERED, CANCELLED }
public static void main(String[] args) {
// сколько заказов в каждом статусе
Map<Status, Integer> byStatus = new EnumMap<>(Status.class);
for (Status s : List.of(Status.PAID, Status.PENDING_PAYMENT, Status.PAID, Status.DELIVERED)) {
byStatus.merge(s, 1, Integer::sum);
}
System.out.println(byStatus);
// какие статусы считаем «заказ ещё в работе»
Set<Status> active = EnumSet.of(Status.PENDING_PAYMENT, Status.PAID, Status.SHIPPED);
System.out.println("PAID в работе? " + active.contains(Status.PAID));
System.out.println("остальные: " + EnumSet.complementOf(EnumSet.copyOf(active)));
try {
byStatus.put(null, 1); // а вот так нельзя
} catch (NullPointerException e) {
System.out.println("null ключом в EnumMap не положить");
}
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Три вещи, которые видно по выводу. Порядок в EnumMap — это порядок объявления констант, а не порядок вставки: PENDING_PAYMENT напечатался первым, хотя добавляли его вторым. EnumSet.of перечисляет нужные значения, а complementOf даёт «все остальные» — удобно, когда правило описывают через исключения. И обе коллекции отказываются принимать null в качестве ключа или элемента: у HashMap это разрешено, здесь — NullPointerException, последняя строка вывода про это.
Неизменяемые коллекции
Иногда коллекцию нужно защитить от изменений — например, вернуть из метода так, чтобы вызывающий код её не испортил. Для этого есть фабричные методы List.of, Set.of, Map.of (с Java 9):
живой пример
import java.util.List;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> roles = List.of("admin", "user"); // неизменяемый список
try {
roles.add("guest");
} catch (UnsupportedOperationException e) {
System.out.println("UnsupportedOperationException: менять нельзя");
}
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
У этих фабрик есть ещё два свойства, о которые спотыкаются чаще, чем о запрет менять. null в них класть нельзя вообще: List.of(null) бросит NullPointerException на месте, а не вернёт список с дыркой. И у Map.of ровно десять пар потолком — дальше нужен Map.ofEntries(Map.entry(k, v), ...).
Такие коллекции иммутабельны: попытка что-то добавить или удалить бросит исключение. Это безопаснее и нагляднее — сразу видно, что данные менять не предполагается. Если позже нужна изменяемая копия — оберните: new ArrayList<>(List.of(...)).
Рядом с List.of живут два старших способа, и их путают с ним чаще всего. Arrays.asList("a", "b") даёт список фиксированного размера поверх массива: set работает и меняет исходный массив, а add и remove бросают UnsupportedOperationException. Это не неизменяемый список, это массив в одежде списка, и null в нём разрешён. Collections.unmodifiableList(list) даёт представление: менять через него нельзя, но исходный список остался у того, кто его передал, и его правки видны через обёртку. Так возвращают из метода коллекцию, которую вызывающий не должен править, не копируя её. А List.of и List.copyOf создают настоящую копию, которую не изменить ни через обёртку, ни через исходник.
живой пример
import java.util.*;
public class ThreeListsDemo {
public static void main(String[] args) {
List<String> fixed = Arrays.asList("a", "b");
fixed.set(0, "z");
try {
fixed.add("c");
} catch (UnsupportedOperationException e) {
System.out.println("asList: set можно, add нельзя: " + fixed);
}
List<String> source = new ArrayList<>(List.of("x"));
List<String> view = Collections.unmodifiableList(source);
source.add("y");
System.out.println("unmodifiableList видит правки исходника: " + view);
List<String> copy = List.copyOf(source);
source.add("w");
System.out.println("List.copyOf не видит: " + copy);
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Правило выбора: нужен снимок, который никто не поменяет, — List.of или List.copyOf; нужно отдать наружу живой список только для чтения — unmodifiableList; Arrays.asList оставьте для случаев, когда у вас уже есть массив и его надо передать туда, где ждут список.
Глубже: сортировка: Comparable, Comparator, thenComparing и nullsLastрасширенное
sorted упомянут в перечне операций, а в статье про дженерики мелькает Comparable; порядок элементов стоит разобрать целиком, потому что на нём держатся TreeMap, sort и половина задач тренажёра.
Два способа сказать, что больше. Comparable это естественный порядок, встроенный в сам класс: compareTo возвращает отрицательное, ноль или положительное; так устроены числа, строки (лексикографически, по кодам символов, поэтому «Яблоко» после «apple»), даты. Свой класс реализует Comparable<Money>, если у него есть один очевидный порядок. Comparator это порядок снаружи, объект, который сравнивает двоих: их может быть сколько угодно, по цене, по имени, по дате, и они не трогают класс.
живой пример
import java.util.*;
public class Sorting {
record Product(String name, Integer rating, double price) {}
public static void main(String[] args) {
List<Product> items = new ArrayList<>(List.of(
new Product("чай", 5, 120.0), new Product("кофе", null, 350.0),
new Product("какао", 5, 90.0), new Product("сок", 4, 120.0)));
items.sort(Comparator.comparing(Product::rating, Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(Product::price)
.thenComparing(Product::name));
items.forEach(System.out::println);
System.out.println(Collections.max(items, Comparator.comparingDouble(Product::price)).name());
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Comparator.comparing строит компаратор по ключу; thenComparing добавляет следующий критерий для равных; reversed() переворачивает, но с ловушкой: comparing(Product::price).reversed() переворачивает всё, что до него, поэтому обратный порядок для одного ключа задают вторым аргументом comparing, как в примере. nullsLast и nullsFirst оборачивают компаратор ключа и решают, куда деть null, иначе первый же null уронит сортировку с NullPointerException. TreeMap и TreeSet принимают компаратор в конструкторе, и для них равенство определяется им, а не equals: два товара с одинаковой ценой в TreeSet по цене это один элемент.
Контракт компаратора тот же, что у compareTo: согласованность и транзитивность; компаратор, который для a и b отвечает по-разному в зависимости от порядка аргументов, даёт IllegalArgumentException: Comparison method violates its general contract из недр sort, и это одна из самых непонятных ошибок для новичка. Сортировка List.sort и Collections.sort устойчива: равные по компаратору элементы сохраняют исходный порядок, и на этом строят сортировку по нескольким полям в несколько проходов, хотя thenComparing читается лучше.
Коротко
- Три базовых интерфейса:
List(порядок + дубликаты),Set(уникальные значения),Map(пары «ключ → значение»). - По умолчанию берите
ArrayList,HashSet,HashMap— они быстрые и покрывают большинство задач. LinkedListпочти никогда не нужен:ArrayListбыстрее даже там, где в теории должен проигрывать.TreeSet/TreeMap— когда нужна сортировка.- Перебор — через for-each; удаление во время обхода — только через
Iterator.remove(). - Для элементов
Setи ключейMapнужны согласованныеequalsиhashCode; проще всего —record. List.of,Set.of,Map.ofсоздают неизменяемые коллекции — удобно для защиты данных;nullв них не кладут, а уMap.ofпредел — десять пар.- Естественный порядок задаёт
Comparable, внешнийComparatorчерезcomparing,thenComparing,nullsLastиreverseOrderвторым аргументом;TreeSetсчитает равенство по компаратору, а нарушенный контракт роняетsort. ArrayListрастёт в полтора раза с копированием всего массива (размер известен — задайте сразу), аremove(0)сдвигает весь хвост.Queue(offer/poll/peek) иDeque(плюсpush/popкак стек) по умолчанию этоArrayDequeбезnull;PriorityQueueотдаёт наименьший только черезpoll, обход её не сортирует.Arrays.asListэто массив в одежде списка (setда,addнет),Collections.unmodifiableListэто представление живого списка,List.of/List.copyOfэто снимок.
Что почитать дальше
- ООП в Java — классы, объекты, наследование от
Objectи record. - Как устроена HashMap внутри — бакеты, коллизии и почему нужен hashCode.
- Дженерики (generics) — что означают
<String>и зачем они нужны. - Лямбды и Stream API — современная обработка коллекций без ручных циклов.