当我执行此代码时,会在流管道中打开大量文件:
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
在遍历流时不会关闭流。但鉴于它是终端操作,我不明白为什么不应该这样做。对于reduce
和forEach
等其他终端操作也是如此。另一方面,flatMap
关闭它所包含的流。
文档告诉我如有必要,可以使用try-with-resouces-statement来关闭流。在我的情况下,我可以用这样的东西替换count
行:
.map(s -> { long c = s.count(); s.close(); return c; } )
但这很嘈杂,并且在某些情况下可能会给大型复杂的管道带来真正的不便。
所以我的问题如下:
runtimizeException
是一个在RuntimeException
s中包装已检查异常的方法。
答案 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
;但你没有那么多行的文件,是吗?