Почему 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, иначе ресурсы будут утечкой.

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


person MikeFHay    schedule 03.12.2015    source источник
comment
По моему опыту, с потоками, которые автоматически закрываются, когда вы этого не хотите, почти невозможно работать. Вы не можете повторно открыть то, что уже было закрыто для вас. Отметить, сбросить, искать. Вы можете читать некоторые данные более одного раза с одним и тем же потоком в зависимости от реализации.   -  person ebyrob    schedule 03.12.2015
comment
@ebyrob не с этим потоком   -  person assylias    schedule 03.12.2015
comment
Не лучше, чем простая попытка с ресурсом, но если вам действительно нужно сделать это с помощью одного выражения: stackoverflow.com/a/ 31179709/2711488   -  person Holger    schedule 03.12.2015
comment
Я хотел бы указать, что все потоки в java land нельзя использовать повторно, FWIW ...   -  person rogerdpack    schedule 02.01.2018


Ответы (4)


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

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

К счастью, язык предоставляет для вас механизм автоматизации: попробуйте с ресурсами. Поскольку Stream реализует AutoCloseable, вы можете:

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

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

person Brian Goetz    schedule 03.12.2015
comment
Я предполагаю, что причина здесь в том, что если есть необработанное исключение, Stream может быть прочитан не полностью, и тогда базовый дескриптор никогда не будет закрыт. Так что это позволяет избежать этой проблемы. Жаль, что это нарушает цепочку потоков и сбивает с толку, потому что большинству других потоков эта парадигма не нужна. Итак, когда вы используете Try-with-Resources с объектами типа Stream? Иногда ... но не в другой раз. Появляется, что метод #close никогда не вызывается в обычном конвейере, даже когда конвейер завершен ... - person rogerdpack; 02.01.2018
comment
На мой взгляд, это трудно заметить. Этого нет в файле javadoc Files.lines (), и Eclipse не предупреждает о том, что ресурс не закрывается, если вы поместите завершающую операцию в ту же строку и у вас нет Stream в качестве переменной. - person aalku; 16.02.2018
comment
Привет, у меня есть случай использования, когда я хочу предоставить вызывающим абонентам поток, возвращаемый Files.lines (). Map (parseIntoInternalRepresentation), потому что внутреннее представление слишком загружено памятью. Я думаю, что лучше не материализовать поток в коллекцию и позволить вызывающим абонентам решать, какие дополнительные операции они хотят связать, чтобы уменьшить память. Можно ли раскрывать этот поток, если я упоминаю в документации, что вызывающие API должны использовать его с помощью try-with-resources? Интересно, какая здесь лучшая практика. - person user2103008; 26.03.2019

У меня есть более конкретный пример в дополнение к ответу @BrianGoetz. Не забывайте, что Stream имеет методы аварийного вывода, такие как 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.
person Tagir Valeev    schedule 04.12.2015
comment
Спасибо. Это действительно спасло мне день !! - person Sivaranjani D; 22.05.2020

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

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

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

person Michael Starodynov    schedule 30.09.2016
comment
Обратите внимание, что .stream() здесь не нужен. - person Tagir Valeev; 30.09.2016
comment
И вы должны быть уверены, что файл не слишком велик, чтобы поместиться в памяти. - person Oliv; 29.09.2017

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

  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);
  }
person rogerdpack    schedule 02.01.2018
comment
Это не работает. Нет гарантии, что поток потребляет все элементы. Есть операции короткого замыкания, такие как find…() или …Match(…), а также limit(…) и takeWhile(…). Если приложение завершает поток с помощью iterator() или spliterator(), также нет гарантии, что оно выполнит итерацию до конца. Таким образом, ваше решение обслуживает лишь несколько вариантов использования, значительно снижая при этом эффективность. - person Holger; 03.01.2018
comment
Также хорошие моменты, спасибо! (работает, если вы прочитали все строки, но если это не так, лучше не использовать это). Или, возможно, некоторые сочтут это функцией, которую вы можете передать поток, например, из метода, который его открывает, и при этом он все равно будет изящно самозакрываться, если / когда он в конечном итоге израсходован :) - person rogerdpack; 03.01.2018