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

Чи можна повторно використовувати Stream?

Отримаєте помилку: java.lang.IllegalStateException: stream has already been operated upon or closed

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

🟢 Junior Level

Ні, не можна. Стрим — це одноразовий об’єкт. Після виклику terminal-операції його не можна використовувати знову.

Stream<String> stream = list.stream();
stream.forEach(System.out::println); // OK
stream.forEach(System.out::println); // IllegalStateException!

Отримаєте помилку: java.lang.IllegalStateException: stream has already been operated upon or closed

Рішення: Створіть новий стрим:

list.stream().forEach(System.out::println);
list.stream().forEach(System.out::println); // OK — кожного разу новий стрим

🟡 Middle Level

Чому так зроблено?

Стрими — це ледачі конвеєри даних, а не колекції:

  • Джерело може не підтримувати повторний обхід (Iterator, Socket)
  • Stream — це ланцюжок із Pipeline-об’єктів. Кожен вузол має прапорець «використано». Після terminal-операції всі вузли позначаються consumed. Це захист від race condition у parallelStream.
  • Оптимізації JIT та Pipeline-fusion працюють тільки для одного проходу

Як обходити обмеження

**1. Патерн Supplier:**

Supplier<Stream<String>> supplier = () -> list.stream()
    .filter(s -> s.length() > 5);

supplier.get().forEach(System.out::println);
long count = supplier.get().count(); // Працює — кожного разу новий стрим

НЕ використовуйте Supplier якщо джерело дороге (DB query, HTTP). У цьому випадку краще один раз зібрати в колекцію. Supplier підходить для дешевих джерел (колекції в пам’яті).

2. Збірка в колекцію:

List<String> cached = stream.collect(Collectors.toList());
cached.stream().forEach(...);
cached.stream().count();

🔴 Senior Level

Архітектурні наслідки

Stream як параметр методу: Ніколи не приймайте Stream у публічному API, якщо плануєте кілька проходів. Приймайте Iterable, Collection або Supplier<Stream>.

Resource Leaks: Стрим на зовнішньому ресурсі (Files.lines()) реалізує AutoCloseable. Повторний виклик не просто кине помилку — може завадити звільненню дескрипторів.

Object Allocation

Один Stream-пайплайн — ~5-10 об’єктів. Мільйон стримів = 5-10M алокацій. Для Young Gen це відчутно. Якщо створюєте > 100K стримів/сек — розгляньте звичайний цикл.

Edge Cases

  • parallelStream() використовує спільний пул. Багато стримів-повторів → голодування потоків у ForkJoinPool.commonPool()
  • Exhaustion: Стрим може бути вичерпаний не тільки terminal-операцією, але й при явному виклику close()

Діагностика

При помилці stream has already been operated upon or closed шукайте місце, де посилання на стрим збережено у змінну і використовується двічі. В IntelliJ IDEA в режимі налагодження стрим позначається як “consumed” одразу після terminal-операції.


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

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

  • Стрим — одноразовий об’єкт; після terminal-операції повторне використання кидає IllegalStateException
  • Stream — це ледачий конвеєр даних, а не колекція; джерело може не підтримувати повторний обхід (Iterator, Socket)
  • Кожен вузол Pipeline має прапорець «consumed»; після terminal-операції всі вузли позначаються використаними — захист від race condition
  • Патерн Supplier<Stream> — кожен виклик get() створює новий стрим; підходить для дешевих джерел (колекції в пам’яті)
  • Для дорогих джерел (DB query, HTTP) краще один раз зібрати в колекцію, а не створювати стрим кожного разу
  • Ніколи не приймайте Stream у публічному API, якщо плануєте кілька проходів — приймайте Iterable, Collection або Supplier<Stream>
  • Стрим на зовнішньому ресурсі (Files.lines()) реалізує AutoCloseable; повторний виклик може завадити звільненню дескрипторів

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

  • Чому стрим не можна перевикористати? — Stream — ланцюжок Pipeline-об’єктів з прапорцем consumed; це захист від race condition у parallelStream і дозволяє JIT-оптимізаціям
  • Як обійти обмеження одноразовості? — Патерн Supplier<Stream> (кожен get() — новий стрим) або збірка в колекцію один раз
  • **Коли Supplier — погана ідея?** — Якщо джерело дороге (DB query, HTTP call); тоді зберіть у колекцію один раз
  • Скільки об’єктів створює один Stream-пайплайн? — ~5-10 об’єктів; мільйон стримів = 5-10M алокацій, відчутно для Young Gen

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

  • «Можна викликати reset() на стримі і використовувати знову» — такого методу не існує
  • «Stream — це те саме, що колекція» — Stream ледачий, одноразовий, може не підтримувати повторний обхід
  • «Можна безпечно передавати Stream у публічний API» — якщо метод планує кілька проходів, приймайте Collection або Supplier
  • «parallelStream() вирішує проблему повторного використання» — навпаки, поглиблює: голодування потоків у commonPool

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

  • [[Що таке lazy evaluation в Stream]]
  • [[Коли починається виконання операцій в Stream]]
  • [[Які потенційні проблеми можуть бути з паралельними стримами]]
  • [[Що робить операція collect()]]