代码之家  ›  专栏  ›  技术社区  ›  Ray Zhang

Java:尝试用资源恰当地关闭嵌套资源吗?[副本]

  •  0
  • Ray Zhang  · 技术社区  · 7 年前

    Java 7 尝试使用资源 语法(也称为arm块( 自动资源管理 ))当只使用一个 AutoCloseable 资源。但是,当我需要声明相互依赖的多个资源时,我不确定什么是正确的习惯用法,例如 FileWriter 和A BufferedWriter 就这样。当然,这个问题涉及任何情况 自动关闭 资源是包装的,不仅仅是这两个特定的类。

    我想出了以下三种选择:

    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
        }
    }
    

    这件衣服又短又好,但破了。因为潜在的 字符输出流 不是在变量中声明的,在生成的 finally 封锁。只有通过 close 包装方法 缓冲写入程序 . 问题是,如果从 bw 的构造函数 关闭 不会被调用,因此 字符输出流 不会关闭 .

    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 (它是 自动关闭 ,其合同声明多次调用 关闭 允许:

    关闭此流并释放与之关联的任何系统资源。如果流已关闭,则调用此方法无效。

    但是,在一般情况下,我可以拥有只实现 自动关闭 (而不是 封闭的 ,但不能保证 关闭 可以多次调用:

    注意,与java.io.closeable的close方法不同,此close方法不要求是等幂的。换句话说,多次调用此close方法可能会产生一些可见的副作用,这与closeable.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”从未关闭


    那么,应该采用哪种方法呢?或者我错过了其他的成语 正确的 一个?

    0 回复  |  直到 8 年前
        1
  •  74
  •   user1156544    7 年前

    以下是我对备选方案的看法:

    1)

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

    对我来说,15年前从传统C++来到Java的最好的事情就是你可以信任你的程序。即使事情陷入困境,并且经常出错,我也希望代码的其余部分保持最佳行为,散发出玫瑰的香味。事实上, BufferedWriter 可能会在这里引发异常。例如,内存不足并不罕见。对于其他的装修师,你知道 java.io 包装类从其构造函数中抛出检查异常?我不知道。如果你依赖那种晦涩难懂的知识,代码的可理解性就没有多大用处。

    还有“毁灭”。如果存在错误条件,则可能不希望将垃圾刷新到需要删除的文件(未显示该文件的代码)。当然,删除文件也是另一个有趣的错误处理操作。

    一般来说你想要 finally 砌块应尽可能短且可靠。添加刷新对这个目标没有帮助。对于许多版本,jdk中的一些缓冲类有一个bug,其中 flush 在内部 close 引起 关闭 在装饰对象上不被调用。虽然这个问题已经解决了一段时间,但希望从其他实现中得到解决。

    2)

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

    我们仍在隐式finally块中刷新(现在重复 关闭 -当您添加更多的decorators时,情况会变得更糟),但是构造是安全的,我们必须隐式finally块,因此即使是失败的 脸红 不会阻止资源释放。

    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成为更好的选择。然而,尝试使用资源仍然是一个不足的解决方案,您应该坚持 Execute Around idiom ,它应该在Java SE 8中具有更清晰的语法。

        2
  •  19
  •   Daniel C. Sobral    12 年前

    第一种风格是 suggested by Oracle . BufferedWriter 不抛出已检查的异常,因此如果抛出任何异常,程序将不会从中恢复,这使得资源恢复基本上没有意义。

    主要是因为它可能发生在一个线程中,线程死了,但程序仍在继续——比如说,有一个临时内存中断,时间不足以严重损害程序的其余部分。不过,这是一个非常棘手的问题,如果这种情况经常发生,导致资源泄漏成为一个问题,那么使用资源的尝试是您遇到的问题中最少的一个。

        3
  •  5
  •   m_vitaly    9 年前

    选项4

    将资源更改为可关闭,如果可以,则不可自动关闭。构造函数可以被链接这一事实意味着它并非闻所未闻地要关闭两次资源。(这在ARM之前也是正确的。)下面将详细介绍。

    选择5

    不要非常小心地使用arm和代码来确保close()不会被调用两次!

    选项6

    不要使用arm,在try/catch中使用finally close()调用。

    为什么我不认为这个问题是手臂特有的

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

    不好,因为FW可以关闭两次。(这对文件编写者来说很好,但在您的假设示例中却不行):

    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();
    }
    
        4
  •  3
  •   AmyGamy    13 年前

    我只是想根据珍妮·博雅斯基的建议,不使用ARM,但要确保文件编写器总是关闭一次。别以为这里有什么问题…

    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
  •  3
  •   Nils von Barth    8 年前

    同意前面的评论:最简单的是 (2) 使用 Closeable 并在try with resources子句中按顺序声明它们。如果你只有 AutoCloseable ,您可以将它们包装在另一个(嵌套的)类中,该类只检查 close 只调用一次(门面模式),例如 private bool isClosed; . 实际上,即使是甲骨文 (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 , T S 反过来,如果你第一次锁链 S T 然后 U ,对应于 (ST)U ,或者如果你第一次锁链 T U 然后 S ,对应于 S(TU) . 不过,在单个工厂函数中写出一个显式的三重链会更清楚。

        6
  •  2
  •   poison    13 年前

    由于资源是嵌套的,因此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
    }
    
        7
  •  0
  •   sakthisundar    13 年前

    我会说不要用手臂,继续靠近。使用类似的方法,

    public void close(Closeable... closeables) {
        for (Closeable closeable: closeables) {
           try {
               closeable.close();
             } catch (IOException e) {
               // you can't much for this
              }
        }
    
    }
    

    你也应该考虑打电话给 BufferedWriter 因为这不仅仅是授权 FileWriter ,但它会做一些清理工作,比如 flushBuffer .

        8
  •  0
  •   Earth Engine    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 也可以写

    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 .

    再次更新

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

    为了解决这个问题,我们需要一个 自动关闭 每当它关闭时,下划线 缓冲写入程序 应该是 刷新() 实际上,这向我们展示了另一种绕过警告的方法,因为 BufferWriter 从来没有以任何方式关闭过。