为什么 Files.lines(和类似的流)不会自动关闭?

Why is Files.lines (and similar Streams) not automatically closed?

Stream 的 javadoc 状态:

Streams have a BaseStream.close() method and implement AutoCloseable, but nearly all stream instances do not actually need to be closed after use. Generally, only streams whose source is an IO channel (such as those returned by Files.lines(Path, Charset)) will require closing. Most streams are backed by collections, arrays, or generating functions, which require no special resource management. (If a stream does require closing, it can be declared as a resource in a try-with-resources statement.)

因此,绝大多数时候都可以在一行中使用 Streams,例如 collection.stream().forEach(System.out::println); 但对于 Files.lines 和其他资源支持的流,必须使用 try-with -resources声明否则泄漏资源。

我觉得这很容易出错而且没有必要。由于 Streams 只能迭代一次,在我看来,不存在 Files.lines 的输出在迭代后立即关闭的情况,因此实现应该简单地在任何终端操作结束。我错了吗?

是的,这是一个深思熟虑的决定。我们考虑了两种选择。

这里的运行设计原则是"whoever acquires the resource should release the resource"。当您读取到 EOF 时,文件不会 auto-close;我们希望文件被打开它们的人明确关闭。 IO资源支持的流是一样的。

幸运的是,该语言提供了一种为您自动执行此操作的机制:try-with-resources。因为 Stream 实现了 AutoCloseable,你可以这样做:

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

"it would be really convenient to auto-close so I could write it as a one-liner" 的说法很好,但主要是摇尾巴。如果您打开了一个文件或其他资源,您也应该做好关闭它的准备。有效且一致的资源管理胜过 "I want to write this in one line",我们选择不扭曲设计只是为了保留 one-line-ness。

除了@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.

另外如果你想要"one line write"。你可以这样做:

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

如果您确定需要整个文件并且文件很小,您可以使用它。因为它不是懒惰的阅读。

如果你像我一样懒惰并且不介意 "if an exception is raised, it will leave the file handle open" 你可以将流包装在一个自动关闭的流中,就像这样(可能还有其他方法):

  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);
  }