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


168

仅使用一种资源时,Java 7 try-with-resources语法(也称为ARM块(自动资源管理))非常好,简短而直接AutoCloseable。但是,当我需要声明相互依赖的多个资源时(例如a FileWriter和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块中直接关闭。只能通过close包装方法将其关闭BufferedWriter。问题是,如果从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不同,后者需要多次调用才无效。但是,强烈建议该接口的实现者使其封闭方法成为幂等。

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
当然,如果基础FileWriter的构造函数抛出异常,则它甚至都不会打开,并且一切正常。第一个示例是关于如果创建FileWriter会发生什么,但是BufferedWriter的构造函数将引发异常。
Natix 2012年

6
值得注意的是BufferedWriter不会引发异常。有没有一个例子可以让您想到这个问题不是纯粹的学术问题。
彼得·劳瑞

10
@PeterLawrey是的,您是对的,在这种情况下,BufferedWriter的构造函数很可能不会引发异常,但是正如我所指出的,这个问题与任何装饰风格的资源有关。但例如public BufferedWriter(Writer out, int sz)可以抛出一个IllegalArgumentException。另外,我可以使用一个类扩展BufferedWriter,该类将从其构造函数中抛出某些内容或创建我需要的任何自定义包装器。
Natix 2012年

5
BufferedWriter构造可以很容易地抛出异常。OutOfMemoryError可能是最常见的一种,因为它为缓冲区分配了相当大的内存(尽管可能表明您想重新启动整个过程)。/你需要flush你的BufferedWriter,如果你不关闭并希望保留的内容(一般非例外情况)。FileWriter选取碰巧是“默认”文件编码的任何东西-最好是明确的。
汤姆·哈特芬

10
@Natix我希望SO中的所有问题都像这个问题一样得到充分的研究和阐明。我希望我可以对此进行100次投票。
极客

Answers:


75

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

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块,因此即使失败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惯用语,该惯用语应具有更清晰的语法,并带有Java SE 8中的闭包。


4
在版本3中,您如何知道bw不需要调用其close?而且即使您可以确定是否也不会像您在版本1中提到的那样“晦涩的知识”呢?
TimK

3
软件开发的原因使2成为更好的选择 ”您能否更详细地解释这一说法?
邓肯·琼斯

8
您能否举一个“用闭包执行成语”的例子
马库斯

2
您能否解释一下“ Java SE 8中带有闭包的更清晰语法”?
petertc 2015年

1
“围绕成语执行”的示例在此处:stackoverflow.com/a/342016/258772
mrts

20

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

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


2
这也是有效Java第三版中的推荐方法。
shmosel

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

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循环来做迭代器可以做的事情一样。


5
如果您的tryfinally块都抛出异常,则此构造将丢失第一个(并且可能会更有用)。
rxg

3

同意前面的意见:(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(here 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)。但是,仅在一个工厂函数中写出一个明确的三重链会更加清楚。


我是否正确地收集到仍然需要使用try-with-resource,如try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }
ErikE

@ErikE是的,您仍然需要使用try-with-resources,但是只需要对链接的资源使用一个函数:factory函数 封装了链接。我添加了一个使用示例;谢谢!
尼尔斯·冯·巴特

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
}

5
这与我的第二个示例非常相似。如果没有异常,则FileWriter close将被调用两次。
Natix 2012年

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 FileWriter,而且还进行了一些清理flushBuffer


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不会以任何一种方式关闭警告。


该警告的含义是:我们可以在这里确定bw将实际写出数据吗?它毕竟是经过缓冲的,因此它有时仅需写入磁盘(当缓冲区已满和/或处于on flush()close()method时)。我猜flush()应该调用该方法。但是,无论如何,当我们要立即批量写入缓冲写入器时,就不需要使用它。而且,如果不修改代码,最终可能会以错误的顺序将数据写入文件,甚至根本不会将数据写入文件。
PetrJaneček'13

如果flush()需要调用,则应在调用者决定关闭之前发生FileWriter。因此,这应该在try块中的printToFile任何returns 之前发生。这不应成为writeFileWriter该函数的一部分,因此警告不是关于该函数内部的任何内容,而是关于该函数的调用者。如果我们有注释@LiftWarningToCaller("wanrningXXX"),它将对这种情况有帮助。
Earth Engine
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.