1. Core Java: коллекции, generics и Stream API
Core Java: коллекции, generics и Stream API
Эта статья — фундамент для всего последующего курса: в Spring и REST вы постоянно работаете со структурами данных, обобщёнными типами (List<User>, ResponseEntity<List<OrderDto>>), а Stream API часто используется в сервисном слое и при маппинге DTO. Здесь мы разберём, как правильно выбирать коллекции, как generics дают типобезопасность, и как строить пайплайны Stream.
Коллекции в Java: зачем и какие бывают
Коллекции — это стандартные структуры данных из Java Collections Framework, предназначенные для хранения, поиска, обхода и преобразования групп объектов.
Ключевые пакеты:
java.util — основные интерфейсы и реализации (например, List, Map, ArrayList, HashMap)java.util.concurrent — потокобезопасные коллекции и примитивы конкурентности!Иерархия основных интерфейсов коллекций и типичных реализаций
Интерфейсы: Collection и Map
В Java есть две крупные «семьи» контейнеров:
Collection<E> — хранит элементы как последовательность/набор (List, Set, Queue)Map<K, V> — хранит пары ключ–значение (ключи уникальны)Важно: Map не наследуется от Collection, потому что его модель данных другая.
List, Set, Queue/Deque, Map: назначение и отличия
List
List<E> — упорядоченная коллекция, допускает дубликаты, доступ по индексу.
Основные реализации:
ArrayList — быстрый доступ по индексу, хороший выбор по умолчаниюLinkedList — удобнее для частых вставок/удалений в середине, но медленнее случайный доступПример:
Set
Set<E> — множество: дубликаты запрещены.
Основные реализации:
HashSet — быстро, порядок не гарантируетсяLinkedHashSet — сохраняет порядок вставкиTreeSet — хранит элементы отсортированными (нужен Comparable или Comparator)Критически важно: уникальность в Set определяется методами equals() и hashCode().
Queue и Deque
Queue<E> — очередь (FIFO), Deque<E> — двусторонняя очередь (можно добавлять/забирать с обоих концов).
Основные реализации:
ArrayDeque — обычно лучший выбор для стека/очереди без конкурентностиPriorityQueue — «очередь с приоритетом» (извлекается минимальный/максимальный по компаратору)Пример Deque как стека:
Map
Map<K, V> — быстрый доступ к значению по ключу.
Основные реализации:
HashMap — быстрый, порядок не гарантируетсяLinkedHashMap — сохраняет порядок вставки (часто полезно для предсказуемого вывода)TreeMap — сортировка по ключуПример:
Как выбрать коллекцию: практические критерии
Выбор коллекции — это баланс требований к:
List допускает, Set нет)get(i) → ArrayList, быстрые проверки «содержит ли» → HashSet)Map)Ниже — упрощённая шпаргалка.
| Требование | Обычно выбирают | Почему |
|---|---|---|
| Нужен индекс и порядок | ArrayList | Быстрый доступ по индексу |
| Нельзя дубликаты, важна скорость | HashSet | Быстрые add/contains |
| Нельзя дубликаты, нужен порядок вставки | LinkedHashSet | Предсказуемый порядок |
| Нужна сортировка элементов | TreeSet | Всегда отсортировано |
| Ключ → значение | HashMap | Быстрый доступ по ключу |
| Нужен порядок ключей | TreeMap | Сортировка по ключу |
| Очередь/стек без потоков | ArrayDeque | Быстро и без лишних накладных |
equals и hashCode: основа корректной работы Set и Map
HashSet и HashMap опираются на контракт:
a.equals(b) истинно, то a.hashCode() == b.hashCode() должен совпадатьequals() должен быть рефлексивным, симметричным, транзитивным и консистентнымЕсли вы используете свои классы как ключи в Map или элементы Set, обязательно реализуйте equals() и hashCode().
Пример (класс-ключ):
Модифицируемые и неизменяемые коллекции
В современном Java-коде часто полезно явно отделять:
Практика:
List.of(...), Set.of(...), Map.of(...) создают неизменяемые коллекции (попытка изменить вызовет исключение)Collections.unmodifiableList(list) создаёт неизменяемый вид на существующий список (если исходный список изменится, это отразится и в «unmodifiable»)Пример:
Итераторы и fail-fast поведение
При обходе коллекций важно понимать разницу:
for-each обычно приводит к ConcurrentModificationExceptionIterator.remove()Пример корректного удаления:
Generics (обобщения): типобезопасность без кастов
Generics позволяют параметризовать типы. Например, List<String> означает «список строк». Компилятор проверяет корректность типов, а вам почти не нужны приведения ((String) ...).
Почему generics важны
ClassCastException)Пример без generics (так писать не надо):
Пример с generics:
Ограничения типов (bounds)
Иногда нужно сказать: «тип должен быть числом» или «должен реализовывать интерфейс».
T extends Number — верхняя граница: T обязан быть Number или его наследникомПример:
Wildcards и принцип PECS
Wildcard — это «неизвестный» параметр типа.
? extends T — коллекция производит значения типа T (можно читать как T, но почти нельзя добавлять)? super T — коллекция потребляет значения типа T (можно добавлять T, но читать придётся как Object или как super-тип)Запоминают это правилом PECS: Producer Extends, Consumer Super.
Пример: копирование из источника в приёмник:
Стирание типов (type erasure)
Generics в Java работают через стирание типов: в байткоде в основном остаются «сырые» типы, а проверки обеспечиваются компилятором и вставленными приведениями.
Практические последствия:
new T() (у типа T нет информации о конструкторе)new List<String>[10] (массивы хранят тип времени выполнения, а generics стираются)Когда особенно важны generics в этом курсе
List<UserDto>, Map<String, Object>ResponseEntity<List<UserDto>>Stream API: декларативная обработка данных
Stream API — это способ выразить обработку коллекций как конвейер операций: что сделать, а не как именно бегать по циклам.
Stream — это не коллекция
Stream<T>:
!Конвейер Stream: источник → промежуточные операции → терминальная операция
Создание стримов
Частые варианты:
list.stream()Stream.of(a, b, c)Arrays.stream(array)IntStream.range(0, n) (примитивные стримы без автобоксинга)Промежуточные и терминальные операции
Промежуточные (возвращают новый Stream):
filter, map, flatMap, distinct, sorted, peek, limit, skipТерминальные (завершают стрим и дают результат):
collect, toList, reduce, forEach, count, min/max, anyMatch/allMatch/noneMatch, findFirst/findAnyПример:
map vs flatMap
map преобразует один элемент в один элементflatMap разворачивает один элемент в поток элементов и «сплющивает» результатПример: получить все теги из списка постов:
Collectors: группировка и агрегации
Collectors позволяют собирать данные в структуры.
Пример: группировка пользователей по роли:
Пример: собрать в Map по id:
Важно: Collectors.toMap выбросит исключение при дубликатах ключей. Если дубликаты возможны, задавайте функцию слияния.
Optional как результат поиска
Optional<T> часто появляется в:
findFirst, findAny, min, maxПример:
Параллельные стримы: осторожно
parallelStream() может ускорить вычисления, но не является «волшебной кнопкой».
Типичные риски:
forEachЕсли вы не уверены — используйте обычный stream() и измеряйте.
Связь с дальнейшими темами курса
После этой статьи будет проще:
Generics)Map/Set/Queue)Stream API)ConcurrentModificationException и почему нужны конкурентные коллекции (к ним мы вернёмся в теме про многопоточность)