Java 7 try-with-resources 语法(也称为ARM块(自动资源管理))在仅使用一个{{{{自动资源管理)时很简单,简单明了1}}资源。但是,当我需要声明彼此依赖的多个资源时,我不确定什么是正确的习惯用法,例如包裹它的AutoCloseable
和FileWriter
。当然,这个问题涉及包裹一些BufferedWriter
资源的任何情况,而不仅仅是这两个特定的类。
我提出了以下三种选择:
我看到的天真习惯是只声明ARM管理变量中的顶级包装器:
AutoCloseable
这很好而且很短,但它已经坏了。由于未在变量中声明基础static void printToFile1(String text, File file) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
bw.write(text);
} catch (IOException ex) {
// handle ex
}
}
,因此永远不会在生成的FileWriter
块中直接关闭它。它只能通过包装finally
的{{1}}方法关闭。问题是,如果从close
的构造函数中抛出异常,则不会调用其BufferedWriter
,因此基础bw
将不会被关闭
close
这里,底层资源和包装资源都是在ARM管理的变量中声明的,因此它们肯定会被关闭,但底层的FileWriter
将被调用两次:不仅直接,而且通过包裹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
}
}
。
对于这两个实现fw.close()
(bw.close()
的子类型)的特定类,这应该不是问题,其合同规定允许多次调用Closeable
:
关闭此流并释放与其关联的所有系统资源。如果流已经关闭,则调用此方法无效。
但是,在一般情况下,我可以拥有仅实现AutoCloseable
(而不是close
)的资源,这并不能保证AutoCloseable
可以多次调用:< / p>
请注意,与java.io.Closeable的close方法不同,此close方法不需要是幂等的。换句话说,不止一次调用此close方法可能会产生一些可见的副作用,这与Closeable.close不同,如果多次调用则需要它无效。但是,强烈建议强制使用此接口的实现者使其接近的方法具有幂等性。
Closeable
此版本在理论上应该是正确的,因为只有close
代表需要清理的真实资源。 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
就足够了。
另一方面,语法有点不规则,并且Eclipse发出警告,我认为这是一种误报,但它仍然是一个必须处理的警告:
资源泄漏:'bw'永远不会关闭
那么,采用哪种方法?或者我错过了一些正确的成语?
答案 0 :(得分:74)
以下是我对替代方案的看法:
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
bw.write(text);
}
对我来说,15年前从传统C ++到Java的最好的事情就是你可以相信你的程序。即使事情发生在他们经常做的事情中,我也希望其余的代码能够达到最好的行为和玫瑰的味道。实际上,BufferedWriter
可能会在此处引发异常。例如,耗尽内存并不罕见。对于其他装饰器,您知道哪个java.io
包装器类从其构造函数中抛出一个已检查的异常吗?我不。如果你依赖那种模糊的知识,那么代码的可理解性就不会很好。
还有“破坏”。如果存在错误情况,那么您可能不希望将垃圾清除到需要删除的文件(未显示的代码)。当然,删除文件也是另一个有趣的错误处理操作。
通常,您希望finally
块尽可能短且可靠。添加刷新对此目标没有帮助。对于许多版本,JDK中的某些缓冲类存在一个错误,即flush
中的close
异常导致装饰对象上的close
未被调用。虽然已经修复了一段时间,但期望从其他实现中获得。
try (
FileWriter fw = new FileWriter(file);
BufferedWriter bw = new BufferedWriter(fw)
) {
bw.write(text);
}
我们仍在冲洗隐含的finally块(现在重复close
- 这会因为添加更多装饰器而变得更糟),但构造是安全的,我们必须隐含finally块,所以即使是失败的{ {1}}不会阻止资源释放。
flush
这里有一个错误。应该是:
try (FileWriter fw = new FileWriter(file)) {
BufferedWriter bw = new BufferedWriter(fw);
bw.write(text);
}
一些执行不佳的装饰器实际上是资源,需要可靠地关闭。还有一些流可能需要以特定方式关闭(可能它们正在进行压缩并需要写入位来完成,而不能只是刷新所有内容。
虽然3是技术上优越的解决方案,但软件开发的原因使2成为更好的选择。但是,try-with-resource仍然是一个不充分的修复,你应该坚持使用Execute Around idiom,它应该有一个更清晰的语法与Java SE 8中的闭包。
答案 1 :(得分:19)
第一种风格是suggested by Oracle。 BufferedWriter
不会抛出已检查的异常,因此如果抛出任何异常,程序不会从中恢复,从而使资源恢复变得没有实际意义。
主要是因为它可能发生在一个线程中,线程死亡但程序仍在继续 - 比如说,暂时的内存中断不足以严重损害程序的其余部分。然而,这是一个相当不利的案例,如果它经常发生,足以使资源泄漏成为问题,那么资源尝试是你问题最少的。
答案 2 :(得分:5)
选项4
如果可以,请将资源更改为可关闭,而不是AutoClosable。构造函数可以链接的事实意味着关闭资源两次并不是闻所未闻。 (在ARM之前也是如此。)更多内容见下文。
选项5
不要非常小心地使用ARM和代码来确保不会调用close()两次!
选项6
不要使用ARM并在try / catch中自己进行最后的close()调用。
为什么我认为此问题不是ARM独有的
在所有这些示例中,finally close()调用应该在catch块中。遗漏以便于阅读。
不好,因为fw可以关闭两次。 (这对于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();
}
没有好处,因为如果构造BufferedWriter时出现异常,则fw不会关闭。 (再次,不可能发生,但在您的假设示例中):
FileWriter fw = null;
BufferedWriter bw = null;
try {
fw = new FileWriter(file);
bw = new BufferedWriter(fw);
bw.write(text);
} finally {
if ( bw != null ) bw.close();
}
答案 3 :(得分:3)
我只想建立Jeanne Boyarsky的建议,即不使用ARM但确保FileWriter始终关闭一次。不要以为这里有任何问题......
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循环来做迭代器可能做的事情。
答案 4 :(得分:3)
与先前的评论一致:最简单的是(2)使用Closeable
资源并在try-with-resources子句中按顺序声明它们。如果您只有AutoCloseable
,则可以将它们包装在另一个(嵌套)类中,该类仅检查close
仅被调用一次(Facade Pattern),例如通过private bool isClosed;
。在实践中,甚至Oracle只是(1)链接构造函数,并且不能正确处理链中的异常。
或者,您可以使用静态工厂方法手动创建链式资源;这封装了链,如果部分失败则处理清理:
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子句中将其用作单个资源:
try (BufferedWriter writer = createBufferedWriterFromFile(file)) {
// Work with writer.
}
复杂性来自处理多个异常;否则它只是“你迄今为止获得的资源”。一种常见的做法似乎是首先将保存资源的对象的变量初始化为null
(此处为fileWriter
),然后在清理中包含空检查,但这似乎是不必要的:构造函数失败,没有什么可以清理的,所以我们可以让这个异常传播,这简化了代码。
你可能会这样做:
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 <如果您首先链接 S 和 T > em>,然后 U ,对应(ST)U ,或者如果你第一次链接 T 和 U ,然后 S ,对应 S(TU)。但是,在单个工厂函数中写出一个明确的三重链会更清楚。
答案 5 :(得分:2)
由于您的资源是嵌套的,因此您的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
}
答案 6 :(得分:0)
我想说不要使用ARM并继续使用Closeable。使用方法,
public void close(Closeable... closeables) {
for (Closeable closeable: closeables) {
try {
closeable.close();
} catch (IOException e) {
// you can't much for this
}
}
}
此外,您应该考虑致电BufferedWriter
,因为它不仅仅是将FileWriter
委托给我,而是像flushBuffer
一样进行清理。
答案 7 :(得分:0)
我的解决方案是进行“提取方法”重构,如下所示:
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
}
}
对于类lib设计者,我建议他们使用另一种方法扩展AutoClosable
接口以抑制关闭。在这种情况下,我们可以手动控制关闭行为。
对于语言设计师来说,经验教训是添加新功能可能意味着添加很多其他功能。在这种Java情况下,显然ARM功能将更好地与资源所有权转移机制一起使用。
<强>更新强>
最初上面的代码需要@SuppressWarning
,因为函数内的BufferedWriter
需要close()
。
正如评论所建议的那样,如果在关闭writer之前要调用flush()
,我们需要在try块中的任何return
(隐式或显式)语句之前执行此操作。目前我无法确保调用者这样做,因此必须记录writeFileWriter
。
再次更新
上述更新使@SuppressWarning
变得不必要,因为它需要函数将资源返回给调用者,因此不需要关闭它。不幸的是,这让我们回到了这种情况的开始:警告现在又回到了呼叫方。
因此,为了正确解决这个问题,我们需要一个自定义的AutoClosable
,只要它关闭,下划线BufferedWriter
就会flush()
。实际上,这向我们展示了绕过警告的另一种方法,因为BufferWriter
永远不会以任何方式关闭。