Gestione delle eccezioni in Java

Java possiede un meccanismo che permette al programmatore di trattare le situazioni anomale in modo flessibile, elegante e perfettamente integrato con la metodologia orientata ad oggetti:
  • le eccezioni sono viste come oggetti di classi particolari;
  • la classe di un oggetto-eccezione ne descrive la natura;

  • una nuova eccezione può essere creata in presenza di situazioni inattese per poi essere riferita da un apposito gestore.

Come funziona il meccanismo di gestione delle eccezioni?

  • Quando si verifica un imprevisto, il metodo attivo lancia (throws) un'eccezione che viene passata al metodo chiamante. Il metodo attivo termina l'esecuzione (come con return).

  • Per default, un metodo che riceve un'eccezione termina l'esecuzione e passa l'eccezione al metodo chiamante.
  • Quando l'eccezione raggiunge main, l'esecuzione del programma termina stampando un opportuno messaggio di errore.

Ma un metodo chiamante può essere progettato in modo da:
  1. catturare (catch) un'eccezione lanciata da un metodo invocato;

  2. trattarla con opportune istruzioni;

  3. proseguire l'elaborazione senza terminare disastrosamente.

Ad esempio, se in un ciclo che legge dati da internet cade la connessione, è naturale gestire questa situazione da programma senza causarne necessariamente la terminazione.




Esempio di gestione default

Per sperimentare con la propagazione delle eccezioni, si compili e si esegua la classe NestedNullPointer:
 
public class NestedNullPointer {
     public static void bar(){
         Object o = null;
         System.out.println(o.toString());
     }
     public static void foo(){
         bar();
     }
     public static void main(String [] args){
         foo();
     }
 }

La macchina astratta Java scriverà qualcosa come:
 


> java NestedNullPointer
Exception in thread "main" java.lang.NullPointerException
        at NestedNullPointer.bar(NestedNullPointer.java:4)
        at NestedNullPointer.foo(NestedNullPointer.java:7)
        at NestedNullPointer.main(NestedNullPointer.java:10)

elencando la catena dei metodi attivi nel momento in cui si verifica l'eccezione (bar - foo - main) e per ogni metodo la linea di codice dove si è verificata.




Gerarchie di eccezioni

Per comprendere bene il meccanismo di gestione delle eccezioni, è utile vedere come questo si integra con il paradigma orientato ad oggetti: 

     
  • Le eccezioni in Java sono oggetti, istanze (di sottoclassi) della classe Throwable (lanciabile);

  •  
  • L'ereditarietà consente di definire gerarchie di eccezioni: utili per distinguere tra le varie situazioni anomale, e gestire ognuna nel modo opportuno;

  •  
  • Java fornisce una ricca gerarchia di eccezioni predefinite;

  •  
  • Si possono definire nuove (sottoclassi di) eccezioni per meglio caratterizzare le situazioni che si possono verificare in specifiche applicazioni (solo se strettamente necessario, altrimenti è più sensato ricondursi ad eccezioni predefinite).



Un pezzo della gerarchia

  • Tutte le classi di eccezioni sono sottoclassi di Throwable
     
  • Throwable ha due sottoclassi dirette: Error e Exception
     
  • Le sottoclassi di Error rappresentano errori fatali software (della Java Virtual Machine) o hardware (crash di un disco)

Vediamo un "frammento" della gerarchia di classi di eccezioni di Java (nel prossimo esempio cattureremo un'eccezione ArrayIndexOutOfBoundsException):


Object
  |
  +----Throwable
          |
          +----Error
          |      |
          |      +----VirtualMachineError
          |      ...
          |
          +----Exception
                  |
                  +----RuntimeException
                  |         |
                  |         +----ArithmeticException
                  |         |
                  |         +----IndexOutOfBoundsException
                  |         |        |
                  |         |        +----ArrayIndexOutOfBoundsException
                  |         |
                  |         +----ClassCastException
                  |         ...
                  |
                  +----IOException
                  |         |
                  |         +----EOFException
                  |         |
                  |         +----FileNotFoundException
                  |         ...
                  ...

 
 



Catturare un'eccezione: try e catch

Se una chiamata di metodo può generare un'eccezione, possiamo racchiuderla in un blocco try, seguito da uno o più blocchi catch contenenti le istruzioni da eseguire in corrispondenza dell'eccezione lanciata.

Esempio: Stampa di un array usando eccezioni (CatchOutOfBounds):
 

public class CatchOutOfBounds { 
    public static void main(String [] args) {
        int [] array = new int [5];
        for (int i = 0; i < array.length; i++) { 
            array[i] = (int) (100 * Math.random()); 
        }

        System.out.println("Contenuto dell'array:");

        try { 
            int i = 0;
            while (true) 
                System.out.println(array[i++]);
        }
        // catch (Throwable e) {
        // catch (Exception e) {
        // catch (RuntimeException e) {
        // catch (IndexOutOfBoundsException e) {
        catch (ArrayIndexOutOfBoundsException e) {
        // catch (Error e) {
            System.out.println("Stampa terminata...");
        } 
    }
}

Attenzione: è un buon esempio di try-catch, ma è un PESSIMO stile di programmazione!!!




Sintassi di try-catch-finally

    try {
       <istruzioni-try>// possono lanciare delle eccezioni
    }
    catch(<sottoclasse-Throwable1> e1) {
       // catturiamo l'eccezione e1 di tipo <sottoclasse-Throwable1>
       <istruzioni-catch1>// gestiamo e1
    }
    catch(<sottoclasse-Throwable2> e2) {
       // catturiamo l'eccezione e2 di tipo <sottoclasse-Throwable2>
       <istruzioni-catch2>// gestiamo e2
    }
    ...
    catch(<sottoclasse-ThrowableN> eN) {
       // catturiamo l'eccezione eN di tipo <sottoclasse-ThrowableN>
       <istruzioni-catchN>// gestiamo e2
    }
    finally {
       // istruzioni da eseguire comunque
       <istruzioni-finally>;
    } 

Se è presente almeno un blocco catch, allora il blocco finally è facoltativo.




Significato di try-catch-finally

  • Si eseguono le <istruzioni-try>.

  •  
  • Se l'esecuzione termina senza fallimenti si eseguono le eventuali <istruzioni-finally> e poi si prosegue ad eseguire la prima istruzione successiva al blocco try-catch.

  •  
  • Altrimenti, se l'esecuzione di <istruzioni-try> lancia un'eccezione except, si cerca il PRIMO BLOCCO catch tale che except sia istanza di <sottoclasse-ThrowableX>.

  •  
  • Se un tale blocco esiste, si eseguono le <istruzioni-catchX>  dopo aver associato except all'identificatore eX (in questo caso si dice che l'eccezione è stata catturata con successo); poi si eseguono le eventuali <istruzioni-finally> e infine si prosegue ad eseguire la prima istruzione successiva al blocco try-catch.

  •  
  • Se invece except non è istanza di nessuna <sottoclasse-ThrowableX>, allora VENGONO COMUNQUE ESEGUITE le eventuali <istruzioni-finally>, ma poi l'eccezione viene passata al metodo chiamante (ed il metodo attivo viene terminato con fallimento).
Note:
  • Le eventuali <istruzioni-finally> vengono eseguite sempre, anche in presenza di un return del blocco trycatch. Il blocco finally può contenere delle istruzioni che chiudono dei files oppure rilasciano delle risorse, per garantire la consistenza dello stato.
     
  • I costrutti try-catch-finally possono essere annidati a piacere.  Esempio: se in un blocco catch o finally può essere generata un'eccezione, si possono racchiudere le sue istruzioni in un altro blocco try.