Перейти к основному содержимому

Должны ли мы закрыть поток Java?

· 3 мин. чтения

1. Обзор

С введением лямбда-выражений в Java 8 стало возможным писать код более лаконичным и функциональным способом. Потоки и функциональные интерфейсы — сердце этого революционного изменения в платформе Java.

В этом кратком руководстве мы узнаем, должны ли мы явно закрывать потоки Java 8, рассматривая их с точки зрения ресурсов.

2. Закрытие потоков

Потоки Java 8 реализуют интерфейс AutoCloseable :

public interface Stream<T> extends BaseStream<...> {
// omitted
}
public interface BaseStream<...> extends AutoCloseable {
// omitted
}

Проще говоря, мы должны думать о потоках как о ресурсах, которые мы можем одолжить и вернуть, когда закончим с ними. В отличие от большинства ресурсов, нам не нужно всегда закрывать потоки.

Поначалу это может показаться нелогичным, поэтому давайте посмотрим, когда мы должны и когда мы не должны закрывать потоки Java 8.

2.1. Коллекции, массивы и генераторы

В большинстве случаев мы создаем экземпляры Stream из коллекций, массивов или функций-генераторов Java. Например, здесь мы работаем с коллекцией String через Stream API:

List<String> colors = List.of("Red", "Blue", "Green")
.stream()
.filter(c -> c.length() > 4)
.map(String::toUpperCase)
.collect(Collectors.toList());

Иногда мы генерируем конечный или бесконечный последовательный поток:

Random random = new Random();
random.ints().takeWhile(i -> i < 1000).forEach(System.out::println);

Кроме того, мы также можем использовать потоки на основе массива:

String[] colors = {"Red", "Blue", "Green"};
Arrays.stream(colors).map(String::toUpperCase).toArray()

Имея дело с такими потоками, мы не должны закрывать их явно. Единственный ценный ресурс, связанный с этими потоками, — это память, и сборка мусора (GC) позаботится об этом автоматически.

2.2. Ресурсы ввода-вывода

Однако некоторые потоки поддерживаются ресурсами ввода-вывода, такими как файлы или сокеты. Например, метод Files.lines() передает все строки для данного файла:

Files.lines(Paths.get("/path/to/file"))
.flatMap(line -> Arrays.stream(line.split(",")))
// omitted

Под капотом этот метод открывает экземпляр FileChannel , а затем закрывает его при закрытии потока. Поэтому, если мы забудем закрыть поток, базовый канал останется открытым, и тогда мы получим утечку ресурсов .

Чтобы предотвратить такие утечки ресурсов, настоятельно рекомендуется использовать идиому try-with-resources для закрытия потоков на основе ввода-вывода:

try (Stream<String> lines = Files.lines(Paths.get("/path/to/file"))) {
lines.flatMap(line -> Arrays.stream(line.split(","))) // omitted
}

Таким образом, компилятор автоматически закроет канал. Ключевым моментом здесь является закрытие всех потоков на основе ввода-вывода .

Обратите внимание, что закрытие уже закрытого потока вызовет исключение IllegalStateException .

3. Заключение

В этом коротком руководстве мы увидели различия между простыми потоками и потоками с большим объемом операций ввода-вывода. Мы также узнали, как эти различия влияют на наше решение о том, следует ли закрывать потоки Java 8.

Как обычно, пример кода доступен на GitHub .