Czy konieczne jest osobne zamykanie każdego zagnieżdżonego programu OutputStream i Writer?

128

Piszę kawałek kodu:

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

Czy muszę zamykać każdy strumień lub program zapisujący, jak poniżej?

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

A może po prostu zamknięcie ostatniego strumienia będzie w porządku?

bw.close();
Adon Smith
źródło
1
Odpowiednie przestarzałe pytanie dotyczące Java 6 można znaleźć na stronie stackoverflow.com/questions/884007/…
Raedwald
2
Zwróć uwagę, że Twój przykład zawiera błąd, który może spowodować utratę danych, ponieważ zamykasz strumienie w innej kolejności niż ich otwieranie. Podczas zamykania BufferedWritermoże być konieczne zapisanie buforowanych danych w źródłowym strumieniu, który w Twoim przykładzie jest już zamknięty. Unikanie tych problemów to kolejna zaleta podejść opartych na próbach z zasobami przedstawionych w odpowiedziach.
Joe23

Odpowiedzi:

151

Zakładając, że wszystkie strumienie zostały utworzone w porządku, tak, samo zamknięcie bwjest w porządku w przypadku tych implementacji strumieni ; ale to duże założenie.

Użyłbym try-with-resources ( tutorial ), aby wszelkie problemy z konstruowaniem kolejnych strumieni, które generują wyjątki, nie pozostawiły poprzednich strumieni w zawieszeniu, więc nie musisz polegać na implementacji strumienia, która ma wywołanie do zamknięcia podstawowy strumień:

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

Zauważ, że już closew ogóle nie dzwonisz .

Ważna uwaga : aby zamykać je z zasobami typu „próba z zasobami”, należy przypisać strumienie do zmiennych podczas ich otwierania, nie można używać zagnieżdżania. Jeśli używasz zagnieżdżania, wyjątek podczas konstruowania jednego z późniejszych strumieni (powiedzmy GZIPOutputStream) pozostawi otwarty każdy strumień utworzony przez zagnieżdżone wywołania wewnątrz niego. Od JLS §14.20.3 :

Instrukcja try-with-resources jest sparametryzowana zmiennymi (zwanymi zasobami), które są inicjowane przed wykonaniem trybloku i zamykane automatycznie, w odwrotnej kolejności, w jakiej zostały zainicjowane, po wykonaniu trybloku.

Zwróć uwagę na słowo „zmienne” (moje wyróżnienie) .

Np. Nie rób tego:

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

... ponieważ wyjątek od GZIPOutputStream(OutputStream)konstruktora (który mówi, że może zgłosić IOExceptioni zapisuje nagłówek do źródłowego strumienia) pozostawiłby FileOutputStreamotwarty. Ponieważ niektóre zasoby mają konstruktory, które mogą rzucać, a inne nie, dobrym zwyczajem jest umieszczanie ich osobno.

Możemy dwukrotnie sprawdzić naszą interpretację tej sekcji JLS za pomocą tego programu:

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óry ma wyjście:

Konstruowanie przykładu $ InnerMost
Konstruowanie przykładu $ Middle
Konstruowanie przykładu $ OuterMost
W bloku haczykowym
W końcu blok
Pod koniec main

Zauważ, że nie ma tam żadnych połączeń close.

Jeśli naprawimy 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");
}

wtedy otrzymujemy odpowiednie closewezwania:

Konstruowanie przykładu $ InnerMost
Konstruowanie przykładu $ Middle
Konstruowanie przykładu $ OuterMost
Przykład $ Środek zamknięty
Przykład $ InnerMost zamknięte
Przykład $ InnerMost zamknięte
W bloku haczykowym
W końcu blok
Pod koniec main

(Tak, dwa wywołania InnerMost#closesą poprawne; jedno pochodzi z Middle, a drugie z try-with-resources.)

TJ Crowder
źródło
7
+1 za zauważenie, że wyjątki mogą być rzucane podczas tworzenia strumieni, chociaż zauważę, że realistycznie albo dostaniesz wyjątek braku pamięci, albo coś równie poważnego (w tym momencie to naprawdę nie ma znaczenia jeśli zamkniesz strumienie, ponieważ Twoja aplikacja ma się zakończyć), lub to GZIPOutputStream wyrzuca IOException; pozostałe konstruktory nie mają sprawdzonych wyjątków i nie ma innych okoliczności, które mogą spowodować wyjątek czasu wykonywania.
Jules
5
@Jules: Tak, rzeczywiście dla tych konkretnych strumieni. Chodzi bardziej o dobre nawyki.
TJ Crowder
3
@PeterLawrey: Zdecydowanie nie zgadzam się na używanie złych nawyków lub nie w zależności od implementacji strumienia. :-) To nie jest rozróżnienie YAGNI / no-YAGNI, chodzi o wzorce, które tworzą niezawodny kod.
TJ Crowder
2
@PeterLawrey: Nie ma też nic ponad brak zaufania java.io. Niektóre strumienie - uogólniając, niektóre zasoby - wyrzucają od konstruktorów. Tak więc upewnianie się, że wiele zasobów jest otwieranych indywidualnie, aby można je było niezawodnie zamknąć, jeśli kolejny wyrzucony zasób jest moim zdaniem dobrym nawykiem. Możesz tego nie robić, jeśli się nie zgadzasz, w porządku.
TJ Crowder
3
@EJP: Zmienne nie dotyczą zamknięcia, ale niepowodzenia otwarcia, więc strumień, o którym mówisz, nawet nie istnieje ani nie ma szansy na zamknięcie tego, który jest podstawą.
TJ Crowder
11

Możesz zamknąć najbardziej zewnętrzny strumień, w rzeczywistości nie musisz zachowywać wszystkich opakowanych strumieni i możesz użyć Java 7 try-with-resources.

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

Jeśli subskrybujesz YAGNI lub nie będziesz go potrzebować, powinieneś dodawać tylko kod, którego faktycznie potrzebujesz. Nie powinieneś dodawać kodu, który Twoim zdaniem może być potrzebny, ale w rzeczywistości nie robi nic pożytecznego.

Weź ten przykład i wyobraź sobie, co mogłoby się nie udać, gdybyś tego nie zrobił i jaki byłby tego wpływ?

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

Zacznijmy od FileOutputStream, który wywołuje opencałą prawdziwą pracę.

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

Jeśli plik nie zostanie znaleziony, nie ma bazowego zasobu do zamknięcia, więc zamknięcie go nie spowoduje żadnej różnicy. Jeśli plik istnieje, powinien generować wyjątek FileNotFoundException. Nie ma więc nic do zyskania, próbując zamknąć zasób tylko z tej linii.

Powodem, dla którego musisz zamknąć plik, jest pomyślne otwarcie pliku, ale później pojawia się błąd.

Spójrzmy na następny strumień GZIPOutputStream

Istnieje kod, który może zgłosić wyjątek

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

To zapisuje nagłówek pliku. Teraz byłoby bardzo nietypowe, gdybyś mógł otworzyć plik do zapisu, ale nie byłbyś w stanie zapisać do niego nawet 8 bajtów, ale wyobraźmy sobie, że może się to zdarzyć i nie zamykamy pliku później. Co się dzieje z plikiem, jeśli nie jest zamknięty?

Nie dostajesz żadnych niezapisanych zapisów, są one odrzucane iw tym przypadku nie ma pomyślnie zapisanych bajtów do strumienia, który i tak nie jest w tym momencie buforowany. Ale plik, który nie jest zamknięty, nie żyje wiecznie, zamiast tego FileOutputStream ma

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

Jeśli w ogóle nie zamkniesz pliku, i tak zostanie on zamknięty, ale nie od razu (i jak powiedziałem, dane pozostawione w buforze zostaną w ten sposób utracone, ale w tym momencie ich nie ma)

Jakie są konsekwencje braku natychmiastowego zamknięcia pliku? W normalnych warunkach potencjalnie utracisz część danych i potencjalnie zabraknie deskryptorów plików. Ale jeśli masz system, w którym możesz tworzyć pliki, ale nie możesz nic do nich pisać, masz większy problem. tj. trudno sobie wyobrazić, dlaczego wielokrotnie próbujesz utworzyć ten plik, mimo że nie udaje Ci się.

Zarówno OutputStreamWriter, jak i BufferedWriter nie zgłaszają wyjątku IOException w swoich konstruktorach, więc nie jest jasne, jaki problem spowodowałby. W przypadku BufferedWriter możesz uzyskać OutOfMemoryError. W tym przypadku natychmiast uruchomi GC, który, jak widzieliśmy, i tak zamknie plik.

Peter Lawrey
źródło
1
Zobacz odpowiedź TJ Crowdera na sytuacje, w których może się to nie udać.
TimK
@TimK, czy możesz podać przykład, gdzie plik jest tworzony, ale strumień później kończy się niepowodzeniem i jakie są tego konsekwencje. Ryzyko niepowodzenia jest niezwykle niskie, a wpływ znikomy. Nie trzeba robić tego bardziej skomplikowane niż to konieczne.
Peter Lawrey
1
GZIPOutputStream(OutputStream)dokumenty IOExceptioni patrząc na źródło, w rzeczywistości pisze nagłówek. Więc to nie jest teoria, że ​​konstruktor może rzucać. Możesz uznać, że pozostawienie instrumentu bazowego FileOutputStreamotwartego po zapisaniu do niego jest w porządku . Ja nie.
TJ Crowder
1
@TJCrowder Każdy, kto jest doświadczonym, profesjonalnym programistą JavaScript (i poza innymi językami), przed którym chodzę. Nie mogłem tego zrobić. ;)
Peter Lawrey
1
Powracając do tego, innym problemem jest to, że jeśli używasz GZIPOutputStream na pliku i nie wywołujesz jawnie finish, zostanie on wywołany w jego zamkniętej implementacji. To nie jest próba ... w końcu, więc jeśli zakończenie / opróżnianie zgłasza wyjątek, to uchwyt pliku źródłowego nigdy nie zostanie zamknięty.
robert_difalco
6

Jeśli wszystkie strumienie zostały utworzone, zamknięcie tylko najbardziej zewnętrznego jest w porządku.

Dokumentacja dotycząca Closeableinterfejsu stwierdza, że ​​metoda close:

Zamyka ten strumień i zwalnia wszystkie powiązane z nim zasoby systemowe.

Zwalnianie zasobów systemowych obejmuje strumienie zamykające.

Stwierdza również, że:

Jeśli strumień jest już zamknięty, wywołanie tej metody nie ma żadnego efektu.

Więc jeśli później je zamkniesz, nic złego się nie stanie.

Grzegorz Żur
źródło
2
Zakłada to brak błędów podczas konstruowania strumieni, co może, ale nie musi, być prawdziwe dla wymienionych, ale ogólnie nie jest wiarygodne.
TJ Crowder
6

Wolałbym raczej używać try(...)składni (Java 7), np

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}
Dmitrij Bychenko
źródło
4
Chociaż zgadzam się z tobą, możesz chcieć podkreślić korzyści płynące z tego podejścia i odpowiedzieć na pytanie, czy OP musi zamknąć strumienie dziecka / wewnętrzne
MadProgrammer
5

Będzie dobrze, jeśli zamkniesz tylko ostatni strumień - wywołanie zamknięcia zostanie również wysłane do strumieni bazowych.

Codeversum
źródło
1
Zobacz komentarz do odpowiedzi Grzegorza Żura.
TJ Crowder
5

Nie, najwyższy poziom Stream lub readerzapewni, że wszystkie podstawowe strumienie / czytniki są zamknięte.

Sprawdź implementacjęclose() metody swojego strumienia najwyższego poziomu.

TheLostMind
źródło