В чому різниця між 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()]]