Как работает Stream API: чем отличаются промежуточные и терминальные операции и что такое ленивость?
Короткий ответ
- Промежуточные операции (map, filter) строят конвейер и ленивы
- Терминальная операция (collect, forEach, count) запускает обработку
- Без терминальной операции конвейер не выполняется вообще
- Элементы проходят конвейер по одному, а не «весь поток через map»
- Stream одноразовый — повторное использование даёт исключение
- short-circuit операции (findFirst, limit, anyMatch) обрабатывают не всё
Stream — ленивый конвейер: промежуточные операции лишь описывают обработку, а запускает её терминальная.
Как сказать вслух
пример ответаПромежуточные операции вроде filter и map ничего не вычисляют — они только описывают конвейер. Реальная работа начинается, когда вызвана терминальная операция, например collect или count. Причём элементы идут через весь конвейер по одному: взяли элемент, профильтровали, преобразовали, отдали дальше. Благодаря этому findFirst может завершиться, обработав всего пару элементов, не трогая остальные.
Подробный ответ
Основной ответ
Stream-конвейер состоит из источника (коллекция, generate, lines), промежуточных операций и одной терминальной. Промежуточные (filter, map, flatMap, sorted, distinct, limit, peek) возвращают новый Stream и ленивы: до терминальной операции ни один элемент не обрабатывается. Терминальные (collect, reduce, forEach, count, findFirst, anyMatch) запускают проход. Обработка поэлементная: каждый элемент проходит все стадии подряд, что позволяет short-circuit операциям (findFirst, anyMatch, limit) останавливаться рано. Исключения — stateful-операции sorted и distinct, которым нужно накопить элементы. Stream нельзя переиспользовать после терминальной операции, и он не меняет источник. parallelStream распараллеливает через общий ForkJoinPool — полезно для CPU-bound задач на больших данных, вредно для блокирующих операций.
Ключевые моменты
- Ленивость. Конвейер — только описание; без терминальной операции даже peek не выполнится.
- Поэлементность. filter→map→collect для каждого элемента по очереди; sorted/distinct накапливают и ломают «поштучность».
- Short-circuit. findFirst/anyMatch/limit прекращают обработку досрочно — важно для бесконечных стримов.
- Одноразовость. Повторный вызов терминальной операции — IllegalStateException; нужен новый стрим из источника.
Практический контекст
Stream — повседневный инструмент трансформации коллекций и DTO. Интервьюер часто просит предсказать вывод цепочки с peek, чтобы проверить понимание ленивости, или спрашивает, почему parallelStream замедлил код с блокирующим IO. Хороший тон — упомянуть Collectors.groupingBy/toMap как типовые терминальные сборки и то, что side-эффекты в map/filter — антипаттерн.
Частые ошибки
- Ожидают, что peek или map сработают без терминальной операции
- Используют parallelStream с блокирующим IO и удивляются деградации
- Мутируют внешнее состояние внутри лямбд конвейера, получая гонки в parallel