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

时间:2008-10-11 16:16:25

标签: java design-patterns raii resource-management

我刚刚使用Java文件系统API,并提供了以下功能,用于复制二进制文件。最初的源代码来自Web,但我添加了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提出了类似于使用模式的模式http://tech.puredanger.com/java7/#resourceblock的模式。我明确地描述了我的问题。显然,即使使用Java 7 do,问题仍然存在。

5 个答案:

答案 0 :(得分:18)

在大多数情况下,对于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()) {}
  }
}

答案 1 :(得分:4)

存在一些问题,但您在网络上发现的代码非常糟糕。

关闭缓冲区流会关闭下面的流。你真的不想那样做。您要做的就是刷新输出流。另外,指定底层流是用于文件也没有意义。性能很糟糕,因为你一次复制一个字节(实际上,如果你使用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,那么你可以用“执行”这个习惯来解决它。

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

答案 2 :(得分:3)

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

顺便说一下,如果对oSBuffer.read或oDBuffer.write的一个调用抛出异常,那么你可能想让该异常渗透到调用层次结构中。

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

我通常通过在内部try:

中包含一个显式的close-call来解决这个问题
  try {
    while (...) {
      read...
      write...
    }
    oSBuffer.close(); // exception NOT ignored here
    oDBuffer.close(); // exception NOT ignored here
  } finally {
    silentClose(oSBuffer); // exception ignored here
    silentClose(oDBuffer); // exception ignored here
  }
  static void silentClose(Closeable c)  {
    try {
      c.close();
    } catch (IOException ie) {
      // Ignored; caller must have this intention
    }
  }

最后,为了提高性能,代码应该可以使用缓冲区(每次读/写多个字节)。无法通过数字来支持,但更少的呼叫应该比在顶部添加缓冲流更有效。

答案 3 :(得分:3)

是的,这就是java的工作方式。存在控制反转 - 对象的用户必须知道如何清理对象而不是对象本身自我清理。遗憾的是,这会导致许多清理代码散布在您的Java代码中。

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

答案 4 :(得分:2)

对于常见的IO任务,例如复制文件,上面显示的代码正在重新发明轮子。不幸的是,JDK没有提供任何更高级别的实用程序,但apache commons-io确实如此。

例如,FileUtils包含用于处理文件和目录(包括复制)的各种实用程序方法。另一方面,如果您确实需要在JDK中使用IO支持,IOUtils包含一组closeQuietly()方法,这些方法关闭读取器,写入器,流等,而不会抛出异常。