在试用资源块中管理多个链式资源的习惯用法是否正确?

当只使用一个 AutoCloseable资源时,Java7尝试使用资源语法(也称为 ARM 块(自动资源管理))是漂亮、简短和直接的。然而,当我需要声明多个相互依赖的资源时,我不确定什么是正确的习惯用法,例如一个 FileWriter和一个包装它的 BufferedWriter。当然,这个问题涉及到包装某些 AutoCloseable资源的任何情况,而不仅仅是这两个特定的类。

我提出了以下三种选择:

1)

我看到的一个简单的习惯做法是在 ARM 管理的变量中仅声明顶级包装器:

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块中直接关闭。只有通过包装 BufferedWriterclose方法,它才会被关闭。问题是,如果从 bw的构造函数抛出异常,则不会调用它的 close,因此也不会调用底层的 FileWriter 将不会关闭

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
}
}

在这里,底层资源和包装资源都是在 ARM 管理的变量中声明的,因此它们肯定都是关闭的,但是底层 fw.close() 将被召唤两次: 不仅直接关闭,而且还通过包装 bw.close()关闭。

对于这两个同时实现 Closeable(它是 AutoCloseable的子类型)的特定类来说,这应该不成问题,因为它们的协议规定允许对 close进行多次调用:

关闭此流并释放与其关联的任何系统资源。如果流已经关闭,那么调用此方法将无效。

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

注意,与 java.io 的 close 方法不同。可闭的,这个闭方法不需要是幂等的。换句话说,多次调用这个 close 方法可能会产生一些可见的副作用,不像 Closeable.close,如果多次调用则不会产生任何效果。但是,强烈建议该接口的实现者使其 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,因此只关闭底层的 fw就足够了。

另一方面,语法有点不规则,而且 Eclipse 发出了一个警告,我认为这是一个错误的警告,但它仍然是一个必须处理的警告:

资源泄漏: “ bw”永远不会关闭


那么,我该用哪种方法呢? 还是我错过了一些其他的成语,那就是 正确1?

36324 次浏览

选择四

如果可以,将资源更改为可关闭,而不是自动关闭。可以链接构造函数这一事实意味着关闭资源两次并非闻所未闻。(在 ARM 之前也是如此。)更多详情请见下文。

选择五

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

选择6

不要使用 ARM 并且在 try/catch 中调用 finally 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();
}

没有好处,因为 fw 没有关闭,如果构造 BufferedWriter 时发生异常。(同样,不可能发生,但在你的假设例子中) :

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

因为您的资源是嵌套的,所以 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
}
}


}

您还应该考虑调用 close of BufferedWriter,因为它不仅委派 close to FileWriter,而且还会执行一些类似于 flushBuffer的清理工作。

以下是我对其他选择的看法:

1)

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

对我来说,15年前从传统 C + + 进入 Java 最好的事情就是你可以信任你的程序。即使事情一团糟并且出了问题(这种情况经常发生) ,我希望代码的其余部分都是最好的行为和玫瑰花香。事实上,BufferedWriter可能会在这里抛出异常。例如,内存不足并不罕见。对于其他装饰器,您是否知道哪个 java.io包装器类从它们的构造函数抛出一个检查过的异常?我不知道。如果您依赖于那种晦涩难懂的知识,那么代码可理解性就没有多大好处。

还有“破坏”。如果存在错误条件,那么您可能不希望将垃圾清除到需要删除的文件(未显示的代码)。当然,作为错误处理,删除文件也是另一个有趣的操作。

一般来说,您希望 finally块尽可能短和可靠。添加刷新并不能帮助实现这个目标。对于许多版本,JDK 中的一些缓冲类有一个 bug,其中 close中的 flush异常导致修饰对象上的 close不被调用。虽然这个问题已经解决了一段时间,但希望从其他实现中得到解决。

2)

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

我们仍然在使用隐式 finally 块(现在使用重复的 close-随着添加更多装饰符,情况会变得更糟) ,但是构造是安全的,我们必须隐式 finally 块,这样即使 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();
}

一些实现不佳的装饰器实际上是资源,需要可靠地关闭。此外,一些流可能需要以特定的方式关闭(也许它们正在进行压缩,需要写入位来完成,而不能只是刷新所有内容。)。

裁决

虽然3是一个技术上优越的解决方案,软件开发的原因使2更好的选择。但是,使用资源进行 try-with-resource 仍然不够,您应该坚持使用 围绕成语执行,它应该具有更清晰的 Java SE 8闭包语法。

我只是想建立在 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 循环来做迭代器可以做的事情一样。

我的解决方案是进行“提取方法”重构,如下所示:

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
}
}

对于类库设计人员,我建议他们使用一个额外的方法来扩展 AutoClosable接口,以抑制关闭。在这种情况下,我们可以手动控制关闭行为。

对于语言设计人员来说,这个教训就是添加一个新特性可能意味着添加许多其他特性。在这个 Java 案例中,显然 ARM 特性在资源所有权转移机制下会工作得更好。

更新

最初,上面的代码需要 @SuppressWarning,因为函数内部的 BufferedWriter需要 close()

如注释所示,如果在关闭编写器之前调用 flush(),则需要在 try 块中的任何 return(隐式或显式)语句之前调用 flush()。目前没有办法确保呼叫者这样做,我认为,所以这必须为 writeFileWriter记录。

再次更新

上面的更新使得 @SuppressWarning变得不必要,因为它需要函数将资源返回给调用者,所以它本身不需要关闭。不幸的是,这将我们拉回到最初的情况: 警告现在被移回到调用方。

因此,为了正确解决这个问题,我们需要一个定制的 AutoClosable,每当它关闭,下划线 BufferedWriter应该是 flush()ed。实际上,这向我们展示了另一种绕过警告的方法,因为 BufferWriter从未以任何一种方式关闭。

第一种是 甲骨文的建议BufferedWriter不会抛出已检查异常,因此如果抛出任何异常,程序都不会从中恢复,这使得资源恢复大多没有意义。

主要是因为它可能发生在一个线程中,虽然线程已经死亡,但是程序仍在继续——比如说,有一个临时的内存中断,这个时间不足以严重损害程序的其余部分。不过,这是一个相当棘手的问题,如果这种情况经常发生,导致资源泄漏成为一个问题,那么尝试使用资源就是最小的问题。

与前面的注释一致: 最简单的方法是 (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) ,然后在清理中包含一个 null 检查,但这似乎是不必要的: 如果构造函数失败,就没有什么可清理的了,所以我们可以让异常传播,这样可以简化一些代码。

你可以通常这样做:

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;
}
}

类似地,您可以链接三个资源,等等。

从数学的角度来说,你甚至可以通过一次链接两个资源来链接三次,而且它是关联的,这意味着你在成功时会得到相同的对象(因为构造函数是关联的) ,如果任何一个构造函数出现故障,也会得到相同的异常。假设你在上面的链中加入了一个 是的(所以你从一个 V开始并以一个 是的结束,依次应用 美国T是的) ,如果你先链 是的T,然后是 美国,对应于 (ST) U,或者如果你先链 T美国,然后是 是的,对应于 V3,你得到相同的结果。然而,在一个工厂函数中写出一个明确的三重链会更清楚。