Что произойдёт при повторном использовании одного объекта Stream после завершения терминальной операции?
Stream рассчитан на одно прохождение: после терминальной операции его нельзя надёжно использовать повторно. При попытке повторной операции реализация обычно обнаруживает повторное использование и выбрасывает IllegalStateException, хотя спецификация формулирует это как недопустимую практику и не требует обнаруживать каждый такой случай.
Терминальная операция не обязательно закрывает поток ресурсов. Она завершает вычисление конвейера, а закрытие внешних ресурсов выполняется отдельно — например, через try-with-resources.
Stream API появился как средство декларативной обработки последовательностей данных вместо ручного управления циклами, индексами и временными коллекциями. Однопроходная модель позволяет строить конвейер обработки, выполнять промежуточные операции лениво и не поддерживать состояние для произвольного возврата к уже обработанным элементам.
Такой подход также совместим с источниками, которые нельзя безопасно или эффективно перематывать: файлами, сетевыми источниками и генераторами значений. Повторная обработка обычно означает создание нового конвейера от исходного источника либо сохранение данных в структуре, допускающей повторный обход.
Разработчик может сохранить Stream в поле или локальной переменной, выполнить над ним терминальную операцию, а затем попытаться использовать тот же объект для другой агрегации. Это приводит к ошибке во время выполнения или к хрупкому коду, зависящему от того, обнаружила ли конкретная реализация повторное использование.
Проблема особенно заметна при добавлении нескольких метрик: например, сначала вычисляется количество элементов, а затем разработчик пытается получить их сумму из того же потока. Если источник одноразовый, автоматическое повторное чтение может быть не только невозможно, но и нежелательно по производительности или семантике.
Объект Stream представляет не коллекцию данных, а одно вычисление над источником. Промежуточные операции формируют новый этап конвейера, а терминальная операция запускает обход источника и переводит исходный конвейер в состояние, после которого повторная обработка не допускается.
Минимальный пример:
После count поток уже использован. Правильная альтернатива — создать новый поток от коллекции:
Для коллекции повторное создание потока обычно безопасно, потому что коллекция является повторно итерируемым источником. Но это не означает, что второй обход всегда бесплатен: он снова читает и обрабатывает элементы.
Если несколько результатов можно получить за один проход, предпочтительнее применить один терминальный сборщик, например Collectors.summarizingInt или собственный корректный Collector. Это уменьшает число обходов, но усложняет агрегацию и может потребовать больше памяти.
Важно отличать завершение потока от закрытия ресурса. Поток, созданный поверх файла или другого ресурса, следует закрывать явно; терминальная операция сама по себе не является универсальным механизмом освобождения ресурса. Для этого применяют try-with-resources.
Параллельность не меняет правило одноразового использования. Параллельный поток также является конкретным вычислительным конвейером; после терминальной операции его нельзя запускать повторно. Если требуется повторная параллельная обработка, создают новый поток и снова явно выбирают параллельный режим.
Сервис получает коллекцию событий и должен построить отчёт с количеством событий и суммой их стоимостей. Один вариант — сохранить поток в переменной и выполнить две терминальные операции. Он выглядит кратко, но некорректен: вторая операция использует уже потреблённый поток.
Второй вариант — дважды получить поток из коллекции. Он прост и прозрачен, но выполняет фильтрацию два раза. Для небольшой коллекции это обычно лучший компромисс благодаря читаемости.
Третий вариант — собрать статистику за один проход с помощью специализированного сборщика. Он снижает число обходов и подходит для больших наборов данных, но требует проверить типы, обработку пустого результата и стоимость промежуточного состояния.
В данном случае выбран один сбор статистики, поскольку отчёт содержит несколько производных показателей, а источник уже представлен коллекцией. Результат — отсутствие ошибки повторного использования и один проход по данным; при этом код остаётся понятным благодаря стандартному сборщику.
Нет. Повторное использование потока запрещено контрактом, но реализация может не обнаружить каждое нарушение. На практике многие повторные терминальные операции приводят к IllegalStateException, однако корректный код не должен рассчитывать именно на это исключение как на обязательный результат.
Зависит от источника. Из коллекции обычно можно многократно вызвать получение нового потока, потому что каждый вызов создаёт отдельный обход. У одноразового источника, например итератора или входного канала, повторное создание потока может быть невозможно, бесполезно или приводить к чтению уже потреблённых данных.
Нет, завершение вычисления и закрытие ресурса — разные действия. Поток может иметь обработчик закрытия, но он срабатывает при явном закрытии, а не просто после терминальной операции. Поэтому поток, связанный с внешним ресурсом, следует использовать в конструкции try-with-resources.