Массив в Java фиксированного размера: создал на 10 элементов — больше не положишь. В реальном коде заранее размер обычно неизвестен: пользователей может быть три, а может три тысячи. Для этого есть коллекции — набор готовых структур данных, которые сами растут, ищут и хранят элементы. Разберём три главных вида и поймём, когда какой брать.
Зачем нужны коллекции
Почти любая программа что-то накапливает: список заказов, набор уникальных тегов, соответствие «логин → пользователь». Писать такие структуры руками — долго и легко ошибиться. В стандартной библиотеке Java они уже есть, отлажены годами и работают быстро.
Все коллекции живут в пакете java.util. В основе — три интерфейса: List (список), Set (множество) и Map (словарь). Интерфейс описывает, что коллекция умеет, а конкретные классы (ArrayList, HashMap и т. д.) — как это сделано внутри.
Короткая формула: упорядоченная последовательность с возможными повторами — List; набор уникальных значений — Set; пары «ключ → значение» — Map.
List — упорядоченный список
List хранит элементы в том порядке, в каком вы их добавили, и допускает дубликаты. К любому элементу можно обратиться по индексу (как в массиве), начиная с нуля.
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 — внутри обычный массив, который сам расширяется. Берите его по умолчанию: быстрый доступ по индексу и быстрое добавление в конец. 95% случаев — это
ArrayList. - LinkedList — связный список: каждый элемент знает соседей. Выигрывает при частых вставках и удалениях в начале или середине, но доступ по индексу медленный (надо «дойти» до элемента). Нужен редко.
Короткая формула: сомневаешься — бери ArrayList.
Set — множество уникальных значений
Set хранит только уникальные элементы: повторное добавление того же значения ничего не меняет. Это удобно, когда нужно убрать дубликаты или проверить «а есть ли уже такой».
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 хранит пары: по ключу мгновенно достаётся значение. Это как телефонная книжка: по имени находите номер. Ключи уникальны, значения могут повторяться.
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 — сохраняет порядок добавления ключей.
getOrDefault удобен, чтобы не получить null, когда ключа нет, — про обработку отсутствующих значений и Optional есть отдельные статьи.
Как перебирать коллекции
Самый частый способ — цикл for-each: читается просто, индексы не нужны.
List<String> cities = List.of("Москва", "Казань", "Сочи");
for (String city : cities) { // «для каждого city из cities»
System.out.println(city);
}
Для Map перебирают пары через entrySet():
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. Правильно так:
List<String> cities = new ArrayList<>(List.of("Москва", "Казань", "Сочи"));
Iterator<String> it = cities.iterator();
while (it.hasNext()) {
String city = it.next();
if (city.startsWith("К")) {
it.remove(); // безопасное удаление через итератор
}
}
Почему важны equals и hashCode
HashSet и HashMap определяют «одинаковость» элементов и ключей не по ссылке в памяти, а через два метода: equals (равны ли два объекта по смыслу) и hashCode (число-«отпечаток», по которому коллекция быстро находит нужную ячейку). Для String и чисел они уже реализованы правильно, поэтому примеры выше работают.
А вот свой класс без этих методов в Set/Map поведёт себя неожиданно:
record Point(int x, int y) {} // record сам генерирует equals и hashCode
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.
Неизменяемые коллекции
Иногда коллекцию нужно защитить от изменений — например, вернуть из метода так, чтобы вызывающий код её не испортил. Для этого есть фабричные методы List.of, Set.of, Map.of (с Java 9):
List<String> roles = List.of("admin", "user"); // неизменяемый список
// roles.add("guest"); // выбросит UnsupportedOperationException
Такие коллекции иммутабельны: попытка что-то добавить или удалить бросит исключение. Это безопаснее и нагляднее — сразу видно, что данные менять не предполагается. Если позже нужна изменяемая копия — оберните: new ArrayList<>(List.of(...)).
Коротко
- Три базовых интерфейса:
List(порядок + дубликаты),Set(уникальные значения),Map(пары «ключ → значение»). - По умолчанию берите
ArrayList,HashSet,HashMap— они быстрые и покрывают большинство задач. LinkedList— для частых вставок в середину;TreeSet/TreeMap— когда нужна сортировка.- Перебор — через for-each; удаление во время обхода — только через
Iterator.remove(). - Для элементов
Setи ключейMapнужны согласованныеequalsиhashCode; проще всего —record. List.of,Set.of,Map.ofсоздают неизменяемые коллекции — удобно для защиты данных.
Что почитать дальше
- ООП в Java — классы, объекты, equals/hashCode подробнее.
- Дженерики (generics) — что означают
<String>и зачем они нужны. - Лямбды и Stream API — современная обработка коллекций без ручных циклов.