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

69

Stream的javadoc说明:

Stream有一个BaseStream.close()方法并实现AutoCloseable接口,但几乎所有的Stream实例在使用之后都不需要关闭。通常情况下,只有那些源是IO通道(比如Files.lines(Path, Charset)返回的那些)的流需要关闭。大多数流都由集合、数组或生成函数支持,不需要特殊的资源管理。(如果某个流确实需要关闭,则可以在try-with-resources语句中将其声明为资源。)

因此,绝大部分时候可以像这样用一行代码来使用Streams:collection.stream().forEach(System.out::println); 但对于Files.lines和其他依赖资源的流,必须使用try-with-resources语句,否则会泄露资源。

这让我感到很容易出错,也很不必要。由于Stream只能迭代一次,我认为不存在Files.lines的输出不应该在迭代完成后立即关闭的情况,因此实现应该在任何终端操作结束时隐式调用close。我理解正确吗?


2
根据我的经验,那些在你不想关闭时自动关闭的流几乎是不可能处理的。你无法重新打开已经为你关闭的内容。标记、重置、查找。根据实现方式,你可以使用相同的流多次读取某些数据。 - ebyrob
4
@ebyrob 不是那个流。 - assylias
4
不比简单的try-with-resource更好,但如果你真的需要用一个表达式来完成,可以参考这个链接:https://dev59.com/SV0Z5IYBdhLWcg3wrSAr#31179709 - Holger
1
我想指出的是,在Java领域中,所有流都不可重复使用,顺便说一下... - rogerdpack
4个回答

90

是的,这是一个有意的决定。我们考虑了两种选择。

操作设计原则是“谁获取资源就应该释放资源”。文件在读取到EOF时不会自动关闭;我们希望打开文件的人明确地关闭文件。由IO资源支持的流也是一样。

幸运的是,语言提供了一个机制来为您自动化此过程:try-with-resources。因为Stream实现了AutoCloseable接口,所以您可以进行以下操作:

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

“希望自动关闭以便我可以将其写成一行代码”的论点很好,但这往往是本末倒置。如果您打开了文件或其他资源,您也应该准备关闭它。有效和一致的资源管理胜过“我想用一行代码来完成”,我们选择不为了保留一行性而扭曲设计。


6
这里的理由是,如果出现未处理的异常,流可能没有被“完全读取”,然后底层句柄将永远不会被关闭。所以这避免了那个问题。但很遗憾它破坏了流链接,并且令人困惑,因为“大多数其他流”不需要这种模式。那么什么时候使用 Try-with-Resources 和 Stream 类型的对象?有时候……但也不总是。在正常管道中似乎从未调用 #close 方法,即使管道已经“完成”… - rogerdpack
18
我认为这很难注意到。在Files.lines()的Javadoc中没有提到,如果您将终止操作放在同一行且没有将Stream作为变量,则Eclipse不会警告资源未关闭的情况。 - aalku
1
嗨,我有一个使用案例,我想将由Files.lines()返回的Stream.map(parseIntoInternalRepresentation)暴露给调用者,因为内部表示对内存非常消耗。我认为最好不要将流材料化为集合,并让调用者决定他们想要链接以减少内存的其他操作。只要在文档中提到API的调用者需要使用try-with-resources,那么公开此流是否可以?想知道这里的最佳实践是什么。 - user2103008

19

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

谢谢。这真的救了我的一天!! - Sivaranjani D

5

如果您想要“一行代码编写”的方式,您可以这样做:

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

如果您确定需要整个文件且该文件较小,则可以使用它。因为它不是一种惰性读取。


6
这里注意到.stream()是不必要的。 - Tagir Valeev
5
请确保文件大小不会太大以致于无法放入内存中。 - Oliv

1
如果你像我一样懒惰,并且不介意“如果出现异常,它将保留文件句柄”的情况,你可以将流包装在自动关闭的流中,类似于这样(可能还有其他方法):
  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);
  }

2
这个不行。没有保证流会消费所有元素。有短路操作,例如 find…()…Match(…),还有 limit(…)takeWhile(…)。如果应用程序使用 iterator()spliterator() 终止流,也不能保证它会迭代到最后。因此,您的解决方案只适用于少数用例,同时显著降低效率。 - Holger
还有一些好的观点,谢谢!(如果您阅读所有行,则有效,但如果不是这种情况,则最好不要使用此功能)。或者也许有些人会认为这是一个特性,例如,您可以将流传递给打开它的方法,并且仍然可以在它被使用完时自动优雅地关闭 :) - rogerdpack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接