在try-with-resources块中pipe理多个链接资源的正确习惯用法?

Java 7 try-with-resources语法(也称为ARM块( 自动资源pipe理 ))在使用一个AutoCloseable资源时很好,简单明了。 然而,我不确定什么是正确的习惯用法,当我需要声明多个相互依赖的资源,例如一个FileWriter和一个BufferedWriter包装它。 当然,这个问题涉及到一些AutoCloseable资源被封装的情况,不仅仅是这两个特定的类。

我提出了以下三个备选scheme:

1)

我见过的天真的习惯用法是只声明ARMpipe理的variables中的顶层包装:

 static void printToFile1(String text, File file) { try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { bw.write(text); } catch (IOException ex) { // handle ex } } 

这很好,很短,但是坏了。 因为底层的FileWriter没有在variables中声明,它永远不会直接在生成的finally块中closures。 它只会通过包装BufferedWriterclose方法来closures。 问题是,如果从bw的构造函数抛出exception,它将不会被调用,因此底层的FileWriter 将不会被closures

2)

 static void printToFile2(String text, File file) { try (FileWriter fw = new FileWriter(file); BufferedWriter bw = new BufferedWriter(fw)) { bw.write(text); } catch (IOException ex) { // handle ex } } 

在这里,底层资源和包装资源都是在ARMpipe理的variables中声明的,所以它们都是肯定closures的,所以潜在的fw.close() 将被调用两次 ,第一次直接,第二次包装bw.close()

对于这两个实现Closeable (这是AutoCloseable的子types)的特定类,这不应该是一个问题,它的合同规定允许多个要close调用:

closures此stream并释放与其关联的所有系统资源。 如果stream已经closures,则调用此方法不起作用。

但是,在一般情况下,我可以拥有仅实现AutoCloseable (而不是Closeable )的资源,但不能保证close可以被多次调用:

请注意,与java.io.Closeable的close方法不同,此close方法不要求是幂等的。 换句话说,不止一次地调用这个close方法可能会有一些明显的副作用,而不像Closeable.close那样,如果多次调用这个方法,这个方法将不起作用。 然而,强烈鼓励这个接口的实现者使他们的密切方法是幂等的。

3)

 static void printToFile3(String text, File file) { try (FileWriter fw = new FileWriter(file)) { BufferedWriter bw = new BufferedWriter(fw); bw.write(text); } catch (IOException ex) { // handle ex } } 

这个版本在理论上应该是正确的,因为只有fw代表了一个需要清理的实际资源。 bw本身并不拥有任何资源,它只是代表fw ,所以只需要closures潜在的fw

另一方面,语法有点不规范,Eclipse也发出了一个警告,我相信这是一个虚惊,但它仍然是一个警告,必须处理:

资源泄漏:'bw'永远不会closures


那么,要采取哪种方法? 还是我错过了其他一些正确的成语?

这是我的select:

1)

 try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { bw.write(text); } 

对我来说,15年前从传统的C ++到Java的最好的事情是,你可以相信你的程序。 即使事情经常发生,我们也希望其余的代码能够做到最好的行为和玫瑰味。 的确, BufferedWriter可能会在这里抛出exception。 例如,内存不足并不是不常见的。 对于其他的装饰器,你知道哪个java.io包装器类从构造器中抛出一个检查的exception吗? 我不。 如果你依赖这种模糊的知识,就不会对代码的可理解性做出太多的贡献。

还有“破坏”。 如果出现错误情况,那么您可能不希望将垃圾清理到需要删除的文件(代码未显示)。 虽然,删除文件当然也是一个有趣的操作,可以作为error handling。

一般来说,你finally希望块尽可能短而可靠。 添加冲洗不能帮助这个目标。 对于许多版本来说,JDK中的一些缓冲类有一个错误,那就是不能调用装饰对象的close内的exception。 虽然这已经被固定了一段时间,但从其他实现中期待。

2)

 try ( FileWriter fw = new FileWriter(file); BufferedWriter bw = new BufferedWriter(fw) ) { bw.write(text); } 

我们仍然在隐式finally块中刷新(现在反复close – 这会随着添加更多的装饰器而变得更糟),但是构造是安全的,我们必须隐式地终止块,所以即使失败的flush也不会阻止资源释放。

3)

 try (FileWriter fw = new FileWriter(file)) { BufferedWriter bw = new BufferedWriter(fw); bw.write(text); } 

这里有一个错误。 应该:

 try (FileWriter fw = new FileWriter(file)) { BufferedWriter bw = new BufferedWriter(fw); bw.write(text); bw.flush(); } 

一些实施不好的装饰器实际上是资源,需要可靠地closures。 还有一些stream可能需要以特定的方式closures(也许他们正在做压缩,需要写点完成,并不能冲刷一切。

判决书

尽pipe3是一个技术上优越的解决scheme,软件开发的原因使得2更好的select。 但是,尝试使用资源仍然是一个不足的解决办法,您应该坚持使用“执行周围”语言,在Java SE 8中应该有更清晰的语法封闭。

第一种方式是Oraclebuild议的方式 。 BufferedWriter不会抛出检查exception,所以如果抛出任何exception,程序不会从中恢复,使得资源恢复大多是没有意义的。

主要是因为它可能发生在一个线程中,随着线程的死亡,程序仍在继续 – 比如暂时的内存中断时间不足以严重影响程序的其余部分。 但是,这是一个相当严重的情况,如果这种情况经常发生,足以使资源泄漏成为问题,那么尝试资源是你的问题中最less的。

选项4

如果可以,请将资源更改为“可closures”,而不是“自动closures”。 构造函数可以链接的事实意味着两次closures资源并不是闻所未闻的。 (在ARM之前,情况也是如此。)下面的更多内容。

选项5

不要使用ARM和代码非常仔细,以确保close()不会被调用两次!

选项6

不要使用ARM,并在try / catch中最后closures()调用。

为什么我不认为这个问题是ARM独有的

在所有这些例子中,finally()调用都应该在catch块中。 为了便于阅读,丢掉了。

不好,因为FW可以closures两次。 (这对FileWriter是好的,但不是在你的假设的例子):

 FileWriter fw = null; BufferedWriter bw = null; try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write(text); } finally { if ( fw != null ) fw.close(); if ( bw != null ) bw.close(); } 

没有好处,因为如果构buildBufferedWriter的例外,fw不会closures。 (再次,不可能发生,但在你的假设的例子):

 FileWriter fw = null; BufferedWriter bw = null; try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write(text); } finally { if ( bw != null ) bw.close(); } 

我只是想让Jeanne Boyarskybuild议不要使用ARM,但要确保FileWriter总是只closures一次。 不要以为这里有什么问题

 FileWriter fw = null; BufferedWriter bw = null; try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write(text); } finally { if (bw != null) bw.close(); else if (fw != null) fw.close(); } 

我想因为ARM只是语法糖,我们不能总是用它来代替最后的块。 就像我们不能总是使用for-each循环来做一些迭代器可能的事情一样。

为了与先前的评论一致:最简单的是(2)使用可Closeable资源并在try-with-resources子句中按顺序声明它们。 如果你只有AutoCloseable,你可以把它们封装在另一个(嵌套的)类中,只检查close只调用一次(Facade Pattern),比如通过private bool isClosed; 。 实际上,即使是Oracle (1)链接构造函数,也不能正确处理链中的exception。

或者,您可以使用静态工厂方法手动创build链式资源; 这封装链,并处理清理,如果它部分失败:

 static BufferedWriter createBufferedWriterFromFile(File file) throws IOException { // If constructor throws an exception, no resource acquired, so no release required. FileWriter fileWriter = new FileWriter(file); try { return new BufferedWriter(fileWriter); } catch (IOException newBufferedWriterException) { try { fileWriter.close(); } catch (IOException closeException) { // Exceptions in cleanup code are secondary to exceptions in primary code (body of try), // as in try-with-resources. newBufferedWriterException.addSuppressed(closeException); } throw newBufferedWriterException; } } 

复杂性来自处理多个例外; 否则这只是“你已经获得的资源,迄今为止”。 通常的做法似乎是首先初始化variables,该variables将持有资源的对象保留为null (这里是fileWriter ),然后在清理中包含一个空检查,但这似乎是不必要的:如果构造函数失败,则没有任何清理所以我们可以让这个exception传播,这简化了一些代码。

你大概可以这样做:

 static <T extends AutoCloseable, U extends AutoCloseable, V> T createChainedResource(V v) throws Exception { // If constructor throws an exception, no resource acquired, so no release required. U u = new U(v); try { return new T(u); } catch (Exception newTException) { try { u.close(); } catch (Exception closeException) { // Exceptions in cleanup code are secondary to exceptions in primary code (body of try), // as in try-with-resources. newTException.addSuppressed(closeException); } throw newTException; } } 

由于你的资源是嵌套的,你的try-with子句也应该是:

 try (FileWriter fw=new FileWriter(file)) { try (BufferedWriter bw=new BufferedWriter(fw)) { bw.write(text); } catch (IOException ex) { // handle ex } } catch (IOException ex) { // handle ex } 

我会说不要使用ARM和继续与Closeable。 使用方法,

 public void close(Closeable... closeables) { for (Closeable closeable: closeables) { try { closeable.close(); } catch (IOException e) { // you can't much for this } } } 

你也应该考虑调用BufferedWriter close,因为它不仅仅是把close放到FileWriter ,而且还做了一些像flushBuffer一样的flushBuffer

我的解决scheme是做一个“提取方法”重构,如下所示:

 static AutoCloseable writeFileWriter(FileWriter fw, String txt) throws IOException{ final BufferedWriter bw = new BufferedWriter(fw); bw.write(txt); return new AutoCloseable(){ @Override public void close() throws IOException { bw.flush(); } }; } 

printToFile也可以被写入

 static void printToFile(String text, File file) { try (FileWriter fw = new FileWriter(file)) { AutoCloseable w = writeFileWriter(fw, text); w.close(); } catch (Exception ex) { // handle ex } } 

要么

 static void printToFile(String text, File file) { try (FileWriter fw = new FileWriter(file); AutoCloseable w = writeFileWriter(fw, text)){ } catch (Exception ex) { // handle ex } } 

对于class libdevise者,我会build议他们用一个额外的方法扩展AutoClosable接口来抑制closures。 在这种情况下,我们可以手动控制closures行为。

对于语言devise师来说,增加新function可能意味着要增加其他function。 在这个Java的情况下,显然ARMfunction将更好地与资源所有权转移机制。

UPDATE

原来上面的代码需要@SuppressWarning因为函数内部的BufferedWriter需要close()

如注释所示,如果在closures写入程序之前调用flush() ,则需要在try块中return (隐式或显式)语句之前执行此操作。 目前还没有办法确保调用者这样做我认为,所以这必须loggingwriteFileWriter

再次更新

上面的更新使@SuppressWarning不必要的,因为它需要函数将资源返回给调用者,所以本身不需要closures。 不幸的是,这使我们回到了开始的情况:警告现在移回到呼叫方。

所以要正确地解决这个问题,我们需要一个自定义的AutoClosable ,每当它closures时,下划线BufferedWriter将被flush() 。 实际上,这向我们展示了绕过警告的另一种方式,因为BufferWriter从来不以任何方式closures。