是否有必要单独关闭每个嵌套的OutputStream和Writer?

Ado*_*ith 126 java file-io outputstream writer

我正在写一段代码:

OutputStream outputStream = new FileOutputStream(createdFile);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(gzipOutputStream));
Run Code Online (Sandbox Code Playgroud)

我是否需要关闭以下每个流或作者?

gzipOutputStream.close();
bw.close();
outputStream.close();
Run Code Online (Sandbox Code Playgroud)

或者只关闭最后一个流好吗?

bw.close();
Run Code Online (Sandbox Code Playgroud)

T.J*_*der 146

假设所有的流获得创建好了,是的,刚刚闭幕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)
    ) {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

请注意,您根本不再打电话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))))) {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

...因为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");
    }
}
Run Code Online (Sandbox Code Playgroud)

...有输出:

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");
}
Run Code Online (Sandbox Code Playgroud)

然后我们得到适当的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.)

  • +1注意到在构建流时可能会抛出异常,虽然我会注意到,实际上你要么会得到一个内存异常或同样严重的东西(此时它真的没关系如果你关闭你的流,因为你的应用程序即将退出),或者它将是抛出IOException的GZIPOutputStream; 其余的构造函数没有检查异常,并且没有其他情况可能产生运行时异常. (7认同)
  • @Jules:是的,确实,对于这些特定的流.这更多是关于良好的习惯. (5认同)
  • @PeterLawrey:我非常不同意使用坏习惯或不依赖于流实现.:-)这不是YAGNI/no-YAGNI的区别,而是关于可靠代码的模式. (2认同)
  • @PeterLawrey:上面没有任何关于不信任`java.io`的内容.一些流 - 一般化,一些*资源* - 从构造函数抛出.因此,在我看来,确保单独打开多个资源,以便在后续资源抛出时可以可靠地关闭它们.如果你不同意,你可以选择*不*这样做,那很好. (2认同)
  • @PeterLawrey:所以你提倡花时间查看一个实现的源代码,记录一个异常的内容,逐个案例,然后说"哦,好吧,它实际上并没有抛出,所以. .."并保存一些打字字符?我们在那里分公司,很大的时间.:-)而且,我只是看了,这不是理论上的:`GZIPOutputStream`的构造函数将一个标题写入流.所以它可以抛出.所以现在的位置是我是否认为值得打扰*尝试*在写入后关闭流.是的:我打开它,我至少应该尝试关闭它. (2认同)
  • @EJP:变量不是关闭,而是关于未能打开,所以你所谈论的流甚至不存在或者有机会关闭它的底层. (2认同)

Pet*_*rey 12

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

try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
                     new GZIPOutputStream(new FileOutputStream(createdFile)))) {
     // write to the buffered writer
}
Run Code Online (Sandbox Code Playgroud)

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

以这个例子为例,想象如果你不这样做会出现什么问题,影响会是什么?

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

让我们从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;
Run Code Online (Sandbox Code Playgroud)

如果找不到该文件,则没有要关闭的底层资源,因此关闭它不会有任何区别.如果文件存在,则应抛出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)
              });
}
Run Code Online (Sandbox Code Playgroud)

这会写入文件的标头.现在,你能够打开一个文件进行写入但是甚至不能写入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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 有关可能失败的情况,请参阅 TJ Crowder 的回答。 (3认同)
  • `GZIPOutputStream(OutputStream)` 记录了 `IOException`,查看源代码,实际上写入了一个标头。所以这不是理论上的,构造函数可以抛出。您可能会觉得在写入抛出后让底层的 `FileOutputStream` 保持打开状态是可以的。我不。 (2认同)

Grz*_*Żur 6

如果已经实例化了所有流,那么仅关闭最外层就好了.

关于Closeable接口的文档说明了close方法:

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

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

它还指出:

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

因此,如果您事后明确地关闭它们,那么不会发生任何错误.

  • 假设没有错误*构造*流,对于列出的那些可能或可能不是,但是**通常不是**可靠的. (2认同)

Dmi*_*nko 6

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

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}
Run Code Online (Sandbox Code Playgroud)

  • 虽然我同意你的意见,但你可能想强调这种方法的好处,如果OP需要关闭子/内部流,则可以回答这一点 (4认同)

Cod*_*sum 5

如果你只关闭最后一个流也没关系 - 关闭调用也将发送到底层流.


The*_*ind 5

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

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


Siv*_*mar 5

在Java 7中,有一个try-with-resources功能.你不需要明确地关闭你的流,它会照顾你.