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

Posted

技术标签:

【中文标题】在 try-with-resources 块中管理多个链接资源的正确习惯用法?【英文标题】:Correct idiom for managing multiple chained resources in try-with-resources block? 【发布时间】:2012-09-15 04:40:09 【问题描述】:

Java 7 try-with-resources 语法(也称为 ARM 块 (Automatic Resource Management))在仅使用一个 @987654321 时非常简洁明了@资源。但是,当我需要声明多个相互依赖的资源时,我不确定什么是正确的习惯用法,例如 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.Closeable 的 close 方法不同,此 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' 永远不会关闭


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

【问题讨论】:

当然,如果底层 FileWriter 的构造函数抛出异常,它甚至不会被打开,一切正常。第一个示例的内容是,如果 FileWriter 被创建,但 BufferedWriter 的构造函数抛出异常,会发生什么。 值得注意的是BufferedWriter不会抛出异常。有没有一个你能想到的例子,这个问题不是纯粹的学术问题。 @PeterLawrey 是的,你说得对,在这种情况下 BufferedWriter 的构造函数很可能不会抛出异常,但正如我所指出的,这个问题涉及任何装饰器风格的资源。但例如public BufferedWriter(Writer out, int sz) 可以抛出IllegalArgumentException。此外,我可以使用一个类扩展 BufferedWriter,该类会从其构造函数中抛出一些东西或创建我需要的任何自定义包装器。 BufferedWriter 构造函数可以轻松抛出异常。 OutOfMemoryError 可能是最常见的一个,因为它为缓冲区分配了相当大的内存块(尽管可能表明您要重新启动整个进程)。 /如果您不关闭并希望保留内容(通常非异常情况),则需要flush您的BufferedWriterFileWriter 选择“默认”文件编码 - 最好是明确的。 @Natix 我希望 SO 中的所有问题都像这个问题一样得到充分研究和明确。我希望我能投票 100 次。 【参考方案1】:

这是我对替代方案的看法:

1)

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

对我来说,15 年前从传统 C++ 转向 Java 的最大好处是您可以信任您的程序。即使事情很糟糕并且出错了,他们经常这样做,我希望其余的代码是最好的行为和玫瑰的气味。实际上,BufferedWriter 可能会在这里抛出异常。例如,内存不足并不少见。对于其他装饰器,您知道哪些java.io 包装器类从它们的构造器中抛出检查异常吗?我不。如果您依赖于那种晦涩的知识,那么代码的可理解性就不会太好。

还有“破坏”。如果出现错误情况,那么您可能不想将垃圾刷新到需要删除的文件中(未显示的代码)。当然,删除文件也是另一个有趣的错误处理操作。

通常,您希望finally 块尽可能短且可靠。添加刷新无助于实现这一目标。对于许多版本,JDK 中的一些缓冲类有一个错误,即来自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 仍然是一个不充分的修复,您应该坚持使用 Execute Around idiom,它在 Java SE 8 中应该具有更清晰的闭包语法。

【讨论】:

在版本 3 中,你怎么知道 bw 不需要调用 close ?即使您可以确定它,那不也是像您在版本 1 中提到的那样“晦涩的知识”吗? 软件开发原因使 2 成为更好的选择”你能更详细地解释一下这句话吗? 你能举一个“Execute Around idiom withclosures”的例子吗 你能解释一下什么是“Java SE 8 中更清晰的闭包语法”吗? “Execute Around idiom”的例子在这里:***.com/a/342016/258772【参考方案2】:

第一个样式是suggested by Oracle。 BufferedWriter 不会抛出已检查的异常,因此如果抛出任何异常,程序预计不会从中恢复,使得资源恢复大多没有意义。

主要是因为它可能发生在线程中,线程终止但程序仍在继续 - 例如,暂时的内存中断时间不足以严重损害程序的其余部分。不过,这是一个相当极端的情况,如果它经常发生以至于导致资源泄漏成为问题,那么 try-with-resources 是您的问题中最少的一个。

【讨论】:

这也是Effective Java第三版推荐的方法。【参考方案3】:

选项 4

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

选项 5

不要非常小心地使用 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();

不好,因为如果构造 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();

【讨论】:

【参考方案4】:

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

【讨论】:

如果您的 tryfinally 块都抛出异常,则此构造将丢失第一个(并且可能更有用)的。【参考方案5】:

同意早期的 cmets:最简单的是 (2) 使用 Closeable 资源并在 try-with-resources 子句中按顺序声明它们。如果您只有AutoCloseable,您可以将它们包装在另一个(嵌套)类中,该类仅检查close 是否只被调用一次(外观模式),例如通过拥有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、TS),如果你首先链接 ST,你会得到相同的结果em>,然后是 U,对应于 (ST)U,或者如果你先将 TU 链接起来,然后是S,对应S(TU)。但是,在单个工厂函数中写出明确的三重链会更清楚。

【讨论】:

我是否正确收集到仍然需要使用 try-with-resource,如 try (BufferedWriter writer = &lt;BufferedWriter, FileWriter&gt;createChainedResource(file)) /* work with writer */ @ErikE 是的,您仍然需要使用 try-with-resources,但您只需要对链接资源使用单个函数:工厂函数 encapsulates链接。我添加了一个使用示例;谢谢!【参考方案6】:

由于你的资源是嵌套的,你的 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

【讨论】:

这与我的第二个示例非常相似。如果没有异常,FileWriter的close会被调用两次。【参考方案7】:

我会说不要使用 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

【讨论】:

【参考方案8】:

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

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(隐式或显式)语句之前这样做。我认为目前无法确保调用者这样做,因此必须为writeFileWriter 记录这一点。

再次更新

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

所以要正确解决这个问题,我们需要一个自定义的AutoClosable,只要它关闭,下划线BufferedWriter 应该是flush()ed。实际上,这向我们展示了另一种绕过警告的方法,因为BufferWriter 永远不会以任何方式关闭。

【讨论】:

警告有其含义:我们可以确定bw 真的会写出数据吗?毕竟它缓冲的,所以它有时只需要写入磁盘(当缓冲区已满和/或在flush()close()方法上)。我想应该调用flush() 方法。但是,无论如何,当我们在一批中立即写出它时,就不需要使用缓冲的写入器。如果你不修复代码,你最终可能会以错误的顺序将数据写入文件,甚至根本没有写入文件。 如果需要调用flush(),它应该在调用者决定关闭FileWriter之前发生。所以这应该发生在 printToFile 中,就在 try 块中的任何 returns 之前。这不应是writeFileWriter 的一部分,因此警告不是关于该函数内部的任何内容,而是该函数的调用者。如果我们有注释 @LiftWarningToCaller("wanrningXXX") 它将有助于这种情况和类似情况。

以上是关于在 try-with-resources 块中管理多个链接资源的正确习惯用法?的主要内容,如果未能解决你的问题,请参考以下文章

fortify 抱怨 try-with-resources 块中的资源

如何修复“SQLServerException:连接已关闭”。在这个 Try-With-Resources 块中出现?

ZipOutputStream 类的 closeEntry()

了解 Java 9 中的 try-with-resources 增强

try-with-resources 关闭异常中的流控制

在 System.in 中使用 try-with-resources [重复]