Idioma corretto per la gestione di più risorse concatenate nel blocco try-with-resources?


168

La sintassi di prova con risorse di Java 7 (nota anche come blocco ARM ( Gestione automatica delle risorse )) è piacevole, breve e semplice quando si utilizza una sola AutoCloseablerisorsa. Tuttavia, io non sono sicuro di quello che è il linguaggio corretto quando ho bisogno di dichiarare più risorse che sono dipendenti l'uno dall'altro, per esempio, una FileWritere una BufferedWriterche lo avvolge. Ovviamente, questa domanda riguarda qualsiasi caso in cui alcune AutoCloseablerisorse vengano spostate, non solo queste due classi specifiche.

Ho trovato le tre seguenti alternative:

1)

Il linguaggio ingenuo che ho visto è di dichiarare solo il wrapper di livello superiore nella variabile gestita da ARM:

static void printToFile1(String text, File file) {
    try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Questo è bello e corto, ma è rotto. Poiché il sottostante FileWriternon è dichiarato in una variabile, non verrà mai chiuso direttamente nel finallyblocco generato . Sarà chiuso solo attraverso il closemetodo di avvolgimento BufferedWriter. Il problema è che se un'eccezione viene generata dal bwcostruttore del, closenon verrà chiamata e quindi il sottostante FileWriter non verrà chiuso .

2)

static void printToFile2(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
            BufferedWriter bw = new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Qui, sia la risorsa sottostante che quella di wrapping sono dichiarate nelle variabili gestite da ARM, quindi entrambe saranno sicuramente chiuse, ma il sottostante fw.close() sarà chiamato due volte : non solo direttamente, ma anche attraverso il wrapping bw.close().

Questo non dovrebbe essere un problema per queste due classi specifiche che entrambe implementano Closeable(che è un sottotipo di AutoCloseable), il cui contratto stabilisce che closesono consentite più chiamate :

Chiude questo flusso e rilascia tutte le risorse di sistema ad esso associate. Se il flusso è già chiuso, invocare questo metodo non ha alcun effetto.

Tuttavia, in un caso generale, posso avere risorse che implementano solo AutoCloseable(e non Closeable), il che non garantisce che closepossano essere chiamate più volte:

A differenza del metodo close di java.io.Closeable, questo metodo close non deve essere idempotente. In altre parole, chiamare questo metodo vicino più di una volta può avere qualche effetto collaterale visibile, a differenza di Closeable.close, che è richiesto di non avere effetto se chiamato più di una volta. Tuttavia, gli implementatori di questa interfaccia sono fortemente incoraggiati a rendere i loro metodi vicini idempotenti.

3)

static void printToFile3(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        BufferedWriter bw = new BufferedWriter(fw);
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Questa versione dovrebbe essere teoricamente corretta, perché solo fwrappresenta una risorsa reale che deve essere ripulita. Lo bwstesso non detiene alcuna risorsa, delega solo a fw, quindi dovrebbe essere sufficiente chiudere solo il sottostante fw.

D'altra parte, la sintassi è un po 'irregolare e, inoltre, Eclipse emette un avviso, che credo sia un falso allarme, ma è comunque un avvertimento che si deve affrontare:

Perdita di risorse: 'bw' non viene mai chiuso


Quindi, quale approccio scegliere? O ho perso qualche altro idioma che è quello corretto ?


4
Naturalmente, se il costruttore di FileWriter sottostante genera un'eccezione, non viene nemmeno aperto e tutto è OK. Di cosa parla il primo esempio, cosa succede se viene creato FileWriter, ma il costruttore di BufferedWriter genera un'eccezione.
Natix,

6
Vale la pena notare che BufferedWriter non genererà un'eccezione. C'è un esempio a cui puoi pensare se questa domanda non è puramente accademica.
Peter Lawrey,

10
@PeterLawrey Sì, hai ragione sul fatto che il costruttore di BufferedWriter in questo scenario molto probabilmente non genererà un'eccezione, ma come ho sottolineato, questa domanda riguarda qualsiasi risorsa in stile decoratore. Ma per esempio public BufferedWriter(Writer out, int sz)può lanciare un IllegalArgumentException. Inoltre, posso estendere BufferedWriter con una classe che potrebbe lanciare qualcosa dal suo costruttore o creare un wrapper personalizzato di cui ho bisogno.
Natix,

5
Il BufferedWritercostruttore può facilmente generare un'eccezione. OutOfMemoryErrorè probabilmente il più comune in quanto alloca una buona porzione di memoria per il buffer (anche se può indicare che si desidera riavviare l'intero processo). / È necessario il flushtuo BufferedWriterse non si chiude e si desidera conservare i contenuti (in genere solo il caso di non eccezione). FileWriterraccoglie qualunque sia la codifica di file "predefinita" - è meglio essere espliciti.
Tom Hawtin - tackline il

10
@Natix Vorrei che tutte le domande in SO siano ben studiate e chiare come questa. Vorrei poter votare questo 100 volte.
Geek,

Risposte:


75

Ecco la mia opinione sulle alternative:

1)

try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
    bw.write(text);
}

Per me, la cosa migliore che è arrivata a Java dal C ++ tradizionale 15 anni fa è stata che potevi fidarti del tuo programma. Anche se le cose sono nel fango e vanno male, cosa che spesso fanno, voglio che il resto del codice sia sul miglior comportamento e sull'odore delle rose. In effetti, BufferedWriterpotrebbe generare un'eccezione qui. A corto di memoria non sarebbe insolito, per esempio. Per altri decoratori, sai quale delle java.ioclassi wrapper genera un'eccezione controllata dai loro costruttori? Io non. Non rende molto comprensibile la comprensione del codice se si fa affidamento su quel tipo di conoscenza oscura.

Inoltre c'è la "distruzione". Se si verifica una condizione di errore, probabilmente non si desidera scaricare la spazzatura in un file che deve essere eliminato (codice per quello non mostrato). Anche se, ovviamente, l'eliminazione del file è un'altra operazione interessante da fare come gestione degli errori.

Generalmente si desidera che i finallyblocchi siano il più corti e affidabili possibile. L'aggiunta di vampate non aiuta questo obiettivo. Per molte versioni alcune delle classi di buffering nel JDK avevano un bug in cui non veniva chiamata un'eccezione flushdall'interno closecausata closesull'oggetto decorato. Anche se è stato risolto da tempo, aspettalo da altre implementazioni.

2)

try (
    FileWriter fw = new FileWriter(file);
    BufferedWriter bw = new BufferedWriter(fw)
) {
    bw.write(text);
}

Stiamo ancora scaricando il blocco finale implicito (ora con ripetuto close- questo peggiora quando aggiungi più decoratori), ma la costruzione è sicura e dobbiamo implicare finalmente i blocchi, quindi anche un fallimento flushnon impedisce il rilascio di risorse.

3)

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
}

C'è un bug qui. Dovrebbe essere:

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
    bw.flush();
}

Alcuni decoratori mal implementati sono infatti risorse e dovranno essere chiusi in modo affidabile. Inoltre, alcuni stream potrebbero dover essere chiusi in un modo particolare (forse stanno facendo compressione e hanno bisogno di scrivere bit per finire, e non possono semplicemente svuotare tutto.

Verdetto

Sebbene 3 sia una soluzione tecnicamente superiore, i motivi di sviluppo del software ne fanno la scelta migliore. Tuttavia, try-with-resource è ancora una soluzione inadeguata e si dovrebbe attenersi al linguaggio Execute Around , che dovrebbe avere una sintassi più chiara con le chiusure in Java SE 8.


4
Nella versione 3, come fai a sapere che bw non richiede la sua chiusura per essere chiamato? E anche se puoi esserne sicuro, non è anche quella "conoscenza oscura" come hai menzionato per la versione 1?
TimK,

3
"Le ragioni di sviluppo del software rendono 2 la scelta migliore " Puoi spiegare questa dichiarazione in modo più dettagliato?
Duncan Jones,

8
Puoi fare un esempio dell '"Esegui intorno al linguaggio con le chiusure"
Markus,

2
Puoi spiegare cos'è "una sintassi più chiara con le chiusure in Java SE 8"?
petertc,

1
Esempio di "Esegui Intorno linguaggio" è qui: stackoverflow.com/a/342016/258772
MRTS

20

Il primo stile è quello suggerito da Oracle .BufferedWriternon genera eccezioni verificate, quindi se viene generata un'eccezione, il programma non dovrebbe ripristinarsi da esso, facendo sì che il recupero delle risorse sia per lo più controverso.

Soprattutto perché potrebbe accadere in un thread, con il thread che muore ma il programma continua ancora - diciamo, c'è stata un'interruzione temporanea della memoria che non è stata abbastanza lunga da compromettere seriamente il resto del programma. È un caso piuttosto angolare, tuttavia, e se succede abbastanza spesso da rendere la perdita di risorse un problema, il tentativo con le risorse è l'ultimo dei tuoi problemi.


2
È anche l'approccio consigliato nella terza edizione di Effective Java.
shmosel,

5

Opzione 4

Cambia le tue risorse in Chiudibile, non Chiudibile automaticamente se puoi. Il fatto che i costruttori possano essere concatenati implica che non è inaudito chiudere due volte la risorsa. (Questo era vero anche prima di ARM.) Maggiori informazioni su questo di seguito.

Opzione 5

Non usare ARM e il codice con molta attenzione per assicurarsi che close () non venga chiamato due volte!

Opzione 6

Non usare ARM e fai finalmente le tue chiamate close () in una modalità try / catch.

Perché non penso che questo problema sia unico per ARM

In tutti questi esempi, le chiamate infine close () dovrebbero trovarsi in un blocco catch. Tralasciato per leggibilità.

Non va bene perché fw può essere chiuso due volte. (che va bene per FileWriter ma non nel tuo esempio ipotetico):

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( fw != null ) fw.close();
  if ( bw != null ) bw.close();
}

Non va bene perché fw non si è chiuso se l'eccezione sulla costruzione di BufferedWriter. (di nuovo, non può succedere, ma nel tuo esempio ipotetico):

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( bw != null ) bw.close();
}

3

Volevo solo basarmi sul suggerimento di Jeanne Boyarsky di non usare ARM ma di assicurarmi che FileWriter fosse sempre chiuso esattamente una volta. Non pensare che ci siano problemi qui ...

FileWriter fw = null;
BufferedWriter bw = null;
try {
    fw = new FileWriter(file);
    bw = new BufferedWriter(fw);
    bw.write(text);
} finally {
    if (bw != null) bw.close();
    else if (fw != null) fw.close();
}

Immagino che dal momento che ARM è solo zucchero sintattico, non possiamo sempre usarlo per sostituire finalmente i blocchi. Proprio come non possiamo sempre usare un ciclo for-each per fare qualcosa che è possibile con gli iteratori.


5
Se sia il tuo tryche i finallyblocchi generano eccezioni, questo costrutto perderà il primo (e potenzialmente più utile).
rxg,

3

Per concordare con i commenti precedenti: è più semplice (2) utilizzare le Closeablerisorse e dichiararle in ordine nella clausola di prova con le risorse. Se ce l' AutoCloseablehai, puoi avvolgerli in un'altra classe (nidificata) che controlla solo quella closechiamata una sola volta (modello di facciata), ad es private bool isClosed;. In pratica, anche Oracle (1) incatena i costruttori e non gestisce correttamente le eccezioni durante la catena.

In alternativa, è possibile creare manualmente una risorsa concatenata, utilizzando un metodo factory statico; questo incapsula la catena e gestisce la pulizia in caso di errore parziale:

static BufferedWriter createBufferedWriterFromFile(File file)
  throws IOException {
  // If constructor throws an exception, no resource acquired, so no release required.
  FileWriter fileWriter = new FileWriter(file);
  try {
    return new BufferedWriter(fileWriter);  
  } catch (IOException newBufferedWriterException) {
    try {
      fileWriter.close();
    } catch (IOException closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newBufferedWriterException.addSuppressed(closeException);
    }
    throw newBufferedWriterException;
  }
}

È quindi possibile utilizzarlo come singola risorsa in una clausola try-with-resources:

try (BufferedWriter writer = createBufferedWriterFromFile(file)) {
  // Work with writer.
}

La complessità deriva dalla gestione di più eccezioni; altrimenti è solo "risorse vicine che hai acquisito finora". Una pratica comune sembra essere l'inizializzazione della variabile che contiene l'oggetto che contiene la risorsa null(qui fileWriter), quindi includere un controllo nullo nella pulizia, ma ciò sembra superfluo: se il costruttore fallisce, non c'è nulla da ripulire, quindi possiamo solo far propagare quell'eccezione, il che semplifica un po 'il codice.

Probabilmente potresti farlo genericamente:

static <T extends AutoCloseable, U extends AutoCloseable, V>
    T createChainedResource(V v) throws Exception {
  // If constructor throws an exception, no resource acquired, so no release required.
  U u = new U(v);
  try {
    return new T(u);  
  } catch (Exception newTException) {
    try {
      u.close();
    } catch (Exception closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newTException.addSuppressed(closeException);
    }
    throw newTException;
  }
}

Allo stesso modo, puoi concatenare tre risorse, ecc.

A parte matematicamente, potresti anche incatenare tre volte concatenando due risorse alla volta, e sarebbe associativo, il che significa che otterrai lo stesso oggetto in caso di successo (perché i costruttori sono associativi) e le stesse eccezioni in caso di fallimento in uno qualsiasi dei costruttori. Supponendo che tu abbia aggiunto una S alla catena sopra (quindi inizi con una V e finisci con una S , applicando a sua volta U , T e S ), otterrai lo stesso sia se prima fai la catena S e T , poi U , corrispondente a (ST) U , o se per la prima volta hai incatenato T e U , quindiS , corrispondente a S (TU) . Tuttavia, sarebbe più chiaro scrivere una catena tripla esplicita in una singola funzione di fabbrica.


Sto raccogliendo correttamente che è ancora necessario utilizzare try-with-resource, come in try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }?
ErikE

@ErikE Sì, dovresti comunque utilizzare try-with-resources, ma dovrai utilizzare solo una singola funzione per la risorsa concatenata: la funzione factory incapsula il concatenamento. Ho aggiunto un esempio di utilizzo; Grazie!
Nils von Barth,

2

Poiché le risorse sono nidificate, le clausole di prova dovrebbero essere anche:

try (FileWriter fw=new FileWriter(file)) {
    try (BufferedWriter bw=new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
} catch (IOException ex) {
    // handle ex
}

5
Questo è praticamente simile al mio secondo esempio. Se non si verifica alcuna eccezione, FileWriter closeverrà chiamato due volte.
Natix,

0

Direi di non usare ARM e di continuare con Closeable. Usa un metodo come

public void close(Closeable... closeables) {
    for (Closeable closeable: closeables) {
       try {
           closeable.close();
         } catch (IOException e) {
           // you can't much for this
          }
    }

}

Inoltre, dovresti considerare di chiudere in BufferedWriterquanto non si tratta solo di delegare il vicino FileWriter, ma fa anche una pulizia flushBuffer.


0

La mia soluzione è quella di fare un refactoring "metodo di estrazione", come segue:

static AutoCloseable writeFileWriter(FileWriter fw, String txt) throws IOException{
    final BufferedWriter bw  = new BufferedWriter(fw);
    bw.write(txt);
    return new AutoCloseable(){

        @Override
        public void close() throws IOException {
            bw.flush();
        }

    };
}

printToFile può essere scritto neanche

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        AutoCloseable w = writeFileWriter(fw, text);
        w.close();
    } catch (Exception ex) {
        // handle ex
    }
}

o

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
        AutoCloseable w = writeFileWriter(fw, text)){

    } catch (Exception ex) {
        // handle ex
    }
}

Per i progettisti di lib di classe, suggerirò loro di estendere l' AutoClosableinterfaccia con un metodo aggiuntivo per sopprimere la chiusura. In questo caso possiamo quindi controllare manualmente il comportamento ravvicinato.

Per i progettisti di lingue, la lezione è che aggiungere una nuova funzionalità potrebbe significare aggiungerne molte altre. In questo caso Java, ovviamente la funzionalità ARM funzionerà meglio con un meccanismo di trasferimento della proprietà delle risorse.

AGGIORNARE

Inizialmente il codice sopra richiede @SuppressWarningpoiché l' BufferedWriterinterno richiede la funzioneclose() .

Come suggerito da un commento, se flush()deve essere chiamato prima di chiudere lo scrittore, dobbiamo farlo prima di qualsiasi returnistruzione (implicita o esplicita) all'interno del blocco try. Al momento non c'è modo di garantire che il chiamante faccia questo, penso, quindi questo deve essere documentato writeFileWriter.

AGGIORNARE ANCORA

L'aggiornamento precedente @SuppressWarningnon è necessario poiché richiede che la funzione restituisca la risorsa al chiamante, pertanto non è necessario che venga chiusa. Sfortunatamente, questo ci riporta all'inizio della situazione: l'avvertimento è ora spostato sul lato chiamante.

Quindi, per risolvere correttamente questo, abbiamo bisogno di un personalizzato AutoClosableche ogni volta che si chiude, la sottolineatura BufferedWriterdeve essere modificata flush(). In realtà, questo ci mostra un altro modo per aggirare l'avvertimento, dal momento che BufferWriternon è mai chiuso in alcun modo.


L'avvertimento ha il suo significato: possiamo essere sicuri qui che i dati bwverranno effettivamente scritti? Si è tamponata adiecente, quindi ha solo scrivere su disco volte (quando il buffer è pieno e / o su flush()e close()metodi). Immagino che il flush()metodo dovrebbe essere chiamato. Ma poi non c'è bisogno di usare lo scrittore bufferizzato, comunque, quando lo scriviamo immediatamente in un batch. E se non aggiusti il ​​codice, potresti finire con i dati scritti nel file in un ordine sbagliato o addirittura non scritti affatto nel file.
Petr Janeček,

Se flush()viene richiesto di essere chiamato, deve accadere ogni volta che prima che il chiamante decida di chiudere il FileWriter. Quindi questo dovrebbe accadere printToFilesubito prima di ogni returns nel blocco try. Questo non deve far parte writeFileWritere quindi l'avvertimento non riguarda nulla all'interno di quella funzione, ma il chiamante di quella funzione. Se abbiamo un'annotazione, @LiftWarningToCaller("wanrningXXX")questo sarà di aiuto e simili.
Earth Engine
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.