Почему Files.lines(и подобные потоки) не закрываются автоматически?

Состояние javadoc для Stream:

Потоки имеют метод BaseStream.close() и реализуют AutoCloseable, но почти все потоковые экземпляры фактически не нужно закрывать после использования. Как правило, только потоки, источник которых является каналом ввода-вывода (например, те, которые возвращаются файлами Files.lines(Path, Charset)), требуют закрытия. Большинство потоков поддерживаются коллекциями, массивами или генерирующими функциями, которые не требуют специального управления ресурсами. (Если поток требует закрытия, он может быть объявлен как ресурс в инструкции try-with-resources.)

Поэтому в большинстве случаев можно использовать потоки в однострочном пространстве, например collection.stream().forEach(System.out::println);, но для Files.lines и других потоков с поддержкой ресурсов, необходимо использовать оператор try-with-resources или утечку ресурсы.

Это поражает меня как подверженного ошибкам и ненужного. Поскольку Streams можно только повторить один раз, мне кажется, что нет ситуации, когда вывод Files.lines не должен быть закрыт, как только он был повторен, и поэтому реализация должна просто вызвать неявно в конце любая операция терминала. Я ошибаюсь?

Ответ 1

Да, это было преднамеренное решение. Мы рассмотрели обе альтернативы.

Принцип операционного проектирования здесь - "объект, который приобретает ресурс, должен освободить ресурс". Файлы не закрываются автоматически при чтении в EOF; мы ожидаем, что файлы будут закрыты явно. Потоки, поддерживаемые ресурсами ввода-вывода, одинаковы.

К счастью, язык предоставляет механизм для автоматизации этого для вас: try-with-resources. Поскольку Stream реализует AutoCloseable, вы можете:

try (Stream<String> s = Files.lines(...)) {
    s.forEach(...);
}

Аргумент, что "было бы очень удобно автоматически закрыть, чтобы я мог написать его как однострочный", приятно, но в основном это будет хвост, виляющий собаку. Если вы открыли файл или другой ресурс, вы также должны быть готовы его закрыть. Эффективные и последовательные козыри управления ресурсами "Я хочу написать это в одной строке", и мы решили не искажать дизайн, чтобы сохранить однострочный характер.

Ответ 2

У меня есть более конкретный пример в дополнение к ответу @BrianGoetz. Не забывайте, что Stream имеет методы escape-hatch, такие как iterator(). Предположим, вы это делаете:

Iterator<String> iterator = Files.lines(path).iterator();

После этого вы можете вызвать hasNext() и next() несколько раз, а затем просто отказаться от этого итератора: интерфейс Iterator отлично поддерживает такое использование. Нет возможности явно закрыть Iterator, единственным объектом, который вы можете закрыть здесь, является Stream. Таким образом, это будет работать отлично:

try(Stream<String> stream = Files.lines(path)) {
    Iterator<String> iterator = stream.iterator();
    // use iterator in any way you want and abandon it at any moment
} // file is correctly closed here.

Ответ 3

Кроме того, если вы хотите "написать одну строку". Вы можете просто сделать это:

Files.readAllLines(source).stream().forEach(...);

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

Ответ 4

Если вы ленитесь, как я, и не возражаете против "если исключение будет поднято, оно оставит дескриптор файла открытым", вы можете обернуть поток в поток автозаполнения, что-то вроде этого (могут быть и другие способы ):

  static Stream<String> allLinesCloseAtEnd(String filename) throws IOException {
    Stream<String> lines = Files.lines(Paths.get(filename));
    Iterator<String> linesIter = lines.iterator();

    Iterator it = new Iterator() {
      @Override
      public boolean hasNext() {
        if (!linesIter.hasNext()) {
          lines.close(); // auto-close when reach end
          return false;
        }
        return true;
      }

      @Override
      public Object next() {
        return linesIter.next();
      }
    };
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.DISTINCT), false);
  }