是否有必要分别关闭每个嵌套的输出流和写入器?

2022-08-31 08:25:23

我正在写一段代码:

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();

答案 1

假设所有流都创建正常,是的,对于这些流实现,只需关闭就可以了;但这是一个很大的假设。bw

我会使用 try-with-resources教程),这样构造引发异常的后续流的任何问题都不会让以前的流挂起,因此您不必依赖具有调用来关闭底层流的流实现:

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

请注意,您不再拨打电话。close

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

try-with-resources 语句使用变量(称为 resources)进行参数化,这些变量在执行块之前初始化,并在执行块后以初始化的顺序自动关闭。trytry

注意“变量”这个词(我的强调)。

例如,不要这样做:

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

...因为来自 GZIPOutputStream(OutputStream) 构造函数的异常(它说它可能会抛出 ,并将标头写入底层流)将使该构造函数保持打开状态。由于某些资源具有可能引发的构造函数,而其他资源则没有,因此最好将它们单独列出。IOExceptionFileOutputStream

我们可以使用此程序仔细检查我们对该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

(是的,两个调用是正确的;一个来自 ,另一个来自 try-with-resources。InnerMost#closeMiddle


答案 2

您可以关闭最外层的流,实际上您不需要保留所有包装的流,您可以使用Java 7 try-with-resources。

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

如果您订阅了 YAGNI,或者您不需要它,则应该只添加实际需要的代码。你不应该添加你认为你可能需要的代码,但实际上并没有做任何有用的事情。

举个例子,想象一下,如果你不这样做,可能会出错,会有什么影响?

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

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

/**
 * 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)
              });
}

这将写入文件的标头。现在,如果您能够打开文件进行写入,但无法向其写入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();
        }
    }
}

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

不立即关闭文件的后果是什么?在正常情况下,您可能会丢失一些数据,并且可能会用完文件描述符。但是,如果你有一个系统,你可以创建文件,但你不能写任何东西到它们,你有一个更大的问题。也就是说,很难想象为什么你反复尝试创建这个文件,尽管你失败了。

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