Il costrutto try-catch-finally
Durante le lezioni precedenti, parlando delle eccezioni checked, abbiamo spesso detto che il
programmatore è obbligato ad intercettarle e gestirle.
Ma qual è, nello specifico, il significato di questi due termini?
Cosa deve fare in
particolare
il programmatore per intercettare e gestire una eccezione?
Intercettare una eccezione vuol dire mettere in atto delle
strategie per far si che, nel caso l'eccezione si verifichi, il
programma non termini in maniera
anomala, ma lo rilevi, in modo che si possano eseguire le azioni correttive, previste dal
programmatore per quel tipo di eccezione.
Gestire una eccezione vuol dire eseguire una serie di azioni per
mitigare l'effetto dell'anomalia avvenuta, in modo da permettere
al programma di continuare la
sua esecuzione in totale sicurezza, senza rischi di danneggiamento e/o perdita di dati.
Da un punto di vista tecnico e per rispondere alla seconda delle precedenti domande, per
intercettare e gestire una eccezione, il programmatore deve utilizzare il costrutto
try-catch (o try-catch-finally).
In linguaggio Java il costrutto try-catch-finally si presenta come segue.
...
try {
// sequenza di istruzioni da eseguire
// che, potenzialmente, potrebbero
// generare eccezioni
} catch (TipoEccezione1 identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezione1
} catch (TipoEccezione2 identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezione2
} ... catch (TipoEccezioneN identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezionN
} finally {
// istruzioni da eseguire alla fine,
// sia nel caso in cui non si siano
// verificate anomalie, che nel caso
// sia avvenuta una qualsiasi delle
// anomalie dei tipi citati
// (TipoEccezione1 .. TipoEccezioneN)
}
...
Analizziamolo un po' più da vicino.
Quando un programmatore prevede che all'interno di una sequenza di istruzioni possa avvenire una
eccezione e sia obbligato (checked), o voglia gestirla, deve inserire la sequenza di
istruzioni interessata all'interno di un blocco try.
Dopo il blocco try, può inserire uno o più blocchi catch, per gestire tipologie differenti di
anomalie.
Come si può notare dalla struttura di try-catch rappresentata sopra, in ogni catch si indica
un tipo di eccezione che quel blocco catch gestisce.
All'interno del blocco catch il programmatore deve inserire le istruzioni da eseguire nel
caso in cui avvenga quel tipo di anomalia, per rimediarne gli effetti.
Alla fine si potrà inserire un blocco finally, nel caso ci siano delle istruzioni da
eseguire comunque, sia qualora avvengano anomalie, sia nel caso in cui non ve ne siano.
Aggiungiamo, a questo punto, alcune precisazioni. All'interno di un blocco try-catch-finally
possono esserci:
- un solo blocco try, contenente le istruzioni che implementano la logica applicativae e che potrebbero determinare delle eccezioni.
- uno o più blocchi catch, uno per ogni tipologia di eccezione che può capitare e che si voglia trattare in modo differente rispetto alle altre gestite nei blocchi catch precedenti.
- eventualmente un blocco finally, nel caso vi siano delle istruzioni che si vuole comunque eseguire, sia in caso di eccezioni, che in caso di assenza di anomalie.
In sintesi, ogni volta che si utilizza un blocco try è
obbligatorio inserire almeno un blocco catch. Il blocco finally è facoltativo e a discrezione
del programmatore.
In contesti particolari, il blocco try-catch-finally può essere anche utilizzato, per altre finalità, che esulano dalla gestione delle eccezioni, con i soli blocchi try e finally, quindi senza nessun blocco catch.
Questo utilizzo indirizza altre necessità della programmazione O.O. che non riguardano la gestione delle eccezioni e che, per tale ragione, non verrà approfondita in questa sede.
Ricapitolando, in conclusione, l'uso del blocco try richiede uno o più
blocchi catch, in assenza
del blocco finally, oppure richiede il blocco finally, in assenza di blocchi catch.
Di seguito sono riportate differenti modalità di utilizzo del blocco try-catch-finally.
// Utilizzo con un catch, senza finally
try {
// sequenza di istruzioni da eseguire
// che, potenzialmente, potrebbero
// generare eccezioni
} catch (TipoEccezione identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezione
}
// Utilizzo con più catch, senza finally
try {
// sequenza di istruzioni da eseguire
// che, potenzialmente, potrebbero
// generare eccezioni
} catch (TipoEccezione1 identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezione1
} catch (TipoEccezione2 identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezione2
} catch (TipoEccezione3 identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezion3
}
// Utilizzo con uno o più catch e blocco finally
try {
// sequenza di istruzioni da eseguire
// che, potenzialmente, potrebbero
// generare eccezioni
} catch (TipoEccezione1 identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezione1
} catch (TipoEccezione2 identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezione2
} ... catch (TipoEccezioneN identificatore) {
// sequenza di istruzioni da eseguire
// per rimediare gli effetti della
// eccezione del tipo TipoEccezionN
} finally {
// istruzioni da eseguire alla fine,
// sia nel caso in cui non si siano
// verificate anomalie, che nel caso
// sia avvenuta una qualsiasi delle
// anomalie dei tipi citati
// (TipoEccezione1 .. TipoEccezioneN)
}
// Utilizzo senza catch, con finally
try {
// sequenza di istruzioni da eseguire
// che, potenzialmente, potrebbero
// generare eccezioni
} finally {
// istruzioni da eseguire alla fine,
// sia nel caso in cui non si siano
// verificate anomalie, che nel caso
// sia avvenuta una qualsiasi anomalia
}
Vediamo a questo punto cosa succede, da un punto di vista
pratico, durante l'esecuzione di un
programma che prevede la gestione delle eccezioni.
Quando l'esecuzione di un programma incontra un blocco try, vengono eseguite le istruzioni in
esso
contenute fino al completamento, oppure fino a che una di esse non generi una eccezione.
Le situazioni in cui ci si può trovare, a questo punto, possono essere due:
- non si verifica nessuna eccezione: l'esecuzione completa le istruzioni del blocco try, esegue le istruzioni presenti nel blocco finally, se presente, ed infine continua con l'istruzione che segue il blocco try-catch-finally.
- si verifica una eccezione: l'esecuzione del blocco try si interrompe nel punto in cui si è verificata l'eccezione (le istruzioni che seguono non vengono eseguite), vengono poi eseguite tutte le istruzioni del blocco catch che gestisce il tipo di anomalia accaduta, infine vengono eseguite le istruzioni del blocco finally, se presente e, successivamente, continua con l'istruzione che segue il blocco try-catch-finally.
Rivediamo, a questo punto, come sarebbe e come si comporterebbe
il programma del quoziente della
divisione, nel caso in cui il programmatore decidesse di intercettare e gestire
l'eccezione
aritmetica di divisione per zero.
Il codice del programma, visto in precedenza, potrebbe essere riscritto come segue:
import java.util.Scanner;
public class CalcoloQuoziente {
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
System.out.print("Inserisci il dividendo: ");
int dividendo = sc.nextInt();
System.out.print("Inserisci il divisore: ");
int divisore = sc.nextInt();
String msg = quoziente(dividendo, divisore);
System.out.println(msg);
}
public static String quoziente(int dividendo, int divisore) {
try {
int quoziente = dividendo / divisore;
return "Il quoziente della divisione tra " + dividendo +
" e " + divisore + " è " + quoziente;
} catch (ArithmeticException ex) {
return "Non è possibile eseguire la divisione tra " + dividendo +
" e " + divisore + ", perchè il divisore è nullo.";
}
}
}
Eseguendo questa volta il programma, con valori 10 e 0 come dividendo e divisore, a differenza di quanto capitato in assenza di gestione dell'eccezione, lo stesso non termina più in maniera anomala, ma produce l'output visualizzato di seguito.
Inserisci il divisore: 0
Non è possibile eseguire la divisione tra 10 e 0, perchè il divisore è nullo.
Nella pratica è successo quanto elencato di seguito:
- l'esecuzione del programma è partita dalla funzione main e, dopo aver acquisito in input i valori di dividendo e divisore, quest'ultimo nullo, ha chiamato la funzione quoziente che, questa volta è stata scritta in modo da restituire il messaggio completo e non semplicemente il valore del quoziente
- l'esecuzione della funzione quoziente ha eseguito la divisione tra dividendo e divisore, generando, ovviamente, l'eccezione di tipo ArithmetcException
- l'esecuzione del blocco try si è interrotta e la return successiva non è stata eseguita
- l'esecuzione è saltata al blocco catch che gestisce l'eccezione di tipo ArithmeticException (in questo caso l'unico presente) ed ha eseguito l'unica sua istruzione, restituendo il messaggio di divisione non possibile, a causa del valore nullo del divisore
- non essendoci istruzioni dopo il blocco try-catch, l'esecuzione della funzione quoziente è terminata ed il controllo è tornato al main che ha continuato stampando il messaggio restituito dalla chiamata della funzione, come se non fosse avvenuto nulla di anomalo.
Un altro esempio di realizzazione dello stesso programma, ma questa volta senza l'uso di funzioni, ma con l'uso del try-catch-finally avrebbe potuto essere il seguente:
import java.util.Scanner;
public class CalcoloQuoziente {
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
System.out.print("Inserisci il dividendo: ");
int dividendo = sc.nextInt();
System.out.print("Inserisci il divisore: ");
int divisore = sc.nextInt();
String msg = "";
try {
int quoziente = dividendo / divisore;
msg = "Il quoziente della divisione tra " + dividendo +
" e " + divisore + " è " + quoziente;
} catch (ArithmeticException ex) {
msg = "Non è possibile eseguire la divisione tra " + dividendo +
" e " + divisore + ", perchè il divisore è nullo.";
} finally {
System.out.println(msg);
}
}
}
L'output sarebbe stato esattamente il medesimo visto poc'anzi, ma questa volta la sequenza degli eventi sarebbe stata la seguente:
- l'esecuzione del programma parte dalla funzione main e, dopo aver acquisito in input i valori di dividendo e divisore, quest'ultimo nullo, entra nel blocco try ed esegue la divisione tra dividendo e divisore, generando, ovviamente, l'eccezione di tipo ArithmetcException
- l'esecuzione del blocco try si interrompe e l'assegnazione del risultato alla variabile msg non viene eseguita
- l'esecuzione salta al blocco catch che gestisce l'eccezione di tipo ArithmeticException (anche in questo caso l'unico presente) ed esegue l'unica sua istruzione, assegnando alla variabile msg il messaggio di divisione non possibile, a causa del valore nullo del divisore
- successivamente, essendo presente il blocco finally, l'esecuzione esegue l'unica istruzione in esso presente, stampando il valore della variabile msg, anche questa volta come se non fosse avvenuto nulla di anomalo.