← назад к разделу

Массив в 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 — современная обработка коллекций без ручных циклов.