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


127

我正在写一段代码:

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
对于相应的过时的Java 6中的问题,请参见 stackoverflow.com/questions/884007/...
Raedwald

2
请注意,您的示例存在一个错误,该错误可能导致数据丢失,因为关闭流的顺序不正确。关闭a时BufferedWriter,可能需要将缓冲的数据写入基础流,在您的示例中该基础流已关闭。避免这些问题是答案中显示的“ 尝试资源”方法的另一个优点。
23:54乔

Answers:


150

假设所有的流获得创建好了,是的,刚刚闭幕bw的精细与流实现 ; 但这是一个很大的假设。

我将使用try-with-resources教程),以便构造引发异常的后续流的任何问题都不会使之前的流挂起,因此您不必依赖具有调用close的流实现。基础流:

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

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

注意“变量”一词(我强调)

例如,不要这样做:

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

...由于GZIPOutputStream(OutputStream)构造函数的异常(表示可能抛出IOException,并向底层流写入标头)将导致问题FileOutputStream。由于某些资源的构造函数可能会抛出,而其他资源则不会,因此单独列出它们是一个好习惯。

我们可以使用以下程序仔细检查对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");
    }
}

...其输出为:

构造示例$ InnerMost
构造示例$ Middle
构造示例$ OuterMost
在捕获块中
在最后块
主末

请注意,那里没有电话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呼叫:

构造示例$ InnerMost
构造示例$ Middle
构造示例$ OuterMost
示例$中间关闭
示例$内部最封闭
示例$内部最封闭
在捕获块中
在最后块
主末

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


7
+1表示在流的构建过程中可能会引发异常,尽管我会指出,实际上,您要么会遇到内存不足的异常,要么会遇到同样严重的异常(此时,这实际上并不重要如果由于应用程序即将退出而关闭流),否则将抛出IOException的是GZIPOutputStream;其余构造函数没有检查过的异常,并且没有其他可能会产生运行时异常的情况。
2015年

5
@Jules:是的,确实是针对这些特定流。这更多是关于良好的习惯。
TJ Crowder

2
@PeterLawrey:我强烈不同意使用不良习惯或不依赖流实施。:-)这不是YAGNI / no-YAGNI的区别,它与构成可靠代码的模式有关。
TJ Crowder'2

2
@PeterLawrey:关于不信任java.io,上面也没有什么。一些流-泛化,一些资源 -来自构造函数。因此,我认为确保单独打开多个资源,以便在后续资源抛出时可靠地关闭它们是一个好习惯。如果您不同意,可以选择这样做,这很好。
TJ Crowder 2015年

2
@PeterLawrey:因此,您提倡花时间查看实现的源代码,以逐个案例的方式记录下记录异常的内容,然后说:“哦,嗯,它实际上并没有抛出异常,所以。 ..”并保存几个键入字符?我们在那里分开公司,很开心。:-)而且,我只是看了一下,而这并不是理论上的:GZIPOutputStream的构造函数将标头写入流中。因此它可以抛出。因此,现在的立场是,我认为在撰写文章后尝试关闭流是否值得打扰。是的:我打开了它,至少应该尝试关闭它。
TJ Crowder'2

12

您可以关闭最外面的流,实际上不需要保留所有已包装的流,并且可以使用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,正如我们已经看到的那样,它将始终关闭文件。


1
有关可能失败的情况,请参阅TJ Crowder的答案。
2015年

@TimK能否提供一个示例,说明创建文件的位置,但随后流失败,后果是什么。失败的风险极低,影响微不足道。不需要使事情变得比需要的复杂。
彼得·劳瑞

1
GZIPOutputStream(OutputStream)文档IOException,然后查看源代码,实际上写了一个标头。因此,构造函数可以抛出异常不是理论上的。您可能会觉得FileOutputStream在写入基础知识之后将底层保持开放状态是可以的。我不。
TJ Crowder'2

1
@TJCrowder我是一位经验丰富的专业JavaScript开发人员(以及其他语言)的任何人。我做不到 ;)
Peter Lawrey 2015年

1
只是为了重新审视这个问题,另一个问题是,如果您在文件上使用GZIPOutputStream而不显式调用finish,它将在其紧密实现中被调用。这不是尝试...最后,因此,如果finish / flush引发异常,则永远不会关闭基础文件句柄。
robert_difalco

6

如果所有流都已实例化,则仅关闭最外面的流就可以了。

有关的文档 Closeable接口指出了close方法:

关闭此流并释放与其关联的所有系统资源。

释放系统资源包括关闭流。

它还指出:

如果流已经关闭,则调用此方法无效。

因此,如果您随后明确将其关闭,则不会发生任何错误。


2
这假定构造流没有错误,这对于列出的流可能是正确的,也可能不是正确的,但通常并不能可靠地正确。
TJ Crowder


5

如果只关闭最后一个流,那就没问题了-close调用也将发送到基础流。


1
请参阅有关GrzegorzŻur的评论。
TJ Crowder

5

否,最高层,Stream否则reader将确保关闭所有基础流/阅读器。

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.