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

时间:2012-09-23 13:47:09

标签: java try-with-resources

Java 7 try-with-resources 语法(也称为ARM块(自动资源管理))在仅使用一个{{{{自动资源管理)时很简单,简单明了1}}资源。但是,当我需要声明彼此依赖的多个资源时,我不确定什么是正确的习惯用法,例如包裹它的AutoCloseableFileWriter。当然,这个问题涉及包裹一些BufferedWriter资源的任何情况,而不仅仅是这两个特定的类。

我提出了以下三种选择:

1)

我看到的天真习惯是只声明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 将不会被关闭

2)

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不同,如果多次调用则需要它无效。但是,强烈建议强制使用此接口的实现者使其接近的方法具有幂等性。

3)

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'永远不会关闭


那么,采用哪种方法?或者我错过了一些正确的成语?

8 个答案:

答案 0 :(得分:74)

以下是我对替代方案的看法:

1)

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

对我来说,15年前从传统C ++到Java的最好的事情就是你可以相信你的程序。即使事情发生在他们经常做的事情中,我也希望其余的代码能够达到最好的行为和玫瑰的味道。实际上,BufferedWriter可能会在此处引发异常。例如,耗尽内存并不罕见。对于其他装饰器,您知道哪个java.io包装器类从其构造函数中抛出一个已检查的异常吗?我不。如果你依赖那种模糊的知识,那么代码的可理解性就不会很好。

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

通常,您希望finally块尽可能短且可靠。添加刷新对此目标没有帮助。对于许多版本,JDK中的某些缓冲类存在一个错误,即flush中的close异常导致装饰对象上的close未被调用。虽然已经修复了一段时间,但期望从其他实现中获得。

2)

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

我们仍在冲洗隐含的finally块(现在重复close - 这会因为添加更多装饰器而变得更糟),但构造是安全的,我们必须隐含finally块,所以即使是失败的{ {1}}不会阻止资源释放。

3)

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 OracleBufferedWriter不会抛出已检查的异常,因此如果抛出任何异常,程序不会从中恢复,从而使资源恢复变得没有实际意义。

主要是因为它可能发生在一个线程中,线程死亡但程序仍在继续 - 比如说,暂时的内存中断不足以严重损害程序的其余部分。然而,这是一个相当不利的案例,如果它经常发生,足以使资源泄漏成为问题,那么资源尝试是你问题最少的。

答案 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永远不会以任何方式关闭。