Apakah perlu untuk menutup setiap OutputStream dan Penulis yang bersarang secara terpisah?

127

Saya menulis sepotong kode:

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

Apakah saya perlu menutup setiap aliran atau penulis seperti yang berikut ini?

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

Atau hanya akan menutup aliran terakhir baik-baik saja?

bw.close();
Adon Smith
sumber
1
Untuk pertanyaan Java 6 usang yang sesuai, lihat stackoverflow.com/questions/884007/…
Raedwald
2
Perhatikan bahwa contoh Anda memiliki bug yang dapat menyebabkan kehilangan data, karena Anda menutup aliran tidak dalam urutan yang Anda buka. Saat menutup BufferedWriter, mungkin perlu menulis data buffer ke aliran yang mendasarinya, yang dalam contoh Anda sudah ditutup. Menghindari masalah ini adalah keuntungan lain dari pendekatan coba sumber daya yang ditunjukkan dalam jawaban.
Joe23

Jawaban:

150

Dengan asumsi semua aliran bisa dibuat baik-baik saja, ya, hanya menutup bwbaik - baik saja dengan implementasi aliran itu ; tapi itu asumsi besar.

Saya akan menggunakan try-with-resources ( tutorial ) sehingga masalah apa pun yang membangun aliran berikutnya yang melempar pengecualian tidak membuat aliran sebelumnya menggantung, dan jadi Anda tidak harus bergantung pada implementasi aliran yang membuat panggilan untuk ditutup aliran yang mendasarinya:

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

Perhatikan Anda tidak lagi menelepon closesama sekali.

Catatan penting : Agar sumber daya coba tutup, Anda harus menetapkan aliran ke variabel saat Anda membukanya, Anda tidak dapat menggunakan bersarang. Jika Anda menggunakan nesting, pengecualian selama pembangunan salah satu aliran yang lebih baru (katakanlah, GZIPOutputStream) akan membuat aliran apa pun yang dibangun oleh panggilan bersarang di dalamnya terbuka. Dari JLS §14.20.3 :

Pernyataan coba-dengan-sumber daya diparameterisasi dengan variabel (dikenal sebagai sumber daya) yang diinisialisasi sebelum eksekusi tryblok dan ditutup secara otomatis, dalam urutan terbalik dari mana mereka diinisialisasi, setelah eksekusi tryblok.

Perhatikan kata "variabel" (penekanan saya) .

Misalnya, jangan lakukan ini:

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

... karena pengecualian dari GZIPOutputStream(OutputStream)konstruktor (yang mengatakan mungkin melempar IOException, dan menulis header ke aliran yang mendasarinya) akan membiarkan FileOutputStreamterbuka. Karena beberapa sumber memiliki konstruktor yang dapat melempar dan yang lain tidak, itu kebiasaan yang baik untuk hanya daftar secara terpisah.

Kami dapat memeriksa ulang interpretasi kami atas bagian JLS itu dengan program ini:

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");
    }
}

... yang memiliki output:

Contoh Membangun $ InnerMost
Membangun Contoh $ Tengah
Membangun Contoh $ OuterMost
Di blok tangkap
Akhirnya blok
Di ujung utama

Perhatikan bahwa tidak ada panggilan ke closesana.

Jika kami memperbaiki 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");
}

maka kami mendapatkan closepanggilan yang sesuai :

Contoh Membangun $ InnerMost
Membangun Contoh $ Tengah
Membangun Contoh $ OuterMost
Contoh $ Tengah ditutup
Contoh $ InnerMost ditutup
Contoh $ InnerMost ditutup
Di blok tangkap
Akhirnya blok
Di ujung utama

(Ya, dua panggilan untuk InnerMost#closeitu benar; satu dari Middle, yang lain dari coba-dengan-sumber daya.)

TJ Crowder
sumber
7
+1 untuk mencatat bahwa pengecualian mungkin dilemparkan selama pembangunan aliran, meskipun saya akan mencatat bahwa secara realistis Anda akan mendapatkan pengecualian di luar memori atau sesuatu yang sama seriusnya (pada titik itu sebenarnya tidak masalah jika Anda menutup streaming Anda, karena aplikasi Anda akan keluar), atau itu akan menjadi GZIPOutputStream yang melempar IOException; sisa konstruktor tidak memiliki pengecualian diperiksa, dan tidak ada keadaan lain yang cenderung menghasilkan pengecualian runtime.
Jules
5
@ Jules: Ya, untuk aliran spesifik ini, memang. Ini lebih tentang kebiasaan baik.
TJ Crowder
2
@PeterLawrey: Saya sangat tidak setuju dengan kebiasaan buruk atau tidak tergantung pada implementasi stream. :-) Ini bukan perbedaan YAGNI / no-YAGNI, ini tentang pola yang membuat kode dapat diandalkan.
TJ Crowder
2
@PeterLawrey: Tidak ada yang di atas tentang tidak percaya java.io. Beberapa aliran - generalisasi, beberapa sumber daya - membuang dari konstruktor. Jadi, memastikan beberapa sumber daya dibuka secara individual sehingga mereka dapat ditutup andal jika sumber daya berikutnya hanya merupakan kebiasaan yang baik, dalam pandangan saya. Anda dapat memilih untuk tidak melakukannya jika Anda tidak setuju, itu tidak masalah.
TJ Crowder
2
@PeterLawrey: Jadi Anda menganjurkan meluangkan waktu untuk melihat kode sumber implementasi untuk sesuatu yang mendokumentasikan pengecualian, berdasarkan kasus per kasus, dan kemudian berkata "Oh, well, itu tidak benar-benar melempar, jadi. .. "dan menyimpan beberapa karakter mengetik? Kami berpisah di sana, senang. :-) Selain itu, saya hanya melihat, dan ini bukan teori: GZIPOutputStreamkonstruktor menulis header ke stream. Dan itu bisa melempar. Jadi sekarang posisinya adalah apakah saya pikir layak untuk mencoba untuk menutup aliran setelah penulisan melemparkan. Ya: Saya membukanya, setidaknya saya harus mencoba untuk menutupnya.
TJ Crowder
12

Anda dapat menutup aliran paling luar, bahkan Anda tidak perlu mempertahankan semua aliran yang dibungkus dan Anda dapat menggunakan Java 7 coba-dengan-sumber daya.

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

Jika Anda berlangganan YAGNI, atau Anda -tidak akan membutuhkannya, Anda seharusnya hanya menambahkan kode yang benar-benar Anda butuhkan. Anda seharusnya tidak menambahkan kode yang Anda bayangkan mungkin Anda butuhkan tetapi pada kenyataannya tidak melakukan sesuatu yang bermanfaat.

Ambil contoh ini dan bayangkan apa yang mungkin salah jika Anda tidak melakukan ini dan apa dampaknya?

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

Mari kita mulai dengan FileOutputStream yang memanggil openuntuk melakukan semua pekerjaan nyata.

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

Jika file tidak ditemukan, tidak ada sumber daya yang mendasarinya untuk ditutup, jadi menutupnya tidak akan membuat perbedaan. Jika file ada, itu harus membuang FileNotFoundException. Jadi tidak ada yang bisa diperoleh dengan mencoba menutup sumber daya dari garis ini saja.

Alasan Anda perlu menutup file adalah ketika file dibuka dengan sukses, tetapi Anda kemudian mendapatkan kesalahan.

Mari kita lihat aliran selanjutnya GZIPOutputStream

Ada kode yang bisa melempar pengecualian

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

Ini menulis header file. Sekarang akan sangat tidak biasa bagi Anda untuk dapat membuka file untuk menulis tetapi tidak dapat menulis bahkan 8 byte untuk itu, tetapi mari kita bayangkan ini bisa terjadi dan kami tidak menutup file setelahnya. Apa yang terjadi pada file jika tidak ditutup?

Anda tidak mendapatkan tulisan yang tidak disiram, mereka dibuang dan dalam hal ini, tidak ada byte yang berhasil ditulis ke aliran yang tidak buffer pada saat ini. Tetapi file yang tidak ditutup tidak hidup selamanya, sebaliknya FileOutputStream memiliki

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

Jika Anda tidak menutup file sama sekali, file tetap akan ditutup, tidak segera (dan seperti saya katakan, data yang tersisa di buffer akan hilang dengan cara ini, tetapi tidak ada pada saat ini)

Apa konsekuensi dari tidak segera menutup file? Dalam kondisi normal, Anda berpotensi kehilangan beberapa data, dan Anda berpotensi kehabisan file deskriptor. Tetapi jika Anda memiliki sistem di mana Anda dapat membuat file tetapi Anda tidak dapat menulis apa pun kepada mereka, Anda memiliki masalah lebih besar. yaitu sulit membayangkan mengapa Anda berulang kali mencoba membuat file ini meskipun Anda gagal.

Baik OutputStreamWriter dan BufferedWriter tidak melempar IOException ke konstruktor mereka, jadi tidak jelas masalah apa yang akan mereka sebabkan. Dalam kasus BufferedWriter, Anda bisa mendapatkan OutOfMemoryError. Dalam hal ini akan segera memicu GC, yang seperti telah kita lihat akan menutup file.

Peter Lawrey
sumber
1
Lihat jawaban TJ Crowder untuk situasi di mana ini bisa gagal.
TimK
@TimK dapatkah Anda memberikan contoh di mana file dibuat tetapi aliran kemudian gagal dan apa konsekuensinya. Risiko kegagalan sangat rendah dan dampaknya sepele. Tidak perlu membuat yang lebih rumit dari yang seharusnya.
Peter Lawrey
1
GZIPOutputStream(OutputStream)dokumen IOExceptiondan, melihat sumbernya, sebenarnya menulis header. Jadi bukan teori, bahwa konstruktor dapat melempar. Anda mungkin merasa tidak apa-apa untuk membiarkan dasarnya FileOutputStreamterbuka setelah menulis. Bukan saya.
TJ Crowder
1
@TJCrowder Siapa saja yang merupakan pengembang JavaScript profesional yang berpengalaman (dan bahasa lain selain) saya angkat topi. Saya tidak bisa melakukannya. ;)
Peter Lawrey
1
Hanya untuk meninjau kembali ini, masalah lainnya adalah bahwa jika Anda menggunakan GZIPOutputStream pada file dan jangan panggil selesai secara eksplisit, itu akan dipanggil dalam implementasi tertutup. Ini tidak di coba ... akhirnya jadi jika finish / flush melempar pengecualian maka pegangan file yang mendasarinya tidak akan pernah ditutup.
robert_difalco
6

Jika semua aliran telah dipakai maka menutup hanya yang terluar saja.

Dokumentasi pada Closeableantarmuka menyatakan bahwa metode tutup:

Menutup aliran ini dan melepaskan sumber daya sistem apa pun yang terkait dengannya.

Sumber daya sistem pelepasan mencakup aliran penutup.

Ini juga menyatakan bahwa:

Jika aliran sudah ditutup maka menjalankan metode ini tidak akan berpengaruh.

Jadi jika Anda menutupnya secara eksplisit setelah itu, tidak ada yang salah akan terjadi.

Grzegorz Żur
sumber
2
Ini mengasumsikan tidak ada kesalahan dalam membangun stream, yang mungkin benar atau tidak benar untuk yang terdaftar, tetapi secara umum tidak benar.
TJ Crowder
6

Saya lebih suka menggunakan try(...)sintaks (Java 7), mis

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}
Dmitry Bychenko
sumber
4
Meskipun saya setuju dengan Anda, Anda mungkin ingin menyoroti manfaat dari pendekatan ini dan menjawab intinya jika OP perlu menutup aliran anak / batin
MadProgrammer
5

Ini akan baik-baik saja jika Anda hanya menutup aliran terakhir - panggilan dekat juga akan dikirimkan ke aliran yang mendasarinya.

Codeversum
sumber
1
Lihat komentar tentang jawaban Grzegorz Żur.
TJ Crowder
5

Tidak, tingkat paling atas Streamatau readerakan memastikan bahwa semua aliran / pembaca yang mendasarinya ditutup.

Periksa implementasiclose() metode aliran level teratas Anda.

TheLostMind
sumber
5

Di Java 7, ada fitur try-with-resources . Anda tidak perlu menutup aliran secara eksplisit, itu akan mengatasi hal itu.

Sivakumar
sumber