Чтение файла через Stream<String> завершается терминальной операцией: почему стрим всё равно нужно явно закрыть?
Стрим, созданный для чтения файла, владеет открытым системным ресурсом, поэтому завершение терминальной операции само по себе не гарантирует закрытие файла. Такой стрим нужно использовать в try-with-resources, который вызовет close() даже при исключении.
Stream API появился в Java 8 для ленивой обработки последовательностей данных без обязательного промежуточного хранения всей коллекции. Такой подход применим не только к коллекциям, но и к источникам, связанным с ресурсами: файлам, сетевым соединениям и другим каналам ввода.
Для поддержки таких источников интерфейс BaseStream предусматривает AutoCloseable. Это позволяет управлять временем жизни ресурса отдельно от момента выполнения терминальной операции.
Files.lines читает файл лениво: строки извлекаются по мере прохождения элементов стримом, а сам файл остаётся открытым во время обработки. Если не закрыть стрим, файловый дескриптор может сохраняться дольше необходимого.
При большом числе таких операций это приводит к исчерпанию лимита открытых файлов. Дополнительный риск возникает при исключении во время чтения: ручной вызов close() может не выполниться, если код не использует гарантированное управление ресурсами.
Терминальная операция, например подсчёт элементов или поиск строки, завершает вычисление конвейера, но не обязана закрывать источник. Закрытие стрима — отдельная операция жизненного цикла, которая освобождает ресурс и запускает зарегистрированные обработчики закрытия.
Блок try-with-resources вызывает close() после завершения обработки как при обычном выходе, так и при исключении. Закрытие освобождает ресурс, связанный с чтением файла; уже полученные элементы при этом не теряются.
Это правило относится прежде всего к стримам, связанным с внешними ресурсами. Стрим обычной коллекции обычно не владеет ресурсом, который нужно освобождать, поэтому его закрытие практически ничего не меняет.
Если требуется полностью загрузить файл в память, можно использовать операцию, возвращающую список строк: ресурс при этом закрывается внутри метода, но расход памяти становится пропорционален размеру файла. Ленивый стрим экономит память, однако требует явного управления временем жизни ресурса.
Сервис анализирует многогигабайтные журналы. Вариант с полной загрузкой файла прост, но требует большого объёма памяти и создаёт задержку до начала обработки. Вариант с Files.lines позволяет обрабатывать строки постепенно, однако без try-with-resources оставляет открытые файловые дескрипторы.
Ручной вызов close() после терминальной операции хуже: при исключении он может быть пропущен. Выбранный вариант — ленивое чтение внутри try-with-resources; он ограничивает потребление памяти и гарантирует освобождение файла после обработки.
1. Закрывает ли close() стрима коллекции саму коллекцию?
Нет. Стрим коллекции не является владельцем коллекции и не закрывает, не очищает и не изменяет её. close() закрывает сам стрим и запускает его close-обработчики, если они были зарегистрированы; для обычного стрима коллекции внешний ресурс обычно отсутствует.
2. Почему Files.lines нельзя рассматривать как обычный список строк?
Потому что он работает лениво и поддерживает открытый ресурс во время обхода. Часть файла может вообще не быть прочитана, если короткозамыкающая терминальная операция завершит обработку раньше, но стрим всё равно должен быть закрыт.
3. Что произойдёт, если во время обработки возникнет исключение?
При использовании try-with-resources Java сначала выполнит закрытие стрима, а затем передаст исключение вызывающему коду. Если закрытие тоже завершится исключением, оно будет добавлено как подавленное к основному исключению, поэтому исходная причина ошибки не теряется.