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

Posted

技术标签:

【中文标题】为啥 Files.lines(和类似的 Streams)不会自动关闭?【英文标题】:Why is Files.lines (and similar Streams) not automatically closed?为什么 Files.lines(和类似的 Streams)不会自动关闭? 【发布时间】:2016-03-08 10:09:46 【问题描述】:

Stream 状态的 javadoc:

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

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

这让我觉得容易出错且不必要。由于 Streams 只能迭代一次,在我看来,不存在 Files.lines 的输出在迭代后不应该立即关闭的情况,因此实现应该简单地在末尾隐式调用 close任何终端操作。我弄错了吗?

【问题讨论】:

根据我的经验,当您不希望它们自动关闭时,几乎不可能使用它们。您无法重新打开已经为您关闭的内容。标记,重置,寻找。根据实现,您可以使用同一流多次读取某些数据。 @ebyrob 不在该流中 不比一个简单的 try-with-resource 更好,但如果你真的需要用一个表达式来做:***.com/a/31179709/2711488 我要指出的是 all 在 java 领域的流是不可重用的,FWIW... 【参考方案1】:

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

这里的运营设计原则是“谁获得资源,谁就释放资源”。读取 EOF 时文件不会自动关闭;我们希望文件被打开文件的人明确关闭。由 IO 资源支持的流是相同的。

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

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

“自动关闭真的很方便,所以我可以把它写成单行”的论点很好,但主要是摇尾巴。如果您打开了文件或其他资源,您还应该准备好关闭它。有效和一致的资源管理胜过“我想用一行写这个”,我们选择不扭曲设计只是为了保持单行性。

【讨论】:

我猜这里的基本原理是,如果有一个未处理的异常,Stream 可能不会“一直读取”,然后底层句柄将“永远不会关闭”。所以这避免了这个问题。太糟糕了,它破坏了流链接,并且令人困惑,因为“大多数其他流”不需要这种范式。那么,什么时候对 Stream 类型的对象使用 Try-with-Resources 呢?有时……但又不是其他时候。似乎在正常管道中永远不会调用 #close 方法,即使管道“完成”... 在我看来这很难注意到。它不在 Files.lines() javadoc 中,如果您在同一行中放置终止操作并且没有将 Stream 作为变量,Eclipse 不会警告资源未关闭。 嗨,我有一个用例,我想将 Files.lines().map(parseIntoInternalRepresentation) 返回的 Stream 公开给调用者,因为内部表示在内存上非常重。我认为最好不要将流具体化为集合,让调用者决定他们想要链接哪些额外的操作以减少内存。只要我在文档中提到 API 的调用者需要将它与 try-with-resources 一起使用,就可以公开这个流吗?想知道这里的最佳做法是什么。【参考方案2】:

除了@BrianGoetz 的回答,我还有更具体的例子。不要忘记Stream 有像iterator() 这样的escape-hatch 方法。假设你正在这样做:

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(...);

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

【讨论】:

请注意这里不需要.stream() 并且您必须确保文件不会太大而无法放入内存。【参考方案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);
  

【讨论】:

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

以上是关于为啥 Files.lines(和类似的 Streams)不会自动关闭?的主要内容,如果未能解决你的问题,请参考以下文章

这是 Files.lines() 中的错误,还是我对并行流有误解?

为啥在java 8中转换类型的reduce方法需要一个组合器

Java 8流到文件[重复]

为啥 std::transform 和类似的东西将“for”循环增量转换为(void)?

Py4JJavaError:调用 o45.load 时出错。 :java.lang.NoClassDefFoundError:org/apache/spark/sql/sources/v2/Strea

为啥 CSS3 PIE 和其他类似的脚本不是到处都在使用?