Correct idiom for managing multiple chained resources in try-with-resources block?
仅使用一个
我提出了以下三种选择:
1)
我见过的天真习惯是在ARM管理的变量中只声明顶层包装器:
1 2 3 4 5 6 7 | static void printToFile1(String text, File file) { try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { bw.write(text); } catch (IOException ex) { // handle ex } } |
这很不错,也很简短,但是坏了。因为基础
2)
1 2 3 4 5 6 7 8 | 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 } } |
在这里,基础资源和包装资源都在ARM管理的变量中声明,因此它们肯定会被关闭,但是基础
对于这两个都实现了
Closes this stream and releases any system resources associated with it. If the stream is already closed then invoking this method has no effect.
但是,在一般情况下,我可以拥有仅实现
Note that unlike the close method of java.io.Closeable, this close method is not required to be idempotent. In other words, calling this close method more than once may have some visible side effect, unlike Closeable.close which is required to have no effect if called more than once. However, implementers of this interface are strongly encouraged to make their close methods idempotent.
3)
1 2 3 4 5 6 7 8 | 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 } } |
这个版本在理论上应该是正确的,因为只有
另一方面,语法有点不规则,并且,Eclipse发出警告,我认为这是一个错误警报,但它仍然是警告您必须处理以下警告:
Resource leak: 'bw' is never closed
那么,该采用哪种方法呢?还是我错过了其他正确的成语?
这是我对替代方案的看法:
1)
1 2 3 |
对我来说,15年前从传统C ++到Java的最好的事情就是您可以信任您的程序。即使事情经常发生,而且经常出错,我也希望代码的其余部分能够表现出最佳的行为和玫瑰的香味。实际上,
还有"破坏"。如果出现错误情况,那么您可能不想将垃圾刷新到需要删除的文件(未显示代码)。当然,尽管删除文件也是进行错误处理的另一有趣操作。
通常,您希望
2)
1 2 3 4 5 6 | try ( FileWriter fw = new FileWriter(file); BufferedWriter bw = new BufferedWriter(fw) ) { bw.write(text); } |
我们仍然在隐式finally块中刷新(现在使用重复的
3)
1 2 3 4 | try (FileWriter fw = new FileWriter(file)) { BufferedWriter bw = new BufferedWriter(fw); bw.write(text); } |
这里有一个错误。应该:
1 2 3 4 5 | try (FileWriter fw = new FileWriter(file)) { BufferedWriter bw = new BufferedWriter(fw); bw.write(text); bw.flush(); } |
实际上,一些执行不佳的装饰器是资源,因此需要可靠地关闭。另外,某些流可能需要以特定的方式关闭(也许它们正在执行压缩,并且需要写一些位才能结束,而不能仅刷新所有内容。
判决
尽管3是技术上优越的解决方案,但是软件开发的原因使2成为更好的选择。但是,try-with-resource仍然是一个不足的解决方案,您应该坚持Execute Around惯用语,该惯用语应具有更清晰的语法,并带有Java SE 8中的闭包。
第一种样式是Oracle建议的样式。
主要是因为它可能在线程中发生,并且线程死亡,但是程序仍在继续-比方说,有一个暂时的内存中断,持续时间不足以严重损害程序的其余部分。但是,这是一个相当极端的情况,如果发生的频率足以使资源泄漏成为问题,那么使用资源进行尝试是您最少的问题。
选项4
如果可以的话,将资源更改为可关闭,而不是自动关闭。构造函数可以被链接的事实表明,两次关闭资源并不是闻所未闻的。 (在ARM之前也是如此)。
选项5
不要非常小心地使用ARM和代码,以确保不会两次调用close()!
选项6
不要使用ARM,而要自己进行try / catch的finally close()调用。
为什么我认为这个问题不是ARM独有的
在所有这些示例中,finally close()调用应位于catch块中。出于可读性而忽略。
不好,因为fw可以关闭两次。 (这对于FileWriter很好,但在您的假设示例中不适用):
1 2 3 4 5 6 7 8 9 10 | 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(); } |
不好,因为如果构造BufferedWriter异常,则fw不会关闭。 (再次,不会发生,但在您的假设示例中):
1 2 3 4 5 6 7 8 9 | FileWriter fw = null; BufferedWriter bw = null; try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write(text); } finally { if ( bw != null ) bw.close(); } |
同意前面的意见:(2)最简单的方法是使用
另外,您可以使用静态工厂方法手动创建链接的资源。这封装了链,如果中途失败则进行清理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; } } |
然后,您可以在try-with-resources子句中将其用作单个资源:
1 2 3 |
复杂性来自处理多个异常。否则,这仅仅是"到目前为止您已经获得的紧密资源"。一种常见的做法似乎是首先初始化将持有资源的对象的变量初始化为
您可能可以一般地这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; } } |
同样,您可以链接三个资源,等等。
从数学上讲,您甚至可以一次通过链接两个资源来链接三遍,这将是关联的,这意味着您将在成功时获得相同的对象(因为构造函数是关联的),并且在失败时会获得相同的例外在任何构造函数中。假设您在上面的链中添加了S(因此您以V开头,并以S结束,依次应用U,T和S),那么,如果先链接S和T,然后是U,您将获得相同的结果,对应于(ST)U,或者如果您先链接了T和U,则链接S,对应于S(TU)。但是,仅在一个工厂函数中写出一个明确的三重链会更加清楚。
我只是想基于Jeanne Boyarsky的建议,即不使用ARM,而是确保FileWriter始终完全关闭一次。不要以为这里有任何问题...
1 2 3 4 5 6 7 8 9 10 | 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只是语法糖,我们不能总是使用它来代替finally块。就像我们不能总是使用for-each循环来做迭代器可以做的事情一样。
由于资源是嵌套的,因此try-with子句也应为:
1 2 3 4 5 6 7 8 9 | 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 } |
我的解决方案是执行"提取方法"重构,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 | 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(); } }; } |
1 2 3 4 5 6 7 8 | 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 } } |
要么
1 2 3 4 5 6 7 8 | static void printToFile(String text, File file) { try (FileWriter fw = new FileWriter(file); AutoCloseable w = writeFileWriter(fw, text)){ } catch (Exception ex) { // handle ex } } |
对于类库设计人员,我建议他们使用其他方法来扩展
对于语言设计师而言,教训是添加新功能可能意味着添加很多其他功能。在这种Java情况下,显然ARM功能将与资源所有权转移机制一起更好地工作。
更新
最初,上面的代码需要
正如评论所建议的,如果要在关闭编写器之前调用
再次更新
上面的更新使
因此,要正确解决此问题,我们需要一个自定义的
我会说不要使用ARM并继续使用Closeable。使用方法如
1 2 3 4 5 6 7 8 9 10 | public void close(Closeable... closeables) { for (Closeable closeable: closeables) { try { closeable.close(); } catch (IOException e) { // you can't much for this } } } |
另外,您应该考虑调用