В чём разница между intermediate и terminal операциями?
В Stream API все операции делятся на два типа:
🟢 Junior Level
В Stream API все операции делятся на два типа:
Intermediate (промежуточные) операции:
- Возвращают новый Stream
- Можно вызывать цепочкой
- Не выполняются сразу — они ленивые.
Аналогия: ленивость — как рецепт в кулинарной книге. Вы записали шаги (filter, map), но готовить начнёте только когда кто-то попросит результат (terminal операция). Без terminal операции стрим — просто описание плана, ничего не вычисляется.
- Примеры:
filter(),map(),sorted(),limit()
Terminal (терминальные) операции:
- Запускают выполнение всей цепочки
- Возвращают результат (не Stream)
- После них стрим нельзя использовать повторно
- Примеры:
collect(),forEach(),count(),findFirst()
list.stream() // создание стрима
.filter(s -> s.length() > 3) // intermediate
.map(String::toUpperCase) // intermediate
.collect(toList()); // terminal — запускает всё
🟡 Middle Level
Как это работает внутри
Stream API — это машина состояний. Каждая intermediate операция регистрирует новое звено в конвейере (Pipeline), используя структуру Sink:
- Каждая операция создает свой
Sink, который оборачивает следующий - Данные “проталкиваются” через цепочку Sink-ов при вызове terminal операции
Sink — внутренний интерфейс Stream API, который передаёт элементы от одной операции к следующей по цепочке. Каждый intermediate-операция оборачивает предыдущий Sink в свой.
Типы intermediate операций
Stateless (без состояния): filter, map, flatMap, peek
- Каждый элемент обрабатывается независимо
- Идеально для параллелизма
Stateful (с состоянием): distinct, sorted, limit, skip
- Требуют знания о других элементах
- Создают “барьеры” — снижают производительность
Порядок имеет значение
// ПЛОХО: сортируем 1 млн, потом берем 5
list.stream().sorted().limit(5).collect(toList());
// ХОРОШО: фильтруем в начале, уменьшая объем
list.stream().filter(relevantOnly).sorted().limit(5).collect(toList());
Правило: Ставьте stateless операции (filter, map) перед stateful (sorted, distinct).
🔴 Senior Level
Pipeline Overhead
Создание объектов Stream, AbstractPipeline и цепочки Sink имеет цену. Для маленьких коллекций (до ~100 элементов) обычный for быстрее, т.к. overhead на создание pipeline и Sink-цепочки превышает выгоду. Стримы выигрывают на больших объемах или сложной логике.
Fusion (Слияние операций)
Stream API может объединять операции: .map(f1).map(f2) выполняется как один проход с f2(f1(x)). Это снижает количество итераций.
Lazy Evaluation Catch
Тяжелые операции в intermediate лямбдах не выполняются до terminal вызова:
Stream<Integer> stream = list.stream().map(this::expensiveDbCall);
// Ничего не произошло! База не вызывалась.
var result = stream.collect(toList());
// Только теперь выполнился map()
Это может привести к неожиданным задержкам в конце метода.
Edge Cases
peek()без terminal операции: Код внутриpeekникогда не выполнится- Повторное использование стрима: Вызовет
IllegalStateException - Breakpoint внутри лямбды: Сработает только когда terminal операция начнет тянуть данные
Диагностика
Используйте Java Flight Recorder (JFR) или профилировщики для анализа времени “прокачки” данных через Pipeline.
🎯 Шпаргалка для интервью
Обязательно знать:
- Intermediate операции возвращают Stream, ленивые, можно выстраивать цепочкой
- Terminal операции запускают выполнение, возвращают результат (не Stream)
- После terminal операции стрим исчерпан — повторное использование вызовет IllegalStateException
- Intermediate бывают stateless (filter, map) и stateful (sorted, distinct, limit)
- Stateless операции идеальны для параллелизма, stateful создают барьеры
- Sink — внутренний механизм, передающий элементы по цепочке
Частые уточняющие вопросы:
- Почему filter лучше ставить перед sorted? — Чтобы уменьшить объём данных до дорогой stateful операции
- Что такое Fusion? — JVM объединяет
.map(f1).map(f2)в один проход сf2(f1(x)) - Когда peek() не выполнится? — Без terminal операции — ничего не вычисляется из-за ленивости
- Почему stateful операции медленнее? — Требуют буферизации и знания обо всех элементах
Красные флаги (НЕ говорить):
- «Intermediate операции сразу обрабатывают данные» — нет, они ленивые
- «Можно вызвать terminal операцию дважды на одном стриме» — нет, IllegalStateException
- «sorted и filter одинаковы по производительности» — sorted stateful и требует буфера
- «Порядок операций не важен» — filter до map/sorted критично для производительности
Связанные темы:
- [[1. Какие преимущества даёт использование Stream API]]
- [[3. Что делает операция filter()]]
- [[5. Что делает операция collect()]]
- [[7. Что делает операция flatMap()]]