ネストされた各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 /…を

2
ストリームには、開いた順序ではなくストリームを閉じるため、データの損失を引き起こす可能性のあるバグがあることに注意してください。を閉じるときに、バッファリングされたデータを基になるストリームBufferedWriterに書き込む必要がある場合があります。これは、例ではすでに閉じられています。これらの問題を回避することは、回答に示されているリソースを使用した試行の別の利点です。
Joe23、2015

回答:


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でそれらを閉じるには、ストリームを開くときに変数に割り当てる必要があります。ネストは使用できません。ネストを使用する場合、後のストリームの1つ(たとえば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");
    }
}

...出力があります:

Example $ InnerMostの作成
Example $ Middleの構築
Example $ 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呼び出しを取得します。

Example $ InnerMostの作成
Example $ Middleの構築
Example $ OuterMostの構築
例$ミドル閉鎖
例$ InnerMost closed
例$ InnerMost closed
キャッチブロック内
最後にブロックで
メインの終わりに

(はい、2つの呼び出しInnerMost#closeは正しいです。1つはからMiddle、もう1つはtry-with-resourcesからです。)


7
ストリームの構築中に例外がスローされる可能性があることに注意して+1してください。ただし、実際には、メモリ不足の例外または同等に深刻な例外が発生することに注意してください(この時点では、問題はありません。アプリケーションが終了しようとしているためにストリームを閉じた場合)、またはIOExceptionをスローするのはGZIPOutputStreamです。残りのコンストラクターにはチェック例外はありません。また、実行時例外を生成する可能性のある他の状況はありません。
ジュール

5
@ジュール:ええ、確かにこれらの特定のストリームのために。それは良い習慣についてです。
TJクラウダー2015

2
@PeterLawrey:ストリームの実装に依存していない、または悪い習慣を使用することに強く反対します。:-)これはYAGNI / no-YAGNIの区別ではなく、信頼できるコードを作成するためのパターンに関するものです。
TJクラウダー2015

2
@PeterLawrey:信頼しないことについても上記のようなことはありませんjava.io。一部のストリーム(一般化、一部のリソース)は、コンストラクターからスローされます。したがって、複数のリソースが個別に開かれていることを確認してください。そうすれば、後続のリソースのスローがちょうど良い習慣である場合、それらを確実に閉じることができます。同意しない場合は、実行しないことを選択できます。
TJクラウダー2015

2
@PeterLawrey:では、時間をかけて、ケースバイケースで例外を文書化する何かの実装のソースコードを見て、「ああ、まあ、実際にはスローされないので、 .. "と入力のいくつかの文字を保存しますか?私たちはそこに会社を分けます、大きな時間。:-)さらに、私は見たところ、これは理論的ではありません:GZIPOutputStreamのコンストラクターは、ヘッダーをストリームに書き込みます。そしてそれは投げることができます。だから今の立場は、私が書いた投げた後にストリームを閉じることを試みることは気になる価値があると思うかどうかです。ええ:私はそれを開けました、少なくともそれを閉じようとするべきです。
TJクラウダー2015

12

最も外側のストリームを閉じることができます。実際には、すべてのストリームをラップして保持する必要はなく、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)
              });
}

これにより、ファイルのヘッダーが書き込まれます。これで、書き込み用にファイルを開くことができても、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の回答を参照してください。
TimK、2015

@TimKを使用すると、ファイルの作成場所の例を提供できますが、後でストリームが失敗し、その結果はどうなりますか。障害のリスクは非常に低く、影響はごくわずかです。必要以上に複雑にする必要はありません。
Peter Lawrey、2015

1
GZIPOutputStream(OutputStream)ドキュメントIOExceptionと、ソースを見て、実際にヘッダーを書き込みます。ですから、それは理論的ではなく、コンストラクタが投げることができます。FileOutputStreamスローに書き込んだ後、下層を開いたままにしておいても大丈夫だと感じるかもしれません。私はしません。
TJクラウダー2015

1
@TJCrowder経験豊富なプロのJavaScript開発者(およびその他の言語)の人なら誰でも私は脱帽です。できませんでした。;)
Peter Lawrey、2015

1
これを再確認するために、他の問題は、ファイルでGZIPOutputStreamを使用していて、finishを明示的に呼び出さない場合、そのclose実装で呼び出されることです。これは試みではありません...最後に、finish / flushが例外をスローした場合、基になるファイルハンドルは決して閉じられません。
robert_difalco 2017

6

すべてのストリームがインスタンス化されている場合は、最も外側のストリームのみを閉じても問題ありません。

Closeableインターフェイスに関するドキュメントには、そのcloseメソッドが記載されています。

このストリームを閉じ、それに関連付けられているすべてのシステムリソースを解放します。

システムリソースの解放には、ストリームのクローズが含まれます。

また、次のように述べています。

ストリームがすでに閉じている場合、このメソッドを呼び出しても効果はありません。

したがって、後で明示的に閉じても問題はありません。


2
これは、ストリームの構築時にエラーが発生しないことを前提としています。これは、リストされているストリームに当てはまる場合と当てはまらない場合がありますが、一般的に確実に当てはまるわけではありません
TJクラウダー2015

6

try(...)構文(Java 7)を使用したい、たとえば

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

4
私はあなたに同意しますが、このアプローチの利点を強調し、OPが子/内部ストリームを閉じる必要がある場合は要点に答えたいと思うかもしれません
MadProgrammer

5

最後のストリームのみを閉じれば問題はありません。閉じる呼び出しも基になるストリームに送信されます。


1
GrzegorzŻurの回答に関するコメントを参照してください。
TJクラウダー2015

5

いいえ、最上位レベル、Streamまたはreaderすべての基になるストリーム/リーダーが確実に閉じられます。

最上位レベルのストリームのclose()メソッド実装を確認してください。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.