23

当我执行在流管道期间打开大量文件的代码时:

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.count当它完成遍历它时不会关闭它。但我不明白为什么它不应该,因为它是一个终端操作。这同样适用于其他终端操作,例如reduceforEachflatMap另一方面关闭它包含的流。

文档告诉我在必要时使用 try-with-resouces-statement 关闭流。在我的情况下,我可以用这样的count东西替换该行:

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

但这既吵闹又丑陋,在某些大型复杂管道的情况下可能会带来真正的不便。

所以我的问题如下:

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

runtimizeException是一种将检查的异常包装在RuntimeExceptions 中的方法。

4

5 回答 5

22

这里有两个问题:对已检查异常的处理IOException,以及及时关闭资源。

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

在关闭文件之类的资源时,有一些关于在到达流末尾时自动关闭流的调查。这会很方便,但它不处理抛出异常时的关闭。在流中没有为此做正确的事情的神奇机制。

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

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

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

Files.lines调用是打开文件的调用,因此它必须是 TWR 语句中的资源。这个文件的处理是(一些)IOExceptions被抛出的地方,所以我们可以在同一个 TWR 语句中进行异常包装。这建议有一个简单的函数将路径映射到行数,同时处理资源关闭和异常包装:

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);
于 2014-04-08T07:10:20.320 回答
9

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

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

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

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

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

于 2014-04-07T21:25:02.697 回答
4

您将需要调用close()此流操作,这将导致调用所有底层关闭处理程序。

更好的是,将整个语句包装在一个try-with-resources块中,因为它会自动调用关闭处理程序。

在您的情况下这可能是不可能的,这意味着您需要在某些操作中自己处理它。您当前的方法可能根本不适合流。

看来您确实需要在第二次操作中执行此map()操作。

于 2014-04-07T19:49:55.133 回答
3

接口AutoCloseable关闭应该只调用一次。有关更多信息,请参阅AutoCloseable的文档。

如果最终操作会自动关闭流,则可能会调用两次close 。看看下面的例子:

try (Stream<String> lines = Files.lines(path)) {
    lines.count();
}

正如现在定义的那样,行上的close方法将被调用一次。无论最终操作是否正常完成,或者操作是否在IOException中中止。如果流将在最终操作中隐式关闭,则close方法将在发生IOException时调用一次,如果操作成功完成则调用两次

于 2014-04-07T19:56:53.783 回答
0

这是一种替代方法,它使用另一种方法,Files并将避免泄漏文件描述符:

Files.find(Paths.get("JAVA_DOCS_DIR/docs/api/"),
    100, (path, attr) -> path.toString().endsWith(".html"))
    .map(file -> runtimizeException(() -> Files.readAllLines(file, StandardCharsets.ISO_8859_1).size())
    .forEachOrdered(System.out::println);

与您的版本不同,它将返回一个int而不是long行数;但是你没有那么多行的文件,是吗?

于 2014-04-07T20:01:48.090 回答