È necessario chiudere separatamente ogni OutputStream e Writer nidificati?


127

Sto scrivendo un pezzo di codice:

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

Devo chiudere tutti gli stream o i writer come il seguente?

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

O semplicemente chiudere l'ultimo flusso andrà bene?

bw.close();

1
Per la corrispondente domanda obsoleta Java 6, vedere stackoverflow.com/questions/884007/…
Raedwald

2
Nota che il tuo esempio ha un bug che può causare la perdita di dati, perché stai chiudendo i flussi non nell'ordine in cui li hai aperti. Quando si chiude un BufferedWriter, potrebbe essere necessario scrivere i dati bufferizzati nel flusso sottostante, che nel tuo esempio è già chiuso. Evitare questi problemi è un altro vantaggio degli approcci di prova con le risorse mostrati nelle risposte.
Joe23,

Risposte:


150

Supponendo che tutti i flussi vengono creati va bene, sì, proprio la chiusura bwva bene con quelle implementazioni di flusso ; ma questo è un grande presupposto.

Utilizzerei try-with-resources ( tutorial ) in modo tale che eventuali problemi di costruzione dei flussi successivi che generano eccezioni non lascino in sospeso i flussi precedenti e quindi non si debba fare affidamento sull'implementazione del flusso che ha la chiamata per chiudere il flusso sottostante:

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

Nota che non chiami più closeaffatto.

Nota importante : per fare in modo che le risorse di prova li chiudano, è necessario assegnare i flussi alle variabili mentre le si apre, non è possibile utilizzare l'annidamento. Se si utilizza l'annidamento, un'eccezione durante la costruzione di uno dei flussi successivi (ad esempio, GZIPOutputStream) lascerà aperto qualsiasi flusso creato dalle chiamate nidificate. Da JLS § 14.20.3 :

Un'istruzione try-with-resources è parametrizzata con variabili (note come risorse) che vengono inizializzate prima dell'esecuzione del tryblocco e chiuse automaticamente, nell'ordine inverso rispetto a quello in cui sono state inizializzate, dopo l'esecuzione del tryblocco.

Nota la parola "variabili" (la mia enfasi) .

Ad esempio, non farlo:

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

... perché un'eccezione dal GZIPOutputStream(OutputStream)costruttore (che dice che potrebbe essere lanciata IOExceptione scrive un'intestazione nel flusso sottostante) lascerebbe FileOutputStreamaperta. Dal momento che alcune risorse hanno costruttori che possono essere lanciati e altri no, è una buona abitudine elencarle separatamente.

Possiamo ricontrollare la nostra interpretazione di quella sezione JLS con questo programma:

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

... che ha l'output:

Esempio di costruzione $ InnerMost
Esempio di costruzione $ Middle
Esempio di costruzione $ OuterMost
In blocco di cattura
Nel blocco finalmente
Alla fine del principale

Si noti che non ci sono chiamate closelì.

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

quindi riceviamo le closechiamate appropriate :

Esempio di costruzione $ InnerMost
Esempio di costruzione $ Middle
Esempio di costruzione $ OuterMost
Esempio $ Medio chiuso
Esempio $ InnerMost chiuso
Esempio $ InnerMost chiuso
In blocco di cattura
Nel blocco finalmente
Alla fine del principale

(Sì, due chiamate a InnerMost#closesono corrette; una proviene da Middle, l'altra da prova con risorse.)


7
+1 per notare che possono essere generate eccezioni durante la costruzione dei flussi, anche se noterò realisticamente che si otterrà un'eccezione di memoria insufficiente o qualcosa di altrettanto grave (a quel punto non importa davvero se chiudi i tuoi stream, perché l'applicazione sta per uscire), o sarà GZIPOutputStream a generare una IOException; il resto dei costruttori non ha eccezioni verificate e non vi sono altre circostanze che possano produrre un'eccezione di runtime.
Jules,

5
@Jules: Sì, per questi flussi specifici, davvero. Si tratta più di buone abitudini.
TJ Crowder,

2
@PeterLawrey: non sono assolutamente d'accordo con l'uso di cattive abitudini o non a seconda dell'implementazione del flusso. :-) Questa non è una distinzione YAGNI / no-YAGNI, si tratta di schemi che rendono il codice affidabile.
TJ Crowder,

2
@PeterLawrey: Non c'è nulla di cui sopra nel non fidarsi java.io. Alcuni flussi - generalizzando, alcune risorse - vengono lanciati dai costruttori. Pertanto, a mio avviso, assicurarsi che più risorse vengano aperte individualmente in modo da poterle chiudere in modo affidabile se una successiva risorsa viene solo una buona abitudine. Puoi scegliere di non farlo se non sei d'accordo, va bene.
TJ Crowder,

2
@PeterLawrey: Quindi sostenete di prendere il tempo di esaminare il codice sorgente di un'implementazione per qualcosa che documenta un'eccezione, caso per caso, e poi dicendo "Oh, beh, in realtà non getta, quindi. .. "e salvando alcuni caratteri di battitura? Ci separiamo lì, alla grande. :-) Inoltre, ho appena guardato, e questo non è teorico: GZIPOutputStreamil costruttore scrive un header nel flusso. E così può lanciare. Quindi ora la posizione è se penso che valga la pena preoccuparsi di provare a chiudere lo stream dopo aver scritto. Sì: l'ho aperto, dovrei almeno provare a chiuderlo.
TJ Crowder,

12

Puoi chiudere lo stream più esterno, infatti non è necessario conservare tutti gli stream racchiusi e puoi usare Java 7 try-with-resources.

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

Se ti iscrivi a YAGNI, o non ne avrai bisogno, dovresti solo aggiungere il codice di cui hai effettivamente bisogno. Non dovresti aggiungere codice di cui potresti aver bisogno ma in realtà non fa nulla di utile.

Prendi questo esempio e immagina cosa potrebbe andare storto se non lo facessi e quale sarebbe l'impatto?

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

Cominciamo con FileOutputStream che chiama openper fare tutto il lavoro reale.

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

Se il file non viene trovato, non ci sono risorse sottostanti da chiudere, quindi chiuderlo non farà alcuna differenza. Se il file esiste, dovrebbe lanciare FileNotFoundException. Quindi non c'è nulla da guadagnare provando a chiudere la risorsa da questa sola riga.

Il motivo per cui è necessario chiudere il file è quando il file viene aperto correttamente, ma in seguito viene visualizzato un errore.

Diamo un'occhiata al prossimo stream GZIPOutputStream

C'è un codice che può generare un'eccezione

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

Questo scrive l'intestazione del file. Ora sarebbe molto insolito per te essere in grado di aprire un file per la scrittura ma non essere in grado di scrivere anche 8 byte su di esso, ma immaginiamo che ciò possa accadere e non chiudiamo il file in seguito. Cosa succede a un file se non è chiuso?

Non si ottengono scritture non cancellate, vengono scartate e in questo caso, non vi sono byte scritti correttamente nello stream che non sono comunque bufferizzati a questo punto. Ma un file che non è chiuso non vive per sempre, invece ha 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();
        }
    }
}

Se non chiudi affatto un file, viene comunque chiuso, ma non immediatamente (e come ho detto, i dati lasciati in un buffer andranno persi in questo modo, ma a questo punto non ce ne sono)

Qual è la conseguenza di non chiudere immediatamente il file? In condizioni normali, potresti perdere alcuni dati e potenzialmente esaurire i descrittori di file. Ma se hai un sistema in cui puoi creare file ma non puoi scrivere nulla, hai un problema più grande. vale a dire, è difficile immaginare il motivo per cui si sta ripetutamente tentando di creare questo file nonostante si stia fallendo.

Sia OutputStreamWriter che BufferedWriter non generano IOException nei loro costruttori, quindi non chiariscono quale problema potrebbero causare. Nel caso di BufferedWriter, potresti ottenere un OutOfMemoryError. In questo caso attiverà immediatamente un GC, che come abbiamo visto chiuderà comunque il file.


1
Vedi la risposta di TJ Crowder per le situazioni in cui ciò potrebbe non riuscire.
TimK,

@TimK puoi fornire un esempio di dove viene creato il file, ma lo stream in seguito fallisce e quali sono le conseguenze. Il rischio di fallimento è estremamente basso e l'impatto è banale. Non è necessario rendere il più complicato di quanto debba essere.
Peter Lawrey,

1
GZIPOutputStream(OutputStream)documenti IOExceptione, guardando la fonte, in realtà scrive un'intestazione. Quindi non è teorico, quel costruttore può lanciare. Potresti sentire che va bene lasciare FileOutputStreamaperto il sottostante dopo averlo scritto. Io non.
TJ Crowder,

1
@TJCrowder Chiunque sia uno sviluppatore JavaScript professionista con esperienza (e altre lingue oltre) mi tolgo il cappello. Non potevo farlo. ;)
Peter Lawrey,

1
Solo per rivisitare questo, l'altro problema è che se si utilizza un GZIPOutputStream su un file e non si chiama esplicitamente finish, questo verrà chiamato nella sua stretta implementazione. Questo non è un tentativo ... finalmente, quindi se il finish / flush genera un'eccezione, l'handle del file sottostante non verrà mai chiuso.
robert_difalco,

6

Se tutti i flussi sono stati istanziati, chiudere solo il più esterno va bene.

La documentazione Closeablesull'interfaccia afferma che il metodo di chiusura:

Chiude questo flusso e rilascia tutte le risorse di sistema ad esso associate.

Le risorse di sistema di rilascio includono flussi di chiusura.

Dichiara inoltre che:

Se il flusso è già chiuso, invocare questo metodo non ha alcun effetto.

Quindi, se li chiudi esplicitamente in seguito, non accadrà nulla di sbagliato.


2
Ciò non presuppone errori nella costruzione dei flussi, il che può essere vero o no per quelli elencati, ma in generale non è affidabile.
TJ Crowder,

6

Preferirei usare la try(...)sintassi (Java 7), ad es

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

4
Mentre sono d'accordo con te, potresti voler evidenziare i vantaggi di questo approccio e rispondere al punto se l'OP deve chiudere i flussi bambino / interiore
MadProgrammer

5

Andrà bene se chiudi solo l'ultimo flusso: anche la chiamata di chiusura verrà inviata ai flussi sottostanti.


1
Vedi il commento sulla risposta di Grzegorz Żur.
TJ Crowder,

5

No, il livello più alto Streamo readergarantirà la chiusura di tutti i flussi / lettori sottostanti .

Controlla l' implementazione del close()metodo del tuo flusso di livello più alto.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.