在管道中间关闭流

2022-09-01 22:58:40

当我执行此代码时,它在流管道期间打开了很多文件:

public static void main(String[] args) throws IOException {
    Files.find(Paths.get("JAVA_DOCS_DIR/docs/api/"),
            100, (path, attr) -> path.toString().endsWith(".html"))
        .map(file -> runtimizeException(() -> Files.lines(file, StandardCharsets.ISO_8859_1)))
        .map(Stream::count)
        .forEachOrdered(System.out::println);
}

我得到一个例外:

java.nio.file.FileSystemException: /long/file/name: Too many open files

问题是,在完成遍历流时,它不会关闭流。但我不明白为什么它不应该,因为它是一个终端操作。这同样适用于其他终端操作,例如 和 。 另一方面关闭它所包含的流。Stream.countreduceforEachflatMap

文档告诉我,如有必要,请使用 try-with-resouces-语句来关闭流。在我的情况下,我可以用这样的东西替换这行:count

.map(s -> { long c = s.count(); s.close(); return c; } )

但这是嘈杂和丑陋的,在某些情况下,对于大型,复杂的管道,这可能是一个真正的不便。

所以我的问题如下:

  1. 为什么没有将流设计为终端操作关闭它们正在处理的流?这将使它们更好地与IO流一起工作。
  2. 在管道中关闭 IO 流的最佳解决方案是什么?

runtimizeException是将已检查的异常包装在 s 中的方法。RuntimeException


答案 1

这里存在两个问题:处理已检查的异常(如 )和及时关闭资源。IOException

没有预定义的函数接口声明任何已检查的异常,这意味着必须在 lambda 中处理它们,或者将它们包装在未经检查的异常中并重新引发。看起来您的函数可以做到这一点。您可能还必须为其声明自己的功能接口。正如你可能已经发现的那样,这是一种痛苦。runtimizeException

在关闭文件等资源时,有一些调查是在到达流的末尾时自动关闭流。这很方便,但它不处理在引发异常时关闭。在流中没有神奇的做正确的事情机制。

我们只剩下处理资源关闭的标准Java技术,即Java 7中引入的try-with-resources构造。TWR 确实希望在调用堆栈中与打开资源时相同的级别关闭资源。“谁打开它,谁就要关闭它”的原则适用。TWR 还处理异常处理,这通常便于在同一位置处理异常处理和资源关闭。

在此示例中,流有些不寻常,因为它将 a 映射到 .这些嵌套流是未关闭的流,当系统用完打开的文件描述符时,最终会导致异常。使这变得困难的是,文件由一个流操作打开,然后传递到下游;这使得无法使用TWR。Stream<Path>Stream<Stream<String>>

构建此管道的另一种方法如下。

调用是打开文件的对象,因此这必须是 TWR 语句中的资源。此文件的处理是(某些)被抛出的位置,因此我们可以在同一 TWR 语句中执行异常包装。这建议使用一个简单的函数,将路径映射到行计数,同时处理资源关闭和异常换行:Files.linesIOExceptions

long lineCount(Path path) {
    try (Stream<String> s = Files.lines(path, StandardCharsets.ISO_8859_1)) {
        return s.count();
    } catch (IOException ioe) {
        throw new UncheckedIOException(ioe);
    }
}

拥有此帮助程序函数后,主管道如下所示:

Files.find(Paths.get("JAVA_DOCS_DIR/docs/api/"),
           100, (path, attr) -> path.toString().endsWith(".html"))
     .mapToLong(this::lineCount)
     .forEachOrdered(System.out::println);

答案 2

可以创建一个实用程序方法,该方法可靠地关闭管道中间的流。

这可确保使用 try-with-resource 语句关闭每个资源,但避免了对自定义实用程序方法的需求,并且比直接在 lambda 中编写 try 语句要简单得多。

使用此方法时,问题中的管道如下所示:

Files.find(Paths.get("Java_8_API_docs/docs/api"), 100,
        (path, attr) -> path.toString().endsWith(".html"))
    .map(file -> applyAndClose(
        () -> Files.lines(file, StandardCharsets.ISO_8859_1),
        Stream::count))
    .forEachOrdered(System.out::println);

实现如下所示:

/**
 * Applies a function to a resource and closes it afterwards.
 * @param sup Supplier of the resource that should be closed
 * @param op operation that should be performed on the resource before it is closed
 * @return The result of calling op.apply on the resource 
 */
private static <A extends AutoCloseable, B> B applyAndClose(Callable<A> sup, Function<A, B> op) {
    try (A res = sup.call()) {
        return op.apply(res);
    } catch (RuntimeException exc) {
        throw exc;
    } catch (Exception exc) {
        throw new RuntimeException("Wrapped in applyAndClose", exc);
    }
}

(由于需要关闭的资源在分配时通常也会引发异常,因此非运行时异常被包装在运行时异常中,从而避免了使用单独的方法来执行此操作。


推荐