Nel linguaggio di programmazione object-oriented Java, il sistema di gestione delle eccezioni (o exception handling) è costituito da un insieme di costrutti e regole sintattiche e semantiche introdotte allo scopo di rendere più semplice, chiara e sicura la gestione di eventuali situazioni anomale, dette eccezioni, che si possono verificare durante l'esecuzione di un programma.
Lexception handling di Java deriva direttamente (anche da un punto di vista sintattico) da quello del linguaggio C++. Tuttavia, il meccanismo di Java deve considerarsi decisamente più oneroso, ma certamente più sicuro, grazie alla cosiddetta regola dello handle or declare (gestisci o dichiara), che in sostanza obbliga il programmatore a prevedere esplicite contromisure per ogni situazione anomala (prevedibile).
Motivazioni Qualsiasi programma concreto di un certo livello di complessità può incorrere, durante la propria esecuzione, in situazioni anomale che richiedono di venire trattate eseguendo azioni che differiscono da quello che sarebbe stato, altrimenti, il "flusso normale" del programma. Ovviamente, il confine fra "anomalo" o "normale" non è netto. Come esempi di "situazioni anomale", si pensi per esempio all'impossibilità di dialogare con un server attraverso la rete, o il fatto che il programma cerchi di aprire un che non risulta presente su disco, e così via.
La gestione delle situazioni anomale presenta diversi aspetti critici rispetto a considerazioni di qualità del software. Da una parte, sarebbe auspicabile che chi sviluppa un programma ponga una notevole cura nel prevedere tutte le possibili situazioni anomale che potrebbero insorgere durante l'esecuzione e nel predisporre le contromisure che il programma deve adottare in tali casi per ridurre al minimo le conseguenze di tali anomalie. La gestione "puntigliosa" di tutte le possibili situazioni anomale in tutti i possibili luoghi del codice in cui possono manifestarsi è infatti importante ai fini della robustezza e affidabilità del software. D'altra parte, essendo le situazioni "anomale" potenzialmente molto numerose e diversificate, una gestione davvero completa potrebbe avere l'effetto indesiderabile di oscurare la struttura del codice sorgente, poiché le (relativamente poche) istruzioni che il programma dovrebbe eseguire nel caso normale (o nei casi normali) si potrebbero trovare immerse (e "disperse") in mezzo a una quantità preponderante di istruzioni dedicate alla gestione di anomalie (magari molto improbabili), ovviamente a scapito della leggibilità del programma stesso.
Segnalazione del fallimento di un metodo Ogni metodo di un programma Java dovrebbe avere un compito ben preciso da portare a termine (descritto dal suo commento Javadoc). In presenza di anomalie o situazioni impreviste, è possibile che un metodo fallisca, ovvero non sia in grado di portare a termine tale compito. Questa evenienza deve essere evidentemente segnalata al metodo chiamante il quale poi potrà, a seconda dei casi, prendere qualche contromisura che gli consenta di concludere il proprio compito nonostante il fallimento del metodo chiamato, oppure, se questo è impossibile, dichiarare a sua volta il proprio fallimento nei confronti del proprio chiamante (e così via).
Per segnalare il proprio fallimento, un metodo Java può sollevare (o "lanciare", per conservare il significato del corrispondente termine inglese to throw) una eccezione. Si può considerare una eccezione sollevata da un metodo come analogo al concetto di valore restituito dal metodo. Tuttavia, Java distingue i due concetti, così che un metodo potrebbe per esempio tornare un valore intero oppure sollevare un'eccezione, che è un valore di altro tipo (in seguito vedremo quali sono i tipi ammissibili per i valori-eccezione). Il seguente estratto di codice mostra una situazione di questo genere:
/** * Calcola la differenza in giorni fra due date, specificate rispettivamente dal giorno * "gg1", mese "mm1" e anno "aa1" e giorno "gg2", mese "mm2" e anno "aa2" */ public int differenzaDate(int gg1, int mm1, int aa1, int gg2, int mm2, int aa2) throws DataNonValida { if(!dataValida(gg1, mm1, aa1) || !dataValida(gg2, mm2, aa2)) throw new DataNonValida(); else { int risultato; // ... calcola la differenza fra le date return risultato;
Il metodo riportato ritorna, in assenza di errori, un intero che rappresenta la distanza in giorni fra due date. Nel caso in cui una delle triple (giorno, mese, anno) non sia una data valida (per esempio, la tripla 31 2 2000), anziché ritornare un valore intero il metodo solleva una eccezione. La clausola throws nell'intestazione del metodo specifica che questo speciale valore di "eccezione" sarà di tipo (classe) DataNonValida; il punto del codice in cui l'eccezione viene sollevata è l'istruzione throw new DataNonValida(). Ovviamente deve essere stata definita una classe DataNonValida e, come vedremo nel seguito, questa classe deve anche avere alcune caratteristiche specifiche che consentono l'utilizzo delle sue istanze come valori di eccezione.
La semantica dell'operatore throw ha alcuni aspetti in comune con quella dell'operatore return; in particolare, l'esecuzione dell'istruzione throw comporta la terminazione immediata del metodo e il passaggio del controllo (seppure secondo un particolare insieme di regole che si esamineranno nel seguito) al chiamante del metodo stesso.
Gestione dell'eccezione nel chiamante Quando un metodo ne invoca un altro e quest'ultimo può sollevare un'eccezione (come specificato dalla clausola throws della sua intestazione), il chiamante può predisporsi per gestire tale evenienza. La gestione dell'eccezione avviene utilizzando una struttura di controllo specifica, detta blocco try-catch. Come si vedrà, questa struttura di controllo ha un funzionamento in parte simile a (una forma ristretta di) goto e in parte simile alla chiamata di un metodo.
Il seguente frammento di codice mostra l'uso del blocco try-catch nel chiamante:
public void faiQualcosa(Scanner input) { boolean successo = false; int g1, g2, m1, m2, a1, a2; while (!successo) { // chiede all'utente di inserire valori per g1, m1, a1, g2, m2, a2 try { ... int dd = differenzaDate(g1, m1, a1, g2, m2, a2); successo = true; ... System.out.println("La differenza è " + dd); } catch (DataNonValida dnv) { System.out.println("Almeno una delle date inserite non è valida");
La clausola try controlla un blocco di codice all'interno del quale compare il metodo "a rischio" differenzaDate. Quando il metodo viene eseguito, si danno due casi: il metodo ha successo e ritorna un intero. Il blocco controllato dal try viene portato a termine e il controllo passa alla prima istruzione successiva alla struttura di controllo try-catch. Quindi, successo diventa true, viene stampata a video "La differenza è..." e il metodo termina; il metodo fallisce e solleva un'eccezione. Il blocco controllato dal try viene immediatamente terminato e il controllo passa al blocco controllato dalla clausola catch. Quindi, successo rimane false, viene stampato il messaggio di errore ("Almeno una delle date inserite non è valida") e il controllo ritorna al ciclo while.
In sostanza, il blocco try-catch consente di separare accuratamente il funzionamento del metodo nel caso "normale" (blocco try) e la gestione di situazioni anomale (blocco catch).
Il blocco finally Come altri linguaggi, Java supporta un terzo blocco chiamato finally. Questo blocco verrà eseguito sempre, a prescindere dal fatto che l'esecuzione del blocco try abbia generato o meno un'eccezione, e a prescindere da ciò che accade nel blocco catch. La sintassi è la seguente:
try { // codice che può generare un'eccezione } catch (ClasseEccezione e) { // gestisci l'eccezione } finally { // codice da eseguire in ogni caso }
Catch multiple Un metodo può sollevare più tipi di eccezione. Per esempio, un metodo che deve accedere a file potrebbe prevedere diverse segnalazioni di anomalie che rappresentano il fatto che il file non esista oppure che i suoi contenuti risultino danneggiati o scorretti:
public int leggiFile() throws FileInesistente, FileDanneggiato { ... }
Analogamente, un blocco try-catch può comprendere più blocchi catch dedicati a gestire diversi tipi di eccezioni:
public faiQualcosa2() { try { leggiFile(); } catch (FileInesistente fi) { System.out.println("Ooops! Il file " + fi.getNomeFile() + " non esiste!"); } catch (FileDanneggiato fd) { System.out.println("Ooops! Il file " + fd.getNomeFile() + " contiene dati scorretti!");
La regola "handle or declare" Potrebbe darsi il caso in cui, a differenza di quanto visto nell'esempio precedente, il metodo chiamante non sia in grado di prendere contromisure rispetto al problema occorso. Supponiamo per esempio che il metodo che riceve dall'input i valori dei giorni, mesi e anni non sia faiQualcosa ma il suo chiamante (e che quindi, faiQualcosa riceva questi dati come argomenti). In tal caso è sensato supporre che sia opportuno delegare al chiamante di faiQualcosa anche la soluzione del problema (cioè chiedere nuovi dati all'utente). Il blocco di codice seguente mostra quale dovrebbe essere la forma del metodo faiQualcosa in questo caso:
public void faiQualcosa(int g1, int m1, int a1, int g2, int m2, int a2) throws DataNonValida { int dd = differenzaDate(g1, m1, a1, g2, m2, a2); System.out.println("La differenza è " + dd); }
Poiché faiQualcosa non può risolvere il problema eventualmente segnalato da differenzaDate, in questa versione esso non contiene una clausola try-catch. In questo caso, se differenzaDate solleva effettivamente l'eccezione, il modello di exception handling di Java prevede che anche faiQualcosa venga terminato. L'eccezione sollevata da differenzaDate sarà in tal caso automaticamente "propagata" al chiamante di faiQualcosa, esattamente come se quest'ultimo avesse eseguito l'istruzione throw. Per questo motivo, diventa obbligatorio inserire la clausola throws DataNonValida anche nell'intestazione di faiQualcosa, segnalando così il fatto che anche questo metodo (indirettamente) può riportare una eccezione di tipo DataNonValida al proprio chiamante.
Questa regola di Java (innovativa rispetto all'exception handling del C++) viene detta regola handle or declare (gestisci o dichiara): a fronte di una possibile eccezione, un metodo deve gestirla oppure dichiarare a sua volta di sollevarla. Questo modello implica che una eccezione non possa mai passare inosservata; se non la si gestisce, non si fa altro che rimandare al chiamante l'''obbligo di gestirla.
Osservazioni Nei linguaggi sprovvisti di un meccanismo di exception handling, un metodo segnala il proprio fallimento, di norma, ritornando un valore speciale, a cui il programmatore attribuisce convenzionalmente il significato di segnalazione di fallimento. Per esempio, il metodo differenzaDate potrebbe tornare "-1" in caso di fallimento. Questo modello di gestione delle anomalie ha però diverse controindicazioni: la segnalazione segue una convenzione che deve essere documentata accuratamente; in assenza di una documentazione appropriata, il chiamante potrebbe non riuscire a interpretare il valore tornato; non sempre è possibile identificare un valore "speciale" da usare come segnalazione di errore; per esempio, se "differenzaDate" è pensato per fornire la differenza sia fra date crescenti che fra date decrescenti e distinguere i due casi, "-1" potrebbe essere un valore di ritorno lecito (per esempio il risultato di "differenzaDate(1, 1, 1970, 2, 1, 1970)");