Java中关闭嵌套流和写入器的正确方法

98

注意: 本问题及大多数答案均为Java 7发布之前的内容。Java 7提供了自动资源管理功能,可以轻松完成此操作。如果您使用的是Java 7或更高版本,应转到Ross Johnson的答案


什么被认为是在Java中关闭嵌套流的最佳、最全面的方法?例如,考虑以下设置:

FileOutputStream fos = new FileOutputStream(...)
BufferedOS bos = new BufferedOS(fos);
ObjectOutputStream oos = new ObjectOutputStream(bos);

我知道关闭操作需要确保(可能通过使用finally子句)。我想知道的是,是否需要明确确保嵌套流被关闭,还是只需确保关闭外部流(oos)就足够了?

我注意到一件事情,至少处理这个特定的例子时,内部流似乎只会抛出FileNotFoundException。这似乎意味着如果它们失败,实际上不需要担心关闭它们。

以下是一个同事写的:


从技术上讲,如果正确实现,仅关闭最外层流(oos)应该足够。但是,实现似乎存在缺陷。

例如:BufferedOutputStream从FilterOutputStream继承close()方法,将其定义为:

 155       public void close() throws IOException {
 156           try {
 157             flush();
 158           } catch (IOException ignored) {
 159           }
 160           out.close();
 161       }

然而,如果flush()由于某种原因抛出运行时异常,则out.close()将永远不会被调用。因此,最“安全”(但难看)的方法是主要关注关闭FOS,它保持文件处于打开状态。


在嵌套流中关闭流,最可靠、确保万无一失的方法是什么?

是否有任何官方的Java/Sun文档详细介绍这个问题?


1
@BalusC,为什么一个在2009年提出的问题被标记为一个在2015年提出的问题的重复? - Farid
10个回答

42

关闭链接的数据流时,只需要关闭最外层的流即可。任何错误将会通过链传递并被捕获。

有关详细信息,请参阅Java I/O Streams

为解决这个问题

然而,如果 flush() 由于某种原因抛出运行时异常,则 out.close() 将永远不会被调用。

这是不正确的。在你捕获并忽略该异常之后,执行将从 catch 块之后恢复,并且 out.close() 语句将被执行。

你的同事关于 RuntimeException 提出了一个好观点。如果你绝对需要关闭流,你可以尝试逐个关闭每个流,从外向内进行,在第一个异常处停止。


1
如果我理解正确的话,他的同事主张关闭所有东西以处理在catch块中无法捕获的RuntimeException。你同意还是不同意? - McDowell
1
他的担忧是out.close()不会被调用,这是错误的。无论是否忽略异常,它都将被调用。 - Bill the Lizard
如果 flush 抛出运行时异常,close 将不会被调用。 - erickson
1
啊,你们说得对。任何高于IOException的异常都不会被捕获。 - Bill the Lizard
2
我不会停下来,我会继续关闭并记录异常。在这种情况下,使用BufferedStream是很关键的。如果在关闭期间BufferedStream崩溃,您(可能)不希望FileOutputStream仍然处于“未关闭”状态。 - Will Hartung
显示剩余8条评论

34
在Java 7时代,try-with-resources是最佳实践。正如在之前的几个答案中提到的那样,关闭请求会从最外层的流传递到最内层的流。因此,只需要一个close就足够了。

在Java 7时代,try-with-resources是最佳实践。正如在之前的几个答案中提到的那样,关闭请求会从最外层的流传递到最内层的流。因此,只需要一个close就足够了。

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f))) {
  // do something with ois
}

然而,这种模式存在一个问题。try-with-resources不知道内部的FileInputStream,因此如果ObjectInputStream构造函数抛出异常,则FileInputStream永远不会关闭(直到垃圾回收器到达它)。解决方案是...

try (FileInputStream fis = new FileInputStream(f); ObjectInputStream ois = new ObjectInputStream(fis)) {
  // do something with ois
}

这种方法可能不够优雅,但更加健壮。是否真的是问题将取决于在构造外部对象时可能抛出哪些异常。ObjectInputStream 可以抛出 IOException ,这可能会被应用程序处理而不终止。许多流类只抛出未经检查的异常,这可能会导致应用程序终止。


更多阅读:https://dev59.com/-Wcs5IYBdhLWcg3w0HOz - Ryan

22

使用Apache Commons处理IO相关对象是一个良好的习惯。

finally语句块中使用IOUtils

IOUtils.closeQuietly(bWriter); IOUtils.closeQuietly(oWritter);

以下是代码片段。

BufferedWriter bWriter = null;
OutputStreamWriter oWritter = null;

try {
  oWritter  = new OutputStreamWriter( httpConnection.getOutputStream(), "utf-8" );
  bWriter = new BufferedWriter( oWritter );
  bWriter.write( xml );
}
finally {
  IOUtils.closeQuietly(bWriter);
  IOUtils.closeQuietly(oWritter);
}

我点了一个赞,因为我终于发现了一个非常好的使用Apache Commons的方法。而上面的模板解决方案涉及到对我来说太技术性的内容。 - loloof64
哦,还有,非常好的微小示例 :) - loloof64

19

我通常采用以下方法。首先,定义一个基于模板方法的类来处理try/catch混乱的情况。

import java.io.Closeable;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

public abstract class AutoFileCloser {
    // the core action code that the implementer wants to run
    protected abstract void doWork() throws Throwable;

    // track a list of closeable thingies to close when finished
    private List<Closeable> closeables_ = new LinkedList<Closeable>();

    // give the implementer a way to track things to close
    // assumes this is called in order for nested closeables,
    // inner-most to outer-most
    protected final <T extends Closeable> T autoClose(T closeable) {
            closeables_.add(0, closeable);
            return closeable;
    }

    public AutoFileCloser() {
        // a variable to track a "meaningful" exception, in case
        // a close() throws an exception
        Throwable pending = null;

        try {
            doWork(); // do the real work

        } catch (Throwable throwable) {
            pending = throwable;

        } finally {
            // close the watched streams
            for (Closeable closeable : closeables_) {
                if (closeable != null) {
                    try {
                        closeable.close();
                    } catch (Throwable throwable) {
                        if (pending == null) {
                            pending = throwable;
                        }
                    }
                }
            }

            // if we had a pending exception, rethrow it
            // this is necessary b/c the close can throw an
            // exception, which would remove the pending
            // status of any exception thrown in the try block
            if (pending != null) {
                if (pending instanceof RuntimeException) {
                    throw (RuntimeException) pending;
                } else {
                    throw new RuntimeException(pending);
                }
            }
        }
    }
}

请注意“pending”异常--这个异常处理了在关闭期间抛出的异常会掩盖我们真正关心的异常的情况。
finally首先尝试从任何装饰流的外部关闭,因此如果您有一个包装FileWriter的BufferedWriter,我们首先尝试关闭BufferedWriter,如果失败,则仍然尝试关闭FileWriter本身。(请注意,Closeable的定义要求close()在流已经关闭时忽略调用)
您可以按以下方式使用上述类:
try {
    // ...

    new AutoFileCloser() {
        @Override protected void doWork() throws Throwable {
            // declare variables for the readers and "watch" them
            FileReader fileReader = 
                    autoClose(fileReader = new FileReader("somefile"));
            BufferedReader bufferedReader = 
                    autoClose(bufferedReader = new BufferedReader(fileReader));

            // ... do something with bufferedReader

            // if you need more than one reader or writer
            FileWriter fileWriter = 
                    autoClose(fileWriter = new FileWriter("someOtherFile"));
            BufferedWriter bufferedWriter = 
                    autoClose(bufferedWriter = new BufferedWriter(fileWriter));

            // ... do something with bufferedWriter
        }
    };

    // .. other logic, maybe more AutoFileClosers

} catch (RuntimeException e) {
    // report or log the exception
}

使用这种方法,您永远不必担心try/catch/finally来处理关闭文件的问题。
如果这对您来说太重了,至少考虑遵循它使用的try/catch和“pending”变量方法。

4
你是否可以这样写,例如 FileReader fileReader = autoClose(new FileReader("somefile"));,并且达到相同的效果? - Duncan Jones
1
避免使用FileReader/Writer - 因为没有字符编码。 - Mr_and_Mrs_D
@Mr_and_Mrs_D:离题了;这里的问答是关于关闭嵌套流的。FileReader/Writer确实会进行编码——它使用当前平台的默认字符编码,通常情况下可以正常工作。如果您需要指定编码(用于将文件传输到其他系统等),则可以使用带有编码选项的InputStreamReader/Writer。 - Scott Stanchfield
是的,我知道这有点离题,但既然这是一个高度关注的问题中被接受的答案,我只想指出来 - 默认编码仍然不是一个好主意 :) - Mr_and_Mrs_D
1
我真的不喜欢这个。这真的很丑陋。你刚刚失去了所有已检查的异常。使用AutoCloser的方法的调用者现在不知道他们应该合理地处理和恢复什么。此外,您还将错误(Error)转换为RuntimeException。 - steinybot
@Steiny 许多人(包括我)认为检查异常是邪恶的...想象一下,当你将文件IO添加到已经从其他方法调用的方法中时会发生什么;如果您想捕获已检查的异常(而且99%的时间,您只关心“发生了一些糟糕的事情”并且无法进行任何有意义的恢复),则需要修改所有调用方法的签名。在Java 7之前使其通用的唯一方法是将所有异常都放在一起。 - Scott Stanchfield

5
这是一个令人意外的棘手问题。(即使假设acquire; try { use; } finally { release; }代码是正确的。)
如果装饰器的构建失败,那么您将不会关闭底层流。因此,您需要显式地关闭底层流,无论是在使用后的finally中还是更加困难的情况下,在成功将资源交给装饰器后)。
如果异常导致执行失败,您真的想要刷新吗?
一些装饰器实际上也有自己的资源。例如,当前的Sun实现ZipInputStream分配了非Java堆内存。
曾经有人声称(如果我没记错的话),Java库中三分之二的资源都以明显错误的方式实现。
虽然BufferedOutputStream甚至在从flush中抛出IOException时也会关闭,但BufferedWriter则正确地关闭。
我的建议:尽可能直接地关闭资源,并且不要让它们污染其他代码。另一方面,您可能会花太多时间处理这个问题——如果抛出OutOfMemoryError,表现良好很好,但是您程序的其他方面可能是更高优先级的,而且库代码在这种情况下可能已经损坏了。但我总是会写:
final FileOutputStream rawOut = new FileOutputStream(file);
try {
    OutputStream out = new BufferedOutputStream(rawOut);
    ... write stuff out ...
    out.flush();
} finally {
    rawOut.close();
}

(看这里:没有陷阱!)
也许可以使用“执行环绕”惯用语。

你如何避免try?你只是抛出IOException吗?但是如果发生异常,它就不会被关闭了? - Casebash
如果在FileOutputStream中发生异常,则不会分配rawOut,因此我们无法做任何事情,但是如果BuffereOutputStream构造函数失败,则我们将无法像尝试首先关闭它一样关闭rawOut。此外,由于out将为null,因此最终将崩溃。 - Casebash
在 finally 中应该使用 rawOut 而不是 out。(再加上一些其他的笔误。) - Tom Hawtin - tackline
好的,那么你关闭了 rawOut。你也关闭 out 吗?还是无所谓? - Casebash
没有理由关闭 out。那样做就像将其设置为 null 一样毫无意义。 - Tom Hawtin - tackline

5
同事提出了一个有趣的观点,可以从两个方面进行辩论。
个人而言,我会忽略 RuntimeException,因为未经检查的异常表示程序中的错误。如果程序有问题,那就修复它。不能在运行时“处理”一个有问题的程序。

3
我不会认为RuntimeException意味着这样的错误(Error意味着那样的错误)。有些开发者认为未经检查的异常适用于所有情况,而其他人则强烈反对。 - StaxMan
3
由于我在进行线路连接时,我知道使用了哪些装饰器。在特殊情况下,如果被迫使用不正确使用RuntimeException的流,我必须加以考虑。尽管到目前为止还没有遇到这种情况。编写大量不必要复杂的代码来防范一些不太可能发生且可以避免的事情,似乎是一种不好的方法。 - erickson
1
RuntimeExceptions 在以下情况下非常有用:当我们无法处理一个异常,除非在几个层次之上,或者我们需要通过一个接口进行通信时。 - Casebash
“坏程序”并非二元条件。由于无法解析某个用户请求的一些输入而导致的运行时错误与因缺乏文件句柄而导致完全服务失败的错误不属于同一类别。 - Barry Kelly
@BarryKelly 对的,通常你会想要捕获和处理用户输入错误的异常。在这种情况下使用一个已检查的异常是合适的。 - erickson

1

Java SE 7的try-with-resources好像没有被提到。它可以消除需要显式完全关闭,我很喜欢这个想法。

不幸的是,在Android开发中,只有使用Android Studio(我认为)并针对Kitkat及以上版本才能获得此功能。


0
我用以下方式关闭流,而不是将try-catch嵌套在finally块中

public class StreamTest {

public static void main(String[] args) {

    FileOutputStream fos = null;
    BufferedOutputStream bos = null;
    ObjectOutputStream oos = null;

    try {
        fos = new FileOutputStream(new File("..."));
        bos = new BufferedOutputStream(fos);
        oos = new ObjectOutputStream(bos);
    }
    catch (Exception e) {
    }
    finally {
        Stream.close(oos,bos,fos);
    }
  }   
}

class Stream {

public static void close(AutoCloseable... array) {
    for (AutoCloseable c : array) {
        try {c.close();}
        catch (IOException e) {}
        catch (Exception e) {}
    }
  } 
}

2
你在catch块中忽略了一个“异常”! - Jan Bodnar

0

链接已经失效了 :( 这就是为什么只提供链接答案是不被鼓励的原因。 - chbrown

-1

在Sun的JavaDocs文档中,像InputStream的read(byte[], int, int)方法中包含了RuntimeException的说明,被记录为会抛出NullPointerException和IndexOutOfBoundsException异常。

而FilterOutputStream的flush()只被记录为会抛出IOException异常,因此实际上并不会抛出任何RuntimeException。如果有可能抛出这些异常,很可能会被包装在IIOException中。

它仍然可能会抛出Error,但你无法对其进行处理;Sun建议你不要尝试捕获它们。


3
我不指望RuntimeException有文档说明。根据我的经验,只有某些常见的RuntimeException(比如NumberFormatException)才会被记录在文档中。 - Mr. Shiny and New 安宇

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接