Yuvalanmış her bir OutputStream ve Writer'ı ayrı ayrı kapatmak gerekli mi?


127

Bir kod yazıyorum:

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

Her yayını veya yazarı aşağıdaki gibi kapatmam gerekir mi?

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

Yoksa son akışı kapatmak iyi olur mu?

bw.close();

1
İlgili eski Java 6 sorusu için bkz. Stackoverflow.com/questions/884007/…
Raedwald

2
Örneğinizde veri kaybına neden olabilecek bir hata olduğunu unutmayın, çünkü akışları açtığınız sırayla kapatıyorsunuz. Bir kapatırken , sizin örneğinizde zaten kapalı olan temeldeki akışa arabelleğe alınmış verileri BufferedWriteryazması gerekebilir . Bu sorunlardan kaçınmak , yanıtlarda gösterilen kaynakla deneme yaklaşımlarının bir başka avantajıdır .
Joe23

Yanıtlar:


150

Tüm akışların bwsorunsuz bir şekilde oluşturulduğunu varsayarsak, evet, bu akış uygulamalarıyla kapatmak yeterlidir ; ama bu büyük bir varsayım.

Bana kalırsa doğru denemek-ile-kaynaklar ( öğretici size yakın çağrı sahip akışı uygulamasına güvenmek zorunda kalmamak için daha sonraki akışları inşa herhangi bir sorun istisnalar atar önceki akışları asılı terk etmediklerini böylece) ve temeldeki akış:

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

Artık aramadığınızı unutmayın close.

Önemli not : Kaynaklarla denemeyi kapatmak için, akışları açarken değişkenlere atamanız gerekir , iç içe geçmeyi kullanamazsınız. GZIPOutputStreamİç içe yerleştirmeyi kullanırsanız , sonraki akışlardan birinin oluşturulması sırasında bir istisna (örneğin ), içindeki yuvalanmış çağrılar tarafından oluşturulan herhangi bir akışı açık bırakır. Gönderen §14.20.3 JLS :

Kaynaklarla dene ifadesi, bloğun yürütülmesinden önce başlatılan ve bloğun yürütülmesinden sonra başlatıldıkları ters sırada otomatik olarak kapatılan değişkenlerle (kaynaklar olarak bilinir) parametrelendirilir .trytry

"Değişkenler" kelimesine dikkat edin (vurgu) .

Örneğin, bunu yapmayın:

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

... çünkü GZIPOutputStream(OutputStream)yapıcıdan gelen bir istisna (atabileceğini IOExceptionve temeldeki akışa bir başlık yazdığını söyler ) FileOutputStreamaçık bırakacaktır . Bazı kaynakların kuruculara sahip olması ve bazılarının atmaması nedeniyle, bunları ayrı ayrı listelemek iyi bir alışkanlıktır.

Bu JLS bölümüne ilişkin yorumumuzu bu programla iki kez kontrol edebiliriz:

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

... çıktısı olan:

Örnek Oluşturuluyor $ InnerMost
Örnek Oluşturuluyor $ Middle
Örnek Oluşturma $ OuterMost
Yakalama bloğunda
Sonunda blokta
Ana yolun sonunda

Orada arama closeolmadığını unutmayın.

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

sonra uygun closearamaları alıyoruz :

Örnek Oluşturuluyor $ InnerMost
Örnek Oluşturuluyor $ Middle
Örnek Oluşturma $ OuterMost
Örnek $ Orta kapalı
Örnek $ InnerMost kapalı
Örnek $ InnerMost kapalı
Yakalama bloğunda
Sonunda blokta
Ana yolun sonunda

(Evet, iki çağrı InnerMost#closedoğrudur; biri Middlekaynakla denemeden, diğeri kaynaklardan.)


7
Akışların inşası sırasında istisnaların atılabileceğini belirtmek için +1, ancak gerçekçi olarak ya bellek dışı bir istisna ya da eşit derecede ciddi bir şey (bu noktada gerçekten önemli değil) akışlarınızı kapatırsanız, çünkü uygulamanız çıkmak üzeredir) veya bir IOException oluşturan GZIPOutputStream olacaktır; yapıcıların geri kalanının kontrol edilmiş istisnaları yoktur ve bir çalışma zamanı istisnası oluşturması muhtemel başka koşullar yoktur.
Jules

5
@Jules: Evet, bu belirli yayınlar için gerçekten. Daha çok iyi alışkanlıklar hakkında.
TJ Crowder

2
@PeterLawrey: Kötü alışkanlıkları kullanmaya veya akış uygulamasına bağlı olmama konusunda kesinlikle katılmıyorum. :-) Bu bir YAGNI / no-YAGNI ayrımı değildir, güvenilir kod için yapılan kalıplarla ilgilidir.
TJ Crowder

2
@PeterLawrey: Yukarıda güvenmemekle ilgili hiçbir şey java.ioyok. Bazı akışlar - genelleme, bazı kaynaklar - kuruculardan atılır. Bu nedenle, birden fazla kaynağın ayrı ayrı açıldığından emin olmak, böylece sonraki bir kaynak atılırsa güvenilir bir şekilde kapatılmalarını sağlamak sadece iyi bir alışkanlıktır. Aynı fikirde değilseniz, yapmamayı seçebilirsiniz , sorun değil.
TJ Crowder

2
@PeterLawrey: Yani, bir istisnayı belgeleyen bir şeyin kaynak koduna bakmak için zaman ayırmayı, duruma göre ve sonra "Oh, pekala, aslında atmıyor, yani. .. "ve yazarken birkaç karakter kaydediliyor mu? Orada şirketten ayrılıyoruz, büyük zaman. :-) Dahası, baktım ve bu teorik değil: GZIPOutputStream's kurucusu akışa bir başlık yazar. Ve böylece fırlatabilir. Yani şimdi durum, yazı attıktan sonra akışı kapatmaya çalışmanın zahmete değip değmeyeceğidir . Evet: Açtım, en azından kapatmayı denemeliyim.
TJ Crowder

12

En dıştaki akışı kapatabilirsiniz, aslında sarılmış tüm akışları tutmanıza gerek yoktur ve Java 7 kaynakla deneme özelliğini kullanabilirsiniz.

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

YAGNI'ye abone olursanız veya buna ihtiyacınız olmayacaksa, yalnızca gerçekten ihtiyacınız olan kodu eklemelisiniz. İhtiyaç duyabileceğinizi düşündüğünüz kodu eklememelisiniz, ancak gerçekte yararlı hiçbir şey yapmaz.

Bu örneği alın ve bunu yapmazsanız neyin yanlış gidebileceğini ve bunun etkisinin ne olacağını hayal edin.

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

openTüm gerçek işi yapmaya çağıran FileOutputStream ile başlayalım .

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

Dosya bulunmazsa kapatılacak temel kaynak yoktur, bu nedenle kapatmak herhangi bir fark yaratmaz. Dosya varsa, bir FileNotFoundException oluşturması gerekir. Yani kaynağı tek başına bu hattan kapatmaya çalışmakla kazanılacak bir şey yok.

Dosyayı kapatmanızın nedeni, dosyanın başarıyla açıldığı, ancak daha sonra bir hata alacağınız zamandır.

Bir sonraki akışa bakalım GZIPOutputStream

Bir istisna atabilecek bir kod var

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

Bu, dosyanın başlığını yazar. Şimdi, bir dosyayı yazmak için açabilmeniz ancak ona 8 bayt bile yazamamanız çok alışılmadık bir durum olurdu, ancak bunun olabileceğini düşünelim ve sonra dosyayı kapatmayalım. Kapatılmamış bir dosyaya ne olur?

Herhangi bir boşaltılmamış yazma almazsınız, bunlar atılır ve bu durumda, bu noktada zaten arabelleğe alınmayan akışa başarıyla yazılmış baytlar yoktur. Ancak kapatılmamış bir dosya sonsuza kadar yaşamaz, bunun yerine FileOutputStream'de

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

Bir dosyayı hiç kapatmazsanız, yine de kapatılır, hemen değil (ve dediğim gibi, bir arabellekte kalan veriler bu şekilde kaybolur, ancak bu noktada hiçbiri yoktur)

Dosyayı hemen kapatmamanın sonucu nedir? Normal koşullar altında, potansiyel olarak bazı verileri kaybedersiniz ve potansiyel olarak dosya tanımlayıcılarınız tükenir. Ancak dosya oluşturabileceğiniz bir sisteminiz varsa ancak bunlara hiçbir şey yazamıyorsanız, daha büyük bir sorununuz var. yani, başarısız olmanıza rağmen neden bu dosyayı tekrar tekrar oluşturmaya çalıştığınızı hayal etmek zor.

Hem OutputStreamWriter hem de BufferedWriter, kurucularında IOException oluşturmaz, bu nedenle hangi soruna neden olacakları net değildir. BufferedWriter durumunda, bir OutOfMemoryError alabilirsiniz. Bu durumda, derhal bir GC'yi tetikleyecektir, bu da gördüğümüz gibi dosyayı yine de kapatacaktır.


1
Bunun başarısız olabileceği durumlar için TJ Crowder'ın cevabına bakın.
TimK

@TimK, dosyanın nerede oluşturulduğu, ancak akışın daha sonra başarısız olduğu ve sonucun ne olduğu konusunda bir örnek verebilir misiniz? Başarısızlık riski son derece düşüktür ve etkisi önemsizdir. Olması gerekenden daha karmaşık hale getirmeye gerek yok.
Peter Lawrey

1
GZIPOutputStream(OutputStream)belgeler IOExceptionve kaynağa bakıldığında aslında bir başlık yazar. Yani bu kurucunun atabileceği teorik değil. Yazdıktan FileOutputStreamsonra temelini açık bırakmanın sorun olmadığını düşünebilirsiniz . Yapmıyorum.
TJ Crowder

1
@TJCrowder Deneyimli bir profesyonel JavaScript geliştiricisi olan (ve bunun dışında başka diller) şapkamı çıkarırım. Yapamadım ;)
Peter Lawrey

1
Sadece bunu tekrar gözden geçirmek için, diğer sorun, bir dosya üzerinde bir GZIPOutputStream kullanıyorsanız ve Fin'i açıkça çağırmıyorsanız, bunun yakın uygulamada çağrılacağıdır. Bu bir denemede değil ... Son olarak, eğer bitir / boşalt bir istisna atarsa, temeldeki dosya tanıtıcısı asla kapatılmayacaktır.
robert_difalco

6

Tüm akışlar somutlaştırılmışsa, yalnızca en dıştaki akışı kapatmak yeterlidir.

CloseableArayüzle ilgili dokümantasyon, kapatma yöntemini belirtir:

Bu akışı kapatır ve onunla ilişkili tüm sistem kaynaklarını serbest bırakır.

Serbest bırakma sistemi kaynakları, kapanış akışlarını içerir.

Ayrıca şunu belirtir:

Akış zaten kapalıysa, bu yöntemi çağırmanın bir etkisi yoktur.

Yani daha sonra onları açıkça kapatırsanız, yanlış bir şey olmayacaktır.


2
Bu , listelenenler için doğru olabilecek veya olmayabilecek, ancak genel olarak güvenilir şekilde doğru olmayan akışları oluştururken hiçbir hata olmadığını varsayar .
TJ Crowder


5

Yalnızca son akışı kapatırsanız sorun olmayacaktır - kapanış çağrısı da temeldeki akışlara gönderilecektir.


1
Grzegorz Sur'un cevabı hakkındaki yoruma bakınız.
TJ Crowder

5

Hayır, en üst düzey Streamveya readertüm temel akışların / okuyucuların kapatılmasını sağlayacaktır .

En üst düzey akışınızın close()yöntem uygulamasını kontrol edin .


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.