在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。 它只会通过包装BufferedWriter
的close
方法来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。