179

Java 7 try-with-resources语法(也称为 ARM 块(自动资源管理))在仅使用一个AutoCloseable资源时非常好、简短且直接。但是,当我需要声明多个相互依赖的资源时,我不确定什么是正确的习惯用法,例如包装它的 aFileWriter和 a 。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块中直接关闭。它只会通过closewrapping 的方法关闭BufferedWriterbw问题是,如果从的构造函数中抛出异常,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() 会被调用两次:不仅直接调用,还通过 wrapping调用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' 永远不会关闭


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

4

8 回答 8

80

Here's my take on the alternatives:

1)

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

For me, the best thing coming to Java from traditional C++ 15 years ago was that you could trust your program. Even if things are in the muck and going wrong, which they often do, I want the rest of the code to be on best behaviour and smelling of roses. Indeed, the BufferedWriter might throw an exception here. Running out of memory wouldn't be unusual, for instance. For other decorators, do you know which of the java.io wrapper classes throw a checked exception from their constructors? I don't. Doesn't do code understandability much good if you rely upon that sort of obscure knowledge.

Also there's the "destruction". If there is an error condition, then you probably don't want to be flushing rubbish to a file that needs deleting (code for that not shown). Although, of course, deleting the file is also another interesting operation to do as error handling.

Generally you want finally blocks to be as short and reliable as possible. Adding flushes does not help this goal. For many releases some of the buffering classes in the JDK had a bug where an exception from flush within close caused close on the decorated object not be called. Whilst that has been fixed for some time, expect it from other implementations.

2)

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

We're still flushing in the implicit finally block (now with repeated close - this gets worse as you add more decorators), but the construction is safe and we have to implicit finally blocks so even a failed flush doesn't prevent resource release.

3)

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

There's a bug here. Should be:

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

Some poorly implemented decorators are in fact resource and will need to be closed reliably. Also some streams may need to be closed in a particular way (perhaps they are doing compression and need to write bits to finish off, and can't just flush everything.

Verdict

Although 3 is a technically superior solution, software development reasons make 2 the better choice. However, try-with-resource is still an inadequate fix and you should stick with the Execute Around idiom, which should have a clearer syntax with closures in Java SE 8.

于 2012-09-30T21:27:58.083 回答
22

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

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

于 2013-08-28T19:18:32.063 回答
5

选项 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();
}
于 2012-09-23T18:01:17.730 回答
3

同意前面的评论:最简单的是(2)使用Closeable资源并在 try-with-resources 子句中按顺序声明它们。如果AutoCloseable您只有. 在实践中,甚至 Oracle 也只是(1)链接了构造函数,并且没有正确处理链中的异常。closeprivate bool isClosed;

或者,您可以使用静态工厂方法手动创建链接资源;这封装了链,并在它中途失败时处理清理:

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结束,依次应用UTS),如果你先链接ST,然后是U ,你会得到相同的结果,对应于(ST)U,或者如果您首先链接TU,那么S,对应于S(TU)。但是,在单个工厂函数中写出显式的三重链会更清楚。

于 2015-08-30T23:57:48.733 回答
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
}
于 2012-09-26T15:28:48.557 回答
2

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

于 2012-09-30T21:33:20.120 回答
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
    }
}

对于类库设计者,我会建议他们AutoClosable使用附加方法来扩展接口以抑制关闭。在这种情况下,我们可以手动控制关闭行为。

对于语言设计师来说,教训是添加新功能可能意味着添加很多其他功能。在这种 Java 案例中,显然 ARM 功能与资源所有权转移机制配合使用会更好。

更新

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

正如评论所建议的,如果flush()要在关闭编写器之前调用,我们需要return在 try 块内的任何(隐式或显式)语句之前这样做。我认为目前无法确保调用者这样做,因此必须为writeFileWriter.

再次更新

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

所以要正确解决这个问题,我们需要一个自定义AutoClosable的,只要它关闭,下划线BufferedWriter就会被flush()编辑。实际上,这向我们展示了绕过警告的另一种方法,因为BufferWriter无论哪种方式都不会关闭。

于 2013-04-26T06:23:03.023 回答
0

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

于 2012-09-27T10:58:38.777 回答