【问题标题】:RAII in Java... is resource disposal always so ugly?Java中的RAII ...资源处理总是那么难看吗?
【发布时间】: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 语句中。这表明这种做事方式有多不方便。

标签: java design-patterns raii resource-management


【解决方案1】:

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

有些人提倡默默关闭流。出于以下原因,请小心执行此操作: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 等。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-10
    • 1970-01-01
    • 2011-01-29
    • 1970-01-01
    • 2011-02-22
    • 1970-01-01
    相关资源
    最近更新 更多