Вопрос 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()]]