关于Java:在try-with-resources块中管理多个链接资源的正确用法?

Correct idiom for managing multiple chained resources in try-with-resources block?

仅使用一个AutoCloseable资源时,Java 7 try-with-resources语法(也称为ARM块(自动资源管理))非常好,简短而直接。但是,当我需要声明相互依赖的多个资源时,例如不确定FileWriter和包装它的BufferedWriter,我不确定正确的惯用语是什么。当然,这个问题涉及包装某些AutoCloseable资源的任何情况,而不仅是这两个特定的类。

我提出了以下三种选择:

1)

我见过的天真习惯是在ARM管理的变量中只声明顶层包装器:

1
2
3
4
5
6
7
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)

1
2
3
4
5
6
7
8
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

Closes this stream and releases any system resources associated with it. If the stream is already closed then invoking this method has no effect.

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

Note that unlike the close method of java.io.Closeable, this close method is not required to be idempotent. In other words, calling this close method more than once may have some visible side effect, unlike Closeable.close which is required to have no effect if called more than once. However, implementers of this interface are strongly encouraged to make their close methods idempotent.

3)

1
2
3
4
5
6
7
8
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发出警告,我认为这是一个错误警报,但它仍然是警告您必须处理以下警告:

Resource leak: 'bw' is never closed

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


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

1)

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

对我来说,15年前从传统C ++到Java的最好的事情就是您可以信任您的程序。即使事情经常发生,而且经常出错,我也希望代码的其余部分能够表现出最佳的行为和玫瑰的香味。实际上,BufferedWriter可能会在此处引发异常。例如,用尽内存并不稀奇。对于其他装饰器,您是否知道哪个java.io包装器类从其构造函数中引发了已检查的异常?我不。如果您依赖那种晦涩的知识,那么代码的可理解性就不好。

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

通常,您希望finally块尽可能短且可靠。添加冲洗功能无济于事。对于许多发行版,JDK中的某些缓冲类都有一个错误,即close中的flush异常导致未调用装饰对象上的close。尽管已经修复了一段时间,但可以从其他实现中获得期望。

2)

1
2
3
4
5
6
try (
    FileWriter fw = new FileWriter(file);
    BufferedWriter bw = new BufferedWriter(fw)
) {
    bw.write(text);
}

我们仍然在隐式finally块中刷新(现在使用重复的close-随着添加更多装饰器,情况会变得更糟),但是构造是安全的,我们必须隐式finally块,因此即使失败的flush也不会防止资源释放。

3)

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

这里有一个错误。应该:

1
2
3
4
5
try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
    bw.flush();
}

实际上,一些执行不佳的装饰器是资源,因此需要可靠地关闭。另外,某些流可能需要以特定的方式关闭(也许它们正在执行压缩,并且需要写一些位才能结束,而不能仅刷新所有内容。

判决

尽管3是技术上优越的解决方案,但是软件开发的原因使2成为更好的选择。但是,try-with-resource仍然是一个不足的解决方案,您应该坚持Execute Around惯用语,该惯用语应具有更清晰的语法,并带有Java SE 8中的闭包。


第一种样式是Oracle建议的样式。 BufferedWriter不会引发检查的异常,因此,如果引发任何异常,则该程序将无法从中恢复,从而使资源恢复几乎没有实际意义。

主要是因为它可能在线程中发生,并且线程死亡,但是程序仍在继续-比方说,有一个暂时的内存中断,持续时间不足以严重损害程序的其余部分。但是,这是一个相当极端的情况,如果发生的频率足以使资源泄漏成为问题,那么使用资源进行尝试是您最少的问题。


选项4

如果可以的话,将资源更改为可关闭,而不是自动关闭。构造函数可以被链接的事实表明,两次关闭资源并不是闻所未闻的。 (在ARM之前也是如此)。

选项5

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

选项6

不要使用ARM,而要自己进行try / catch的finally close()调用。

为什么我认为这个问题不是ARM独有的

在所有这些示例中,finally close()调用应位于catch块中。出于可读性而忽略。

不好,因为fw可以关闭两次。 (这对于FileWriter很好,但在您的假设示例中不适用):

1
2
3
4
5
6
7
8
9
10
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不会关闭。 (再次,不会发生,但在您的假设示例中):

1
2
3
4
5
6
7
8
9
FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( bw != null ) bw.close();
}

同意前面的意见:(2)最简单的方法是使用Closeable资源并在try-with-resources子句中按顺序声明它们。如果只有AutoCloseable,则可以将它们包装在另一个(嵌套)类中,该类仅检查close仅被调用一次(门面模式),例如通过private bool isClosed;。实际上,即使Oracle仅(1)链接了构造函数,也没有正确处理整个链中的异常。

另外,您可以使用静态工厂方法手动创建链接的资源。这封装了链,如果中途失败则进行清理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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子句中将其用作单个资源:

1
2
3
try (BufferedWriter writer = createBufferedWriterFromFile(file)) {
  // Work with writer.
}

复杂性来自处理多个异常。否则,这仅仅是"到目前为止您已经获得的紧密资源"。一种常见的做法似乎是首先初始化将持有资源的对象的变量初始化为null(此处为fileWriter),然后在清理中包含空检查,但这似乎是不必要的:如果构造函数失败,没有什么需要清理的,因此我们可以让该异常传播,从而使代码略微简化。

您可能可以一般地这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,T和S),那么,如果先链接S和T,然后是U,您将获得相同的结果,对应于(ST)U,或者如果您先链接了T和U,则链接S,对应于S(TU)。但是,仅在一个工厂函数中写出一个明确的三重链会更加清楚。


我只是想基于Jeanne Boyarsky的建议,即不使用ARM,而是确保FileWriter始终完全关闭一次。不要以为这里有任何问题...

1
2
3
4
5
6
7
8
9
10
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循环来做迭代器可以做的事情一样。


由于资源是嵌套的,因此try-with子句也应为:

1
2
3
4
5
6
7
8
9
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
}


我的解决方案是执行"提取方法"重构,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
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可以写

1
2
3
4
5
6
7
8
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
    }
}

要么

1
2
3
4
5
6
7
8
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,每当关闭AutoClosable时,都必须对flush()下划线。实际上,这向我们展示了另一种绕过警告的方法,因为BufferWriter绝不会以任何一种方式关闭。


我会说不要使用ARM并继续使用Closeable。使用方法如

1
2
3
4
5
6
7
8
9
10
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