← Назад к списку
ТехническаяJava и KotlinJunior

Как работает 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

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru