Питання 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()]]