Ist es notwendig, jeden verschachtelten OutputStream und Writer separat zu schließen?

127

Ich schreibe einen Code:

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

Muss ich jeden Stream oder Writer wie folgt schließen?

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

Oder ist es in Ordnung, nur den letzten Stream zu schließen?

bw.close();
Adon Smith
quelle
1
Für die entsprechende veraltete Java 6-Frage siehe stackoverflow.com/questions/884007/…
Raedwald
2
Beachten Sie, dass Ihr Beispiel einen Fehler aufweist, der zu Datenverlust führen kann, da Sie die Streams nicht in der Reihenfolge schließen, in der Sie sie geöffnet haben. Beim Schließen von a müssen BufferedWritermöglicherweise gepufferte Daten in den zugrunde liegenden Stream geschrieben werden, der in Ihrem Beispiel bereits geschlossen ist. Das Vermeiden dieser Probleme ist ein weiterer Vorteil der in den Antworten gezeigten Try-with-Resource- Ansätze.
Joe23

Antworten:

150

Angenommen, alle Streams werden in Ordnung erstellt. Ja, nur das Schließen bwist für diese Stream-Implementierungen in Ordnung . aber das ist eine große Annahme.

Ich würde Try-with-Resources ( Tutorial ) verwenden, damit beim Erstellen der nachfolgenden Streams, die Ausnahmen auslösen, die vorherigen Streams nicht hängen bleiben und Sie sich nicht darauf verlassen müssen, dass die Stream-Implementierung den Aufruf zum Schließen hat der zugrunde liegende Stream:

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

Beachten Sie, dass Sie überhaupt nicht mehr anrufen close.

Wichtiger Hinweis : Damit Try-with-Resources sie schließen kann, müssen Sie die Streams beim Öffnen Variablen zuweisen. Sie können die Verschachtelung nicht verwenden. Wenn Sie die Verschachtelung verwenden, lässt eine Ausnahme während der Erstellung eines der späteren Streams (z. B. GZIPOutputStream) jeden Stream offen, der durch die darin enthaltenen verschachtelten Aufrufe erstellt wurde. Aus JLS §14.20.3 :

Eine Try-with-Resources-Anweisung wird mit Variablen (so genannten Ressourcen) parametrisiert , die vor der Ausführung des tryBlocks initialisiert und nach der Ausführung des tryBlocks automatisch in der umgekehrten Reihenfolge geschlossen werden, in der sie initialisiert wurden .

Beachten Sie das Wort "Variablen" (meine Betonung) .

ZB mach das nicht:

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

... weil eine Ausnahme vom GZIPOutputStream(OutputStream)Konstruktor (der besagt, dass er möglicherweise ausgelöst IOExceptionwird und einen Header in den zugrunde liegenden Stream schreibt) das FileOutputStreamoffen lassen würde. Da einige Ressourcen Konstruktoren haben, die möglicherweise ausgelöst werden, andere jedoch nicht, ist es eine gute Angewohnheit, sie nur separat aufzulisten.

Mit diesem Programm können wir unsere Interpretation dieses JLS-Abschnitts überprüfen:

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

... was die Ausgabe hat:

Konstruktionsbeispiel $ InnerMost
Konstruktionsbeispiel $ Middle
Konstruktionsbeispiel $ OuterMost
Im Fangblock
In endlich blockieren
Am Ende der Hauptleitung

Beachten Sie, dass dort keine Anrufe closeeingehen.

Wenn wir Folgendes beheben 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");
}

dann bekommen wir die entsprechenden closeAnrufe:

Konstruktionsbeispiel $ InnerMost
Konstruktionsbeispiel $ Middle
Konstruktionsbeispiel $ OuterMost
Beispiel $ Middle geschlossen
Beispiel $ InnerMost geschlossen
Beispiel $ InnerMost geschlossen
Im Fangblock
In endlich blockieren
Am Ende der Hauptleitung

(Ja, zwei Aufrufe von InnerMost#closesind korrekt; einer stammt von Middle, der andere von try-with-resources.)

TJ Crowder
quelle
7
+1 für die Feststellung, dass während der Erstellung der Streams Ausnahmen ausgelöst werden können, obwohl ich realistisch feststellen werde, dass Sie entweder eine Ausnahme wegen Speichermangels oder etwas ähnlich Ernstes erhalten (an diesem Punkt spielt es wirklich keine Rolle Wenn Sie Ihre Streams schließen, weil Ihre Anwendung gerade beendet wird, wird GZIPOutputStream eine IOException auslösen. Die übrigen Konstruktoren haben keine geprüften Ausnahmen, und es gibt keine anderen Umstände, die wahrscheinlich zu einer Laufzeitausnahme führen.
Jules
5
@ Jules: Ja, für diese spezifischen Streams in der Tat. Es geht mehr um gute Gewohnheiten.
TJ Crowder
2
@PeterLawrey: Ich bin absolut nicht einverstanden damit, schlechte Gewohnheiten zu verwenden oder nicht, abhängig von der Stream-Implementierung. :-) Dies ist keine Unterscheidung zwischen YAGNI und No-YAGNI, sondern es geht um Muster, die für zuverlässigen Code sorgen.
TJ Crowder
2
@PeterLawrey: Es gibt auch nichts darüber, nicht zu vertrauen java.io. Einige Streams - verallgemeinernd, einige Ressourcen - werden von Konstruktoren ausgegeben. Daher ist es meiner Ansicht nach nur eine gute Angewohnheit, sicherzustellen, dass mehrere Ressourcen einzeln geöffnet werden, damit sie zuverlässig geschlossen werden können, wenn eine nachfolgende Ressource ausgelöst wird. Sie können sich dafür entscheiden, es nicht zu tun, wenn Sie nicht einverstanden sind, das ist in Ordnung.
TJ Crowder
2
@PeterLawrey: Sie empfehlen also, sich die Zeit zu nehmen, den Quellcode einer Implementierung von Fall zu Fall auf etwas zu untersuchen, das eine Ausnahme dokumentiert, und dann zu sagen: "Oh, nun, es wird nicht wirklich geworfen." .. "und ein paar Zeichen der Eingabe speichern? Wir trennen uns dort, große Zeit. :-) Außerdem habe ich nur nachgesehen, und das ist nicht theoretisch: GZIPOutputStreamDer Konstruktor schreibt einen Header in den Stream. Und so kann es werfen. Nun ist die Position also, ob es sich meiner Meinung nach lohnt, zu versuchen , den Stream nach dem Schreiben zu schließen. Ja, ich habe es geöffnet, ich sollte zumindest versuchen, es zu schließen.
TJ Crowder
12

Sie können den äußersten Stream schließen. Tatsächlich müssen Sie nicht alle umschlossenen Streams beibehalten, und Sie können Java 7 Try-with-Resources verwenden.

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

Wenn Sie YAGNI abonnieren oder es nicht brauchen, sollten Sie nur den Code hinzufügen, den Sie tatsächlich benötigen. Sie sollten keinen Code hinzufügen, von dem Sie sich vorstellen, dass Sie ihn benötigen, aber in Wirklichkeit tun Sie nichts Nützliches.

Nehmen Sie dieses Beispiel und stellen Sie sich vor, was möglicherweise schief gehen könnte, wenn Sie dies nicht tun würden und welche Auswirkungen dies hätte.

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

Beginnen wir mit FileOutputStream, der aufruft, openum die gesamte eigentliche Arbeit zu erledigen.

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

Wenn die Datei nicht gefunden wird, muss keine zugrunde liegende Ressource geschlossen werden, sodass das Schließen keinen Unterschied macht. Wenn die Datei vorhanden ist, sollte eine FileNotFoundException ausgelöst werden. Es gibt also nichts zu gewinnen, wenn versucht wird, die Ressource allein aus dieser Zeile zu schließen.

Der Grund, warum Sie die Datei schließen müssen, ist, wenn die Datei erfolgreich geöffnet wurde, aber später eine Fehlermeldung angezeigt wird.

Schauen wir uns den nächsten Stream an GZIPOutputStream

Es gibt Code, der eine Ausnahme auslösen kann

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

Dies schreibt den Header der Datei. Jetzt wäre es sehr ungewöhnlich, dass Sie eine Datei zum Schreiben öffnen können, aber nicht einmal 8 Bytes darauf schreiben können. Stellen Sie sich jedoch vor, dass dies passieren könnte und wir die Datei danach nicht schließen. Was passiert mit einer Datei, wenn sie nicht geschlossen ist?

Sie erhalten keine nicht geleerten Schreibvorgänge, sie werden verworfen, und in diesem Fall gibt es keine erfolgreich geschriebenen Bytes in den Stream, die zu diesem Zeitpunkt ohnehin nicht gepuffert sind. Aber eine Datei, die nicht geschlossen ist, lebt nicht für immer, sondern 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();
        }
    }
}

Wenn Sie eine Datei überhaupt nicht schließen, wird sie trotzdem geschlossen, nur nicht sofort (und wie gesagt, Daten, die in einem Puffer verbleiben, gehen auf diese Weise verloren, aber zu diesem Zeitpunkt gibt es keine).

Was ist die Folge davon, dass die Datei nicht sofort geschlossen wird? Unter normalen Bedingungen verlieren Sie möglicherweise einige Daten, und Ihnen gehen möglicherweise die Dateideskriptoren aus. Wenn Sie jedoch ein System haben, in dem Sie Dateien erstellen, aber nichts darauf schreiben können, haben Sie ein größeres Problem. Das heißt, es ist schwer vorstellbar, warum Sie wiederholt versuchen, diese Datei zu erstellen, obwohl Sie einen Fehler machen.

Sowohl OutputStreamWriter als auch BufferedWriter lösen keine IOException in ihren Konstruktoren aus, sodass nicht klar ist, welches Problem sie verursachen würden. Im Fall von BufferedWriter könnten Sie einen OutOfMemoryError erhalten. In diesem Fall wird sofort ein GC ausgelöst, der, wie wir gesehen haben, die Datei trotzdem schließt.

Peter Lawrey
quelle
1
In der Antwort von TJ Crowder finden Sie Situationen, in denen dies fehlschlagen könnte.
TimK
@TimK können Sie ein Beispiel dafür geben, wo die Datei erstellt wird, der Stream jedoch später fehlschlägt und welche Konsequenzen dies hat. Das Ausfallrisiko ist äußerst gering und die Auswirkungen sind gering. Sie müssen das nicht komplizierter machen, als es sein muss.
Peter Lawrey
1
GZIPOutputStream(OutputStream)Dokumente IOExceptionund schreibt, wenn man sich die Quelle ansieht, tatsächlich einen Header. Es ist also nicht theoretisch, dass der Konstruktor werfen kann. Möglicherweise ist es in Ordnung, den zugrunde liegenden FileOutputStreamWert nach dem Schreiben offen zu lassen. Ich nicht.
TJ Crowder
1
@TJCrowder Jeder, der ein erfahrener professioneller JavaScript-Entwickler (und andere Sprachen) ist, dem ich den Hut abnehme. Ich konnte es nicht tun ;)
Peter Lawrey
1
Um dies noch einmal zu wiederholen, besteht das andere Problem darin, dass wenn Sie einen GZIPOutputStream für eine Datei verwenden und finish nicht explizit aufrufen, dieser in seiner engen Implementierung aufgerufen wird. Dies ist kein Versuch ... Wenn das Finish / Flush eine Ausnahme auslöst, wird das zugrunde liegende Dateihandle niemals geschlossen.
robert_difalco
6

Wenn alle Streams instanziiert wurden, ist es in Ordnung, nur den äußersten zu schließen.

In der Dokumentation zur CloseableSchnittstelle heißt es: Schließen Sie die Methode:

Schließt diesen Stream und gibt alle damit verbundenen Systemressourcen frei.

Das Freigeben von Systemressourcen umfasst das Schließen von Streams.

Es heißt auch, dass:

Wenn der Stream bereits geschlossen ist, hat das Aufrufen dieser Methode keine Auswirkung.

Wenn Sie sie also später explizit schließen, passiert nichts Falsches.

Grzegorz Żur
quelle
2
Dies setzt keine Fehler beim Erstellen der Streams voraus , die für die aufgelisteten möglicherweise zutreffen oder nicht, aber im Allgemeinen nicht zuverlässig zutreffen.
TJ Crowder
6

Ich würde lieber try(...)Syntax (Java 7) verwenden, z

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}
Dmitry Bychenko
quelle
4
Obwohl ich Ihnen zustimme, möchten Sie vielleicht den Nutzen dieses Ansatzes hervorheben und den Punkt beantworten, wenn das OP die
untergeordneten
5

Es ist in Ordnung, wenn Sie nur den letzten Stream schließen - der Abschlussaufruf wird auch an die zugrunde liegenden Streams gesendet.

Codeversum
quelle
1
Siehe Kommentar zu Grzegorz Żurs Antwort.
TJ Crowder
5

Nein, die oberste Ebene Streamoder stellt readersicher, dass alle zugrunde liegenden Streams / Reader geschlossen sind.

Überprüfen Sie die close()Methode Umsetzung Ihrer obersten Ebene Stream.

TheLostMind
quelle