Java中的RAII ...资源处理总是那么难看吗?

Posted

技术标签:

【中文标题】Java中的RAII ...资源处理总是那么难看吗?【英文标题】:RAII in Java... is resource disposal always so ugly? 【发布时间】:2010-09-16 16:38:59 【问题描述】:

我刚刚玩了Java文件系统API,得到了以下函数,用于复制二进制文件。原始来源来自网络,但我添加了 try/catch/finally 子句以确保如果发生错误,缓冲流将在退出函数之前关闭(从而释放我的操作系统资源)。

我把函数删减了显示模式:

public static void copyFile(FileOutputStream oDStream, FileInputStream oSStream) throw etc...

   BufferedInputStream oSBuffer = new BufferedInputStream(oSStream, 4096);
   BufferedOutputStream oDBuffer = new BufferedOutputStream(oDStream, 4096);

   try
    
      try
       
         int c;

         while((c = oSBuffer.read()) != -1)  // could throw a IOException
         
            oDBuffer.write(c);  // could throw a IOException
         
      
      finally
      
         oDBuffer.close(); // could throw a IOException
      
   
   finally
   
      oSBuffer.close(); // could throw a IOException
   

据我了解,我不能将两个close() 放在finally 子句中,因为第一个close() 可以抛出,然后第二个不会被执行。

我知道 C# 有 Dispose 模式,可以使用 using 关键字来处理这个问题。

我什至更清楚 C++ 代码应该类似于(使用类似 Java 的 API):

void copyFile(FileOutputStream & oDStream, FileInputStream & oSStream)

   BufferedInputStream oSBuffer(oSStream, 4096);
   BufferedOutputStream oDBuffer(oDStream, 4096);

   int c;

   while((c = oSBuffer.read()) != -1)  // could throw a IOException
   
      oDBuffer.write(c);  // could throw a IOException
   

   // I don't care about resources, as RAII handle them for me

我遗漏了一些东西,或者我真的必须在 Java 中生成丑陋和臃肿的代码来处理缓冲流的 close() 方法中的异常吗?

(请告诉我我在某处错了......)

编辑:是我,还是在更新此页面时,我看到问题和所有答案都在几分钟内减少了一个点?是否有人在保持匿名时过分享受自己?

编辑 2:McDowell 提供了一个非常有趣的链接,我觉得我不得不在这里提及: http://illegalargumentexception.blogspot.com/2008/10/java-how-not-to-make-mess-of-stream.html

编辑 3:在 McDowell 的链接之后,我偶然发现了 Java 7 的提议,该提议类似于 C# 使用模式:http://tech.puredanger.com/java7/#resourceblock。我的问题已明确描述。显然,即使使用 Java 7 do,问题仍然存在。

【问题讨论】:

为我的错误回答道歉。我不确定您是否真的想找出一种在 Java 中执行 RAII 的方法,或者只是不知道在 Java 中通常如何将数据从输入流复制到输出流中。 没问题...事实是我也不知道如何制作干净高效的副本... :-p ... 如果您将使用 java 代码约定,您可以在第一个清单中保存 8 行代码,在第二个清单中保存 2 行代码。在那种情况下,这段代码不会那么难看。 @msangel : You can save 8 lines of code in first listing and 2 lines in second listing if you will use the java code convention. In that case this code will be not so ugly. : 你SO错过了这篇文章的重点...... :-D oDBuffer 的分配确实应该放在外部 try 语句中。这表明这种做事方式有多不方便。 【参考方案1】:

对于 Java 6 及更低版本,在大多数情况下,try/finally 模式是处理流的正确方法。

有些人提倡默默关闭流。出于以下原因,请小心执行此操作:Java: how not to make a mess of stream handling


Java 7 引入了try-with-resources

/** transcodes text file from one encoding to another */
public static void transcode(File source, Charset srcEncoding,
                             File target, Charset tgtEncoding)
                                                             throws IOException 
    try (InputStream in = new FileInputStream(source);
         Reader reader = new InputStreamReader(in, srcEncoding);
         OutputStream out = new FileOutputStream(target);
         Writer writer = new OutputStreamWriter(out, tgtEncoding)) 
        char[] buffer = new char[1024];
        int r;
        while ((r = reader.read(buffer)) != -1) 
            writer.write(buffer, 0, r);
        
    

AutoCloseable 类型会自动关闭:

public class Foo 
  public static void main(String[] args) 
    class CloseTest implements AutoCloseable 
      public void close() 
        System.out.println("Close");
      
    
    try (CloseTest closeable = new CloseTest()) 
  

【讨论】:

在大多数情况下,但有趣的是,在这种情况下并非如此。 :) @Tom - 是的,这不是一个好的流复制机制 + 我会选择你的。 我的观点更多的是关于 RAII,而不是关于使用 BufferOutputStream 的代码的确切实现。您的链接是我的 RAII 问题的正确答案。作为一个有趣的旁注,我有机会在 Java 上处理一个新项目,但拒绝了邀请,而是(几乎)因为 C# 的 using 和 C++/CLI 析构函数和终结器而选择了另一个 .NET 项目。 .. @McDowell :感谢更新 try-with-resources 代码示例。如果我可以不止一次这样做,我会再次支持您的答案... :-) ...【参考方案2】:

有问题,但是你在网上发现的代码真的很糟糕。

关闭缓冲流会关闭下面的流。你真的不想那样做。您要做的就是刷新输出流。此外,指定底层流用于文件也没有意义。性能很糟糕,因为您一次复制一个字节(实际上,如果您使用 java.io,则可以使用 transferTo/transferFrom ,这会更快一些)。当我们谈到它时,变量名很糟糕。所以:

public static void copy(
    InputStream in, OutputStream out
) throw IOException 
    byte[] buff = new byte[8192];
    for (;;) 
        int len = in.read(buff);
        if (len == -1) 
            break;
        
        out.write(buff, 0, len);
    

如果您发现自己经常使用 try-finally,那么您可以使用“execute around”习语将其分解。

在我看来:Java 应该以某种方式在范围结束时关闭资源。我建议将private 添加为一元后缀运算符,以在封闭块的末尾关闭。

【讨论】:

感谢更好的代码。对于我目前的个人项目,这不是很重要,但我现在复制/粘贴您的代码作为未来的替代品。 +1。 如果他正在复制文件,那么他可能确实想要在完成后关闭流。副本已完成,因此没有必要让流保持打开状态。在这种情况下,他的嵌套 try-finally 块 + close() 调用是合适的。 德里克公园是对的。虽然您的代码让我感兴趣,但它仍然错过了问题的重点,即资源处理。假设我有一个 copyFile(String in, String out) 方法实例化 FileOutputStream 和 FileInputStream,并调用此 copy(InputStream in, OutputStream out) 方法,应该如何编写 copyFile 以正确处理资源处理?【参考方案3】:

不幸的是,这种类型的代码在 Java 中会变得有点臃肿。

顺便说一句,如果对 oSBuffer.read 或 oDBuffer.write 的调用之一引发异常,那么您可能希望让该异常渗透到调用层次结构中。

在 finally 子句中对 close() 进行不受保护的调用将导致原始异常被 close() 调用产生的异常替换。换句话说,失败的 close() 方法可能会隐藏由 read() 或 write() 产生的原始异常。所以,我认为你想忽略 close() 抛出的异常,如果 并且仅当 其他方法没有抛出。

我通常通过在内部尝试中包含显式关闭调用来解决这个问题:

尝试 尽管 (...) 读... 写... oSBuffer.close(); // 这里没有忽略异常 oDBuffer.close(); // 这里没有忽略异常 最后 静默关闭(oSBuffer); // 此处忽略异常 沉默关闭(oDBuffer); // 此处忽略异常 静态无效静默关闭(可关闭 c) 尝试 c.close(); 捕捉(IOException 即) // 忽略;来电者必须有这个意图

最后,为了性能,代码应该可以使用缓冲区(每次读/写多个字节)。不能用数字来支持这一点,但更少的调用应该比在顶部添加缓冲流更有效。

【讨论】:

如果您以这种方式静默关闭,如果关闭抛出异常,您的代码不会进行错误处理。许多流(如 BufferedOutputStream)在关闭时写入数据。 McDowell:是的,如果 close() 抛出异常,它应该捕获异常。请注意,close() 调用首先在 try 块内部进行! finally 块是为了确保在任何方法抛出异常时进行清理。正确的? (注意,我忘记了第一个发布版本中一个缓冲区的关闭。) BufferedOutputStream 有点儿牛逼。我赞成显式刷新(在非例外情​​况下),但你必须记住它。 IIRC,关闭在 Java SE 1.6 之前打破了异常处理。 我知道 close() 的异常是至关重要的。但是,如果 close() 调用是在最后一次调用 write() 之后进行的,那么这不应该确保正确跟踪异常吗? McDowell,请确认这里是否有漏洞;然后我会自己撤销代码,但我真的很想知道。 :) 一些加密/压缩有点讨厌。您不仅有底层资源要处理,而且实现可能还有一些“C”、非 Java 内存资源。【参考方案4】:

是的,这就是 java 的工作原理。存在控制反转——对象的用户必须知道如何清理对象,而不是对象本身自己清理。不幸的是,这会导致大量清理代码分散在您的 java 代码中。

C# 具有“using”关键字,可在对象超出范围时自动调用 Dispose。 Java 没有这样的东西。

【讨论】:

无论是否有特殊语法,客户端代码都必须告诉资源何时清理。资源无法分辨。当然,您可以抽象出资源获取、设置和释放,将有趣的代码作为回调执行。 这不是真的。 C++ 和 C# 对象可以很好地处理自己,而无需调用者的任何参与。阅读这些语言。 我确实对这些语言有些熟悉。在 C# 中,客户端代码在 using 块的末尾调用 dispose 方法。在 C++ 中,客户端代码在作用域结束时调用析构函数。 没错。 C++ 和 C# 调用者不必参与销毁,调用者也不必知道被销毁对象的内部工作原理。只有 Java 有这种控制反转,调用者不仅必须知道如何破坏对象,还必须知道它的副作用。 在 C++ 和 C# 中,调用者通过适当的语法只需要说“我想要自动销毁”(C# 通过“使用”和 C++ 通过基于堆栈的初始化)并且对象本身负责细节.但是在 Java 中,调用者必须自己执行销毁(调用 close() 或其他)。【参考方案5】:

对于复制文件等常见的 IO 任务,如上所示的代码正在重新发明***。不幸的是,JDK 不提供任何更高级别的实用程序,但 apache commons-io 提供。

例如,FileUtils 包含用于处理文件和目录(包括复制)的各种实用方法。另一方面,如果你真的需要使用 JDK 中的 IO 支持,IOUtils 包含一组 closeQuietly() 方法,可以在不抛出异常的情况下关闭 Readers、Writers、Streams 等。

【讨论】:

以上是关于Java中的RAII ...资源处理总是那么难看吗?的主要内容,如果未能解决你的问题,请参考以下文章

RAII

PHP 是不是支持 RAII 模式?如何?

C++RAII机制(智能指针原理)

C++RAII机制(智能指针原理)

C++11的资源管理:泛化的RAII

何时不使用 RAII 进行资源管理 [关闭]