17

我刚刚玩了 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,问题仍然存在。

4

5 回答 5

19

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

有些人提倡默默地关闭流。出于以下原因,请小心执行此操作:Java:如何不弄乱流处理


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()) {}
  }
}
于 2008-10-11T16:33:45.340 回答
4

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

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

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

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

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

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

于 2008-10-11T16:29:02.997 回答
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,那么您可以使用“execute around”习语将其分解。

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

于 2008-10-11T16:36:19.940 回答
3

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

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

于 2008-10-11T16:50:09.363 回答
2

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

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

于 2008-10-11T17:14:52.090 回答