Вопрос 2 · Раздел 8

В чём разница между intermediate и terminal операциями?

В Stream API все операции делятся на два типа:

Версии по языкам: English Russian Ukrainian

🟢 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()]]