Питання 1 · Розділ 8

Які переваги дає використання Stream API?

Stream не зберігає дані — він описує обчислення над даними. Ключовий компонент — Spliterator (Splitable Iterator):

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Stream API — це спосіб опису послідовності обчислень над даними без їх фактичного зберігання.

На відміну від колекції (яка зберігає елементи в пам’яті), стрім — це ліниво обчислюваний конвеєр операцій. Ви описуєте ЩО зробити (filter, map, collect), а Stream вирішує ЯК це виконати.

Основні переваги:

  • Читабельність: Код стає декларативним — ви описуєте “що” потрібно зробити, а не “як”
  • Менше помилок: Знижується ймовірність помилок циклу (off-by-one, неправильні індекси)
  • Ланцюжки операцій: Можна вибудовувати складні перетворення в один ланцюжок
// Старий підхід
List<String> result = new ArrayList<>();
for (String s : list) {
    if (s.startsWith("A")) {
        result.add(s.toUpperCase());
    }
}

// Stream API
List<String> result = list.stream()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

Коли НЕ використовувати Stream API

  1. Прості операції над маленькими колекціями (< 100 елементів) — звичайний for/for-each простіший і швидший
  2. Критичний за latency код — overhead на пайплайн ~мікросекунди, але в hot path це помітно
  3. Робота з примітивами без boxed — використовуйте IntStream/LongStream, а не Stream

🟡 Middle Level

Як працює Stream API всередині

Stream не зберігає дані — він описує обчислення над даними. Ключовий компонент — Spliterator (Splitable Iterator):

  • Вміє розділяти дані на частини для паралельної обробки
  • Повідомляє характеристики джерела: ORDERED, DISTINCT, SORTED, SIZED

Характеристики Spliterator:

  • ORDERED — елементи мають визначений порядок
  • DISTINCT — всі елементи унікальні
  • SORTED — елементи відсортовані
  • SIZED — відомий точний розмір (ArrayList, масив). Критично для паралелізму!

Типи операцій

Intermediate (проміжні) — ліниві:

  • filter, map, flatMap, peek — stateless (без стану)
  • sorted, distinct, limit, skip — stateful (потребують буфера)

Terminal (термінальні) — запускають виконання:

  • collect, reduce, forEach, count, findFirst, anyMatch

Лінива обробка (Lazy Evaluation)

Проміжні операції не виконуються до виклику термінальної. Це дозволяє оптимізувати обробку і працювати з нескінченними стрімами.

🔴 Senior Level

Internal vs External Iteration

External Iteration (for-each, Iterator):

  • Ви контролюєте “як” робити ітерацію
  • JIT-компілятор обмежений в оптимізаціях

Internal Iteration (Stream API):

  • JIT може застосувати Loop Unrolling і Vectorization (SIMD)
  • Stream API сам вирішує оптимальний порядок виконання

Продуктивність та Highload

Primitive Streams:

// ПОГАНО — створює мільйони об'єктів Integer
list.stream().map(String::length).reduce(0, Integer::sum)

// ДОБРЕ — працює з примітивами
list.stream().mapToInt(String::length).sum()

Fusion (злиття): Сучасні реалізації об’єднують кілька проміжних операцій в один прохід.

Коли НЕ використовувати:

  • Прості цикли на маленьких колекціях (до ~1000 елементів) — for-i швидший, бо немає overhead на створення pipeline і Spliterator. На великих даних різниця згладжується.
  • Складні побічні ефекти (код стане нечитабельним)
  • I/O операції (блокують ForkJoinPool)

Паралелізм — підводні камені

.parallel() використовує спільний ForkJoinPool.commonPool() — безконтрольне використання може сповільнити весь додаток. Емпіричне правило: паралелізм вигідний при N * Q > 10,000.

Діагностика

  • Використовуйте peek() лише для налагодження
  • IntelliJ Stream Debugger візуалізує проходження даних
  • JMH для бенчмарків паралельних стрімів

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Stream API — декларативний спосіб опису обчислень над даними, не зберігає дані в пам’яті
  • Основні переваги: читабельність, менше помилок циклу, зручні ланцюжки операцій
  • Intermediate операції ліниві, terminal — запускають виконання
  • Spliterator — ключовий компонент для розділення даних і паралелізму
  • Primitive Streams (IntStream, LongStream) ефективніші за boxed-версії
  • .parallel() використовує спільний ForkJoinPool.commonPool()
  • Не використовувати на маленьких колекціях та в I/O-операціях

Часті уточнюючі запитання:

  • Коли Stream API гірший за for-each? — На колекціях < 100 елементів, в hot path з критичною latency, при простих операціях
  • Що таке Fusion? — JVM об’єднує кілька intermediate операцій в один прохід
  • Чому parallelStream небезпечний? — Спільний ForkJoinPool, блокування потоків вбиває продуктивність всього додатку
  • Як діагностувати проблеми? — JMH для бенчмарків, IntelliJ Stream Debugger, peek() для налагодження

Червоні прапорці (НЕ говорити):

  • «Stream API завжди швидший за for-each» — ні, оверхед на pipeline помітний на малих даних
  • «ParallelStream прискорить будь-яке завдання» — тільки CPU-bound з N*Q > 10,000
  • «Stream зберігає дані» — ні, це конвеєр обчислень, дані — в джерелі
  • «Можна перевикористати стрім» — ні, IllegalStateException при повторному використанні

Пов’язані теми:

  • [[2. В чому різниця між intermediate та terminal операціями]]
  • [[5. Що робить операція collect()]]
  • [[9. Що таке паралельні стріми]]
  • [[10. Коли використовувати parallel streams]]
  • [[4. Що робить операція map()]]