是否需要分别关闭每个嵌套的OutputStream和Writer?

Is it necessary to close each nested OutputStream and Writer separately?

我正在写一段代码:

OutputStream outputStream = new FileOutputStream(createdFile);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(gzipOutputStream));

我是否需要像下面这样关闭每个流或写入器?

gzipOutputStream.close();
bw.close();
outputStream.close();

或者只关闭最后一个流就可以了吗?

bw.close();

如果所有流都已实例化,那么只关闭最外层的就可以了。

Closeable 界面上的文档指出关闭方法:

Closes this stream and releases any system resources associated with it.

释放系统资源包括关闭流

它还指出:

If the stream is already closed then invoking this method has no effect.

因此,如果您之后明确关闭它们,就不会发生任何错误。

如果您只关闭最后一个流就没问题 - 关闭调用也会发送到底层流。

我宁愿使用 try(...) 语法 (Java 7),例如

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}

你可以关闭最外面的流,事实上你不需要保留所有的流,你可以使用Java 7 try-with-resources.

try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
                     new GZIPOutputStream(new FileOutputStream(createdFile)))) {
     // write to the buffered writer
}

如果您订阅 YAGNI,或 you-aint-gonna-need-it,您应该只添加您实际需要的代码。您不应该添加您认为可能需要但实际上没有任何用处的代码。

举个例子,想象一下如果你不这样做可能会出现什么问题,会产生什么影响?

try (
    OutputStream outputStream = new FileOutputStream(createdFile);
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
    OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
    BufferedWriter bw = new BufferedWriter(osw)
    ) {
    // ...
}

让我们从调用 open 完成所有实际工作的 FileOutputStream 开始。

/**
 * Opens a file, with the specified name, for overwriting or appending.
 * @param name name of file to be opened
 * @param append whether the file is to be opened in append mode
 */
private native void open(String name, boolean append)
    throws FileNotFoundException;

如果找不到该文件,则没有要关闭的基础资源,因此关闭它不会有任何影响。如果文件存在,它应该抛出 FileNotFoundException。因此,仅从这一行尝试关闭资源不会有任何收获。

您需要关闭文件的原因是文件打开成功,但稍后出现错误。

让我们看下一个流GZIPOutputStream

存在可以抛出异常的代码

private void writeHeader() throws IOException {
    out.write(new byte[] {
                  (byte) GZIP_MAGIC,        // Magic number (short)
                  (byte)(GZIP_MAGIC >> 8),  // Magic number (short)
                  Deflater.DEFLATED,        // Compression method (CM)
                  0,                        // Flags (FLG)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Extra flags (XFLG)
                  0                         // Operating system (OS)
              });
}

这将写入文件的 header。现在,您能够打开一个文件进行写入但甚至不能向其中写入 8 个字节,这将是非常不寻常的,但让我们想象一下这可能发生并且我们之后不会关闭该文件。如果文件没有关闭,文件会发生什么?

你没有得到任何未刷新的写入,它们被丢弃,在这种情况下,没有成功写入流的字节,此时无论如何都没有缓冲。但是未关闭的文件不会永远存在,而是 FileOutputStream

protected void finalize() throws IOException {
    if (fd != null) {
        if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
            flush();
        } else {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }
}

如果你根本不关闭一个文件,它无论如何都会被关闭,只是不会立即关闭(就像我说的那样,留在缓冲区中的数据将以这种方式丢失,但是 none此时)

不立即关闭文件的后果是什么?在正常情况下,您可能会丢失一些数据,并且可能 运行 超出文件描述符。但是,如果您有一个可以创建文件但不能向其中写入任何内容的系统,那么您的问题就更大了。即很难想象为什么你在失败的情况下反复尝试创建这个文件。

OutputStreamWriter 和 BufferedWriter 都不会在它们的构造函数中抛出 IOException,因此不清楚它们会导致什么问题。在 BufferedWriter 的情况下,您可能会遇到 OutOfMemoryError。在这种情况下,它会立即触发 GC,正如我们所见,无论如何都会关闭文件。

不,最高级别 Streamreader 将确保关闭所有 底层 流/阅读器。

检查最顶层流的close()方法实现

假设所有流都创建好了,是的,关闭 bw 就可以了 那些流实现;但这是一个很大的假设。

我会使用 try-with-resources (tutorial),这样构建后续流时抛出异常的任何问题都不会导致先前的流挂起,因此您不必依赖具有关闭底层流的调用:

try (
    OutputStream outputStream = new FileOutputStream(createdFile);
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
    OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
    BufferedWriter bw = new BufferedWriter(osw)
    ) {
    // ...
}

请注意,您根本不再调用 close

重要说明:要try-with-resources关闭它们,您必须在打开它们时将流分配给变量,你不能使用嵌套。如果您使用嵌套,则在构造后面的流之一(例如,GZIPOutputStream)期间出现异常将使由其中的嵌套调用构造的任何流保持打开状态。来自 JLS §14.20.3:

A try-with-resources statement is parameterized with variables (known as resources) that are initialized before execution of the try block and closed automatically, in the reverse order from which they were initialized, after execution of the try block.

注意这个词"variables" (我的重点)

例如,不要这样做:

// DON'T DO THIS
try (BufferedWriter bw = new BufferedWriter(
        new OutputStreamWriter(
        new GZIPOutputStream(
        new FileOutputStream(createdFile))))) {
    // ...
}

...因为来自 GZIPOutputStream(OutputStream) 构造函数的异常(表示它可能抛出 IOException,并将 header 写入基础流)将离开 FileOutputStream 打开。由于一些资源的构造函数可能会抛出异常而另一些则不会,所以单独列出它们是个好习惯。

我们可以double-check我们用这个程序解释那个 JLS 部分:

public class Example {

    private static class InnerMost implements AutoCloseable {
        public InnerMost() throws Exception {
            System.out.println("Constructing " + this.getClass().getName());
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
        }
    }

    private static class Middle implements AutoCloseable {
        private AutoCloseable c;

        public Middle(AutoCloseable c) {
            System.out.println("Constructing " + this.getClass().getName());
            this.c = c;
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
            c.close();
        }
    }

    private static class OuterMost implements AutoCloseable {
        private AutoCloseable c;

        public OuterMost(AutoCloseable c) throws Exception {
            System.out.println("Constructing " + this.getClass().getName());
            throw new Exception(this.getClass().getName() + " failed");
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
            c.close();
        }
    }

    public static final void main(String[] args) {
        // DON'T DO THIS
        try (OuterMost om = new OuterMost(
                new Middle(
                    new InnerMost()
                    )
                )
            ) {
            System.out.println("In try block");
        }
        catch (Exception e) {
            System.out.println("In catch block");
        }
        finally {
            System.out.println("In finally block");
        }
        System.out.println("At end of main");
    }
}

...输出为:

Constructing Example$InnerMost
Constructing Example$Middle
Constructing Example$OuterMost
In catch block
In finally block
At end of main

请注意,那里没有对 close 的调用。

如果我们修正 main:

public static final void main(String[] args) {
    try (
        InnerMost im = new InnerMost();
        Middle m = new Middle(im);
        OuterMost om = new OuterMost(m)
        ) {
        System.out.println("In try block");
    }
    catch (Exception e) {
        System.out.println("In catch block");
    }
    finally {
        System.out.println("In finally block");
    }
    System.out.println("At end of main");
}

然后我们得到适当的 close 调用:

Constructing Example$InnerMost
Constructing Example$Middle
Constructing Example$OuterMost
Example$Middle closed
Example$InnerMost closed
Example$InnerMost closed
In catch block
In finally block
At end of main

(是的,对 InnerMost#close 的两次调用是正确的;一个来自 Middle,另一个来自 try-with-resources。)

在Java7中,有一个特征try-with-resources。您无需明确关闭流,它会处理好。