Чи можна повторно використовувати Stream?
Отримаєте помилку: java.lang.IllegalStateException: stream has already been operated upon or closed
🟢 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()]]