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 (throw) 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 propagazione delle eccezioni

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:


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

 
 



Lanciare un'eccezione: throw

In tutti gli esempi visti l'eccezione era lanciata dall'interprete quando si verificava una situazione imprevista.

Talvolta un metodo può trovarsi a non poter gestire dei dati che riconosce come errati (ad esempio, ricevendo dei dati in lettura che non corrispondono al formato atteso)... Cosa fare in questi casi?

Invece di elaborare dati errati si può lanciare un'eccezione con il comando throw.

Sintassi
 


// <eccezione> è un oggetto (di una sottoclasse) della classe Exception

throw <eccezione>;
 


// di solito si crea un opportuno oggetto

throw new <Sottoclasse-Exception>(<messaggio-errore>);
 

Esempio: lanciamo un'eccezione (Sqrt)
 

public class Sqrt { 

    // calcola la radice quadrata di x, se e' positivo 
    public static double sqrt(double x) {

        // lancia un'eccezione se il parametro attuale e' illegale 
        if (x < 0)
            throw new IllegalArgumentException("sqrt: " + x);

        return Math.sqrt(x);
    }


    public static void main(String [] args) {  
        double x; 
        do { 
            System.out.print("Scrivi un intero (0 per finire): ");
            x = Input.readInt(); 
            if (x != 0) {
                System.out.println("Risultato di sqrt("+x+"):"); 
                // System.out.println(Math.sqrt(x)); 
                System.out.println(sqrt(x)); 
            } 
        } while(x != 0); 
    } 
}