尝试使用资源文件编写器是一种好习惯吗

Posted

技术标签:

【中文标题】尝试使用资源文件编写器是一种好习惯吗【英文标题】:Is it good practice to try-with-resource a file writer 【发布时间】:2020-08-18 08:05:08 【问题描述】:

我在网上和“Effective Java”一书(作者 Joshua Bloch)中看到了这个示例。

try(BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)))
  writer.write(str); // do something with the file we've opened

catch(IOException e)
  // handle the exception

这个例子没有问题,BufferedWriter 会自动关闭,然后又会关闭FileWriter;但是,在其他情况下,如果我们这样声明 2 个嵌套资源:

try (AutoClosable res = new Impl2(new Impl1())) ... 

我猜new Impl1() 可能运行良好,但new Impl2() 崩溃,在这种情况下,Java 将不会引用Impl1,以便关闭它。

像这样总是独立声明多个资源(即使在这种情况下不需要)不是更好的做法吗?

try(FileWriter fw = new FileWriter(fileName); 
    BufferedWriter writer = new BufferedWriter(fw)) ... 

【问题讨论】:

我会说这在很大程度上取决于外部构造函数的记录失败。如果他们说他们可能会失败,那么当然,在单独的资源中进行。我不认为BufferedWriter 的具体示例会失败,除非传递的作者为空。 我认为理所当然地这样做有可能导致其他奇怪的失败,因为如果外部资源的 close() 方法也有,你最终会多次关闭内部资源关闭内部资源。同样,这取决于特定的资源,因为一些(少数?很多?大多数?)将处理关闭幂等。 还有一些情况下,外部 AutoClosable 会在其 close 方法中关闭内部 AutoClosable。 我不认为多次 close() 可能会失败,因为 1. 每个关闭都是在 try-catch 内完成的 2. 不会​​从隐式 finally 引发异常。但是,当您有这样的嵌套资源声明时,在这种情况下,我们可能会声明一个资源,但在其他情况下,我们必须声明多个资源。我觉得我们必须检查每个构造函数是否会失败,这很无聊。 @AndyTurner 幂等关闭is mandated:“如果流已经关闭,则调用此方法无效。 【参考方案1】:

经过一番搜索,我找到了这篇文章:https://dzone.com/articles/carefully-specify-multiple-resources-in-single-try

根据定义JLS 14.20.3 和ResourceListResources 组成,由; 分隔。基于此,我们可以得出结论,像 AutoClosable res = new Impl2(new Impl1()) 这样的嵌套初始化是单个资源。由于为具有多个资源的try-with-resources 定义的规则不适用于此处,重要的是:

资源按从左到右的顺序初始化。如果资源初始化失败(即,其初始化表达式抛出异常),那么到目前为止由 try-with-resources 语句初始化的所有资源都将关闭。如果所有资源初始化成功,try 块将正常执行,然后 try-with-resources 语句的所有非空资源都将关闭。

资源以与初始化相反的顺序关闭。仅当资源初始化为非空值时才关闭资源。关闭一个资源的异常不会阻止关闭其他资源。如果先前由初始化程序、try 块或资源关闭引发了异常,则此类异常将被抑制。

更重要的是,Implt1#close() 不会被调用,除非它在 ​​Impl2#close() 内部被显式调用

简而言之,最好在用;分隔的单独语句中声明多个资源,如下所示:

try(Impl1 impl1 = new Impl1(); 
    Impl2 impl2 = new Impl2(impl1))

【讨论】:

关于最后一句话可以使用:try(Impl1 impl1 = new Impl1(); Impl2 impl2 = new Impl2(impl1)) @JimmyJames 是的,这正是我的意思 - 为了清楚起见,添加了示例。【参考方案2】:

如果try部分发生异常,则不存在资源泄漏(假设Impl1写得好)。请注意,Impl1() 中引发的异常不会到达 Impl2,因为在调用构造函数参数之前对其进行了评估。

try (AutoClosable res = new Impl2(new Impl1())) 

所以可以嵌套这样的包装构造函数;如果代码不会太长,样式会更好。

备注:FileWriterFileReader 是使用平台编码的旧实用程序类,每个应用程序安装会有所不同。

Path path = Paths.get(fileName);
try (BufferedWriter writer =
        Files.newBufferedWriter​(path, StandardCharsets.UTF8)) 

【讨论】:

如果Impl2构造函数内部引发异常怎么办? Impl1 已经创建但从未关闭,对吧? @Amongalen 你基本上是对的;因此,我很感谢您的回答,但是这样的包装类通常应该处理这些情况;例如,内部 try-finally。刚刚说过new BufferedWriter(w, -1)(第二个构造函数)将违反我的主张。但是显示的代码很好。 顺便说一句,关于构造函数中的异常有很多建议。 Buffered'Writer 也可以写成安全的。 我的问题不是针对 FileWriter 和 BufferedWriter。我认为,编写这样的资源声明,我们不应该检查构造函数做了什么,如果有隐藏的未经检查的异常......我个人更喜欢总是声明 2 个不同的资源,而不必进行检查;我想知道它是否有任何问题以及这是否应该是一个好习惯。【参考方案3】:

首先,为了进行一点完整性检查,我想说,我在 Joshua Bloch 的 Effective Java(2018 年第 3 版)中找不到您提供的示例。如果您正在阅读以前的版本,最好获得最新版本。如果是我的不好,请把具体的页码给我。


现在关于问题本身。

让我们从JSL §14.20.3 开始,它表示资源由; 分隔。这意味着,不管我们的对象创建的装饰链有多长(例如new Obj1(new Obj2(...new ObjK(..)));,它将被视为一个一个单一资源,因为它是一个单一的定义/语句。

我们现在知道单一资源是什么构成的,让我从我的观察中得到一些启发。

原因,为什么最好单独定义资源

    JSL §14.20.3 还指出:

如果资源初始化失败(即它的初始化表达式抛出异常),那么到目前为止由 try-with-resources 语句初始化的所有资源都将关闭。如果所有资源初始化成功,try 块将正常执行,然后 try-with-resources 语句的所有非空资源都将关闭。

:这对我们意味着什么?

A:如果单个资源是传递给包装构造函数的对象链,它确实意味着从初始化阶段将关闭那些嵌入的资源,而不是在 parent(包装器,封闭)对象上调用 .close() 方法,因此,您可能很容易导致资源泄漏。

另一方面,您可以放心,所有资源都将关闭,如果您已定义它们 separately。

    JSL §14.20.3 还指出:

关闭一个资源的异常不会阻止关闭其他资源。如果之前由初始化程序、try 块或资源关闭引发了异常,则此类异常将被抑制。

:这对我们意味着什么?

A:如果你已经单独声明了你的资源,那么无论哪个抛出异常,哪个不抛出都没有关系。它们都会成功关闭。

    很遗憾,您提到的书(至少是3rd version 它)没有涵盖您在这里提出的问题;但是,我做了更多研究,发现 Cay S. Horstmann 的 Core Java(第 10 版)在其第 7.2.5 节中证实了我上面提到的观点:

当块正常退出,或者出现异常时,in.close() 方法被调用,就像你使用了 finally 块一样。

无论区块如何退出,inout 都会关闭。

inout 在书中给出的示例中是 Autoclosable 对象。

:这是什么意思?

A:这意味着,从一个资源的任何阶段抛出的异常,无论如何都不会影响另一个资源的关闭方式。


基于以上所有,我认为这还取决于资源的实现方式。例如。如果封闭资源在调用其.close() 时实现了关闭封闭资源的逻辑,那么您需要担心:

我想可能会发生 new Impl1() 执行良好,但 new Impl2() 崩溃,在这种情况下,Java 不会引用 Impl1,以便关闭它。

在您的特定情况下不会真正成为问题,因为正在关闭的容器资源将持有对所包含资源的引用并最终将其关闭(或只是实现其关闭 );

但是,由于我提出的几个原因,在装饰中将资源链接到包装构造函数中构造资源仍然是一个坏主意。


P。 S.

无论如何,不​​管资源是如何实现的你如何链接它们等等。一切,包括JLS, Core Java book、OCP Java SE 8 book 以及这里带来的要点表明,最好单独声明你的资源。


【讨论】:

BufferedWriter#close() 在构造函数中传递的Writer 上调用close(),在这种情况下,两者都将被关闭(除非发生故障)。

以上是关于尝试使用资源文件编写器是一种好习惯吗的主要内容,如果未能解决你的问题,请参考以下文章

编写返回 void 的方法是一种好习惯吗?

将工厂与 ng 模块绑定是一种好习惯吗?

在整个应用程序中使用此 NSManagedObjectContext 是一种好习惯吗

始终连接 SQL 表是一种好习惯吗?

通过 :: 调用包中的函数是一种好习惯吗

将所有环境(dev uat prod)的 jar 和配置打包在一个 zip 中是一种好习惯吗?