在管道中间关闭流

时间:2014-04-07 19:45:25

标签: java java-8 java-stream

当我执行此代码时,会在流管道中打开大量文件:

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在遍历流时不会关闭流。但鉴于它是终端操作,我不明白为什么不应该这样做。对于reduceforEach等其他终端操作也是如此。另一方面,flatMap关闭它所包含的流。

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

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

但这很嘈杂,并且在某些情况下可能会给大型复杂的管道带来真正的不便。

所以我的问题如下:

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

  3. runtimizeException是一个在RuntimeException s中包装已检查异常的方法。

5 个答案:

答案 0 :(得分:18)

此处有两个问题:处理已检查的例外情况,例如IOException,以及及时关闭资源。

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

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

我们留下了处理资源关闭的标准Java技术,即Java 7中引入的 try-with-resources 结构.TWR真的希望资源被关闭调用堆栈中的相同级别与打开时相同。打开它的人的原则必须关闭它"适用。 TWR还处理异常处理,这通常可以方便地在同一个地方处理异常处理和资源关闭。

在此示例中,流有点不寻常,因为它将Stream<Path>映射到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);

答案 1 :(得分:8)

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

这确保每个资源都使用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);
    }
}

(因为需要关闭的资源在分配时经常会抛出异常,非运行时异常包含在运行时异常中,因此无需单独的方法来执行此操作。)

答案 2 :(得分:4)

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

更好的是,将整个语句包装在 try-with-resources 块中,然后它将自动调用close处理程序。

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

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

答案 3 :(得分:2)

接口 AutoCloseable close 应该只调用一次。有关详细信息,请参阅AutoCloseable的文档。

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

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

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

答案 4 :(得分: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;但你没有那么多行的文件,是吗?