Blocco finally in Java
Esegui codice di pulizia in Java con i blocchi finally che vengono sempre eseguiti, che sia stata lanciata un'eccezione o meno.
Un blocco finally viene eseguito indipendentemente da come esce il try — completamento normale, eccezione gestita, eccezione non gestita, o anche un return anticipato. Questa garanzia è il suo unico scopo: è qui che si inserisce il codice di pulizia che deve necessariamente essere eseguito, sempre. Chiudere un file handle, rilasciare un lock, ripristinare lo stato di un thread — qualsiasi cosa la cui assenza lascerebbe il programma in una condizione peggiore di quella iniziale.
La struttura
try {
// risky code
} catch (SomeException e) {
// optional — zero or more catches
} finally {
// always runs after the try (and any matching catch)
}È possibile abbinare finally a catch, a nessun catch (try { ... } finally { ... }), o a più catch. I blocchi si compongono liberamente.
Cosa significa "viene sempre eseguito"
Un blocco finally viene eseguito quando il controllo lascia il try, indipendentemente da come:
- Fine normale del blocco
try—finallyviene eseguito dopo l'ultima istruzione. - Un'eccezione lanciata da
try—finallyviene eseguito dopo che ilcatchcorrispondente termina (oppure, se nessun catch corrisponde, appena prima che l'eccezione si propaghi). returnall'interno ditryocatch—finallyviene eseguito prima che il return abbia effetto.breakocontinueche uscirebbe daltry—finallyviene eseguito prima del salto.
Gli unici modi per saltare finally sono: la JVM stessa termina (System.exit, un'interruzione di corrente, Runtime.halt), un ciclo infinito o un deadlock all'interno del try, oppure Thread.stop (deprecato proprio per questo motivo). Per tutto ciò che si scrive nel normale codice applicativo, finally è una garanzia assoluta.
try {
return computeAnswer(); // even though there's a return here,
} finally {
cleanup(); // this runs before the method actually returns
}A cosa serve finally
La risposta onesta è: pulizia delle risorse, quasi sempre. Prima che Java 7 introducesse try-with-resources, la struttura canonica era:
InputStream in = null;
try {
in = new FileInputStream(path);
// read from in...
} catch (IOException e) {
// handle
} finally {
if (in != null) {
try { in.close(); } catch (IOException ignored) { /* */ }
}
}Quel try/catch annidato attorno a close() nel finally è esattamente il tipo di rumore che try-with-resources è stato progettato per eliminare. Lo vedremo nel prossimo capitolo. Ma capire a cosa serve finally rende più sensato il costrutto più recente.
Oltre alle risorse, finally è utile per:
- Ripristinare lo stato condiviso modificato per la durata del
try— incrementare un contatore di profondità, cambiare un flag, scambiare unThreadLocal. - Rilasciare lock acquisiti manualmente (
Lock.lock()→try { ... } finally { lock.unlock(); }). - Fermare timer o chiudere transazioni che non implementano
AutoCloseable.
A cosa finally non serve
Non scrivere logica che produce risultati in finally. Il blocco viene eseguito indipendentemente dall'esito — non sa se il try è riuscito. Se si mette commit() in finally, si farà il commit anche in caso di errore.
E non usare return da finally. Questo è uno degli angoli genuinamente pericolosi del linguaggio:
try {
return 1;
} finally {
return 2; // wins — the method returns 2 and the original return is lost
}Il return (o throw) all'interno di finally sovrascrive qualsiasi return o eccezione proveniente dal try. L'eccezione che stava per propagarsi viene scartata silenziosamente. La maggior parte dei linter segnala questo come un errore per questo motivo. La regola: finally esegue la pulizia; finally non produce valori.
Ordine di esecuzione
Quando sono presenti sia un catch che un finally:
try { ... }
catch { ... }
finally { ... }L'ordine è esattamente quello che ci si aspetterebbe: try viene eseguito, se lancia un'eccezione e un catch corrisponde allora viene eseguito quel catch, e finally viene eseguito dopo uno dei due. Se finally lancia a sua volta un'eccezione, quel nuovo lancio sostituisce qualsiasi cosa stesse propagandosi dal try o dal catch — un altro motivo per mantenere finally silenzioso.
Un esempio pratico
Strumentiamo una piccola "transazione" con try/catch/finally e la chiamiamo in tre modi diversi: successo normale, un errore recuperabile e uno non recuperabile. Il finally viene eseguito in tutti e tre i casi, che è l'intero punto.
Nella terza chiamata, doWork lancia una RuntimeException che il catch locale non intercetta. Il finally viene comunque eseguito e stampa "release lock" prima che l'eccezione continui a risalire fino a main. Questa è la proprietà che si desidera dal codice di pulizia — non dipende dal fatto che la gestione sia riuscita.
Cosa segue
La struttura "apri una risorsa, lavora con essa, chiudila in finally" è così comune che Java ha costruito una istruzione dedicata per essa. Continua con Java try-with-resources.