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.

Nota:
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 dividendo: 10
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.