MokaByte 78 - 8bre 2003 
Corso di programmazione Java
XVI parte: la gestione delle eccezioni
di
Andrea Gini
Termina questo mese il corso di Java base, che ci ha accompagnato per più di un anno. Dopo aver approfondito tutti i costrutti del linguaggio, resta ora da approfondire il meccanismo delle eccezioni, un costrutto già introdotto con il C++ che ha trovato in Java una nuova e più coerente modalità operativa.

Eccezioni
Durante la sua normale esecuzione, un programma può andare incontro a vari problemi di funzionamento. Tali problemi a volte non dipendono dal codice, ma da eventi del mondo reale che non sono sotto il controllo del programmatore. Si provi a pensare ad un program-ma che legge un file dal disco: se durante l'esecuzione il disco si rompe, il programma andrà incontro ad un fallimento. Tale fallimento non è dovuto ad un errore di programmazione: si tratta semplicemente di uno di quegli incidenti che nel mondo reale possono capitare quan-do meno ci si aspetta.

Il linguaggio Java definisce in modo rigoroso il concetto di eccezione, e prevede un apposito costrutto per favorirne la gestione. A differenza del C++, la gestione delle eccezioni in Java non è derogabile: tutte le situazioni in cui può avvenire un'eccezione devono essere gestite in modo esplicito dal programmatore.

Questo approccio rende i programmi di gran lunga più robusti, e riduce notevolmente i pro-blemi di affidabilità e di portabilità del codice.
Errori ed Eccezioni
I problemi che si possono presentare in fase di esecuzione appartengono a due categorie: Er-rori di Runtime ed Eccezioni.

Gli Errori di Runtime si verificano quando un frammento di codice scritto correttamente si trova a dover gestire una situazione anomala che impedisce di proseguire l'esecuzione. Si os-servi il seguente assegnamento:

a = b / c;

L'istruzione è formulata correttamente e funzionerà senza problemi in quasi tutti i casi; tut-tavia, se per qualche ragione la variabile c dovesse assumere il valore 0, l'assegnamento non potrebbe avere luogo, dal momento che la divisione per 0 non è definita. Si noti che le circo-stanze per cui la variabile c potrebbe assumere il valore 0 non dipendono necessariamente dallo sviluppatore: se il programma legge i dati da un file, ad esempio, può capitare che quest'ultimo sia stato formulato in modo sbagliato dall'operatore incaricato di inserire i dati. La caratteristica principale degli Errori di Runtime, è che essi causano quasi sempre la termi-nazione irrevocabile del programma.

Le Eccezione sono situazioni anomale che interrompono un'operazione durante il suo nor-male svolgimento. Se un computer sta dialogando con un Server attraverso la rete, e sul più bello un fulmine incenerisce la linea di collegamento, il programma Client dovrà affrontare una circostanza accidentale ed imprevista, che in molti casi può essere gestita senza provoca-re la terminazione del programma. Chiunque navighi in Internet si sarà trovato almeno una volta nell'impossibilità di collegarsi ad un sito: in casi come questo, il browser si limita a pre-sentare un messaggio di errore a schermo, e quindi si predispone a ricevere nuove direttive dall'utente.

 

Gestione delle eccezioni
Molti oggetti Java possono generare eccezioni durante la chiamata di un loro metodo o addi-rittura durante la creazione: questa esigenza è specificata dalla documentazione della classe. Il tentativo di aprire un file in lettura, ad esempio, può causare una FileNotFoundException nel caso il file che si desidera aprire non esista. Se si prova a compilare un programma che con-tiene un'istruzione come la seguente:

Reader i = new FileReader("file.txt");

Il compilatore segnalerà la necessità di "catturare" (catch) in modo esplicito l'eccezione File-NotFoundException:

C:\program.java:6: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown Reader i = new FileReader("file.txt");
^
1 error

L'esempio seguente mostra la sintassi da utilizzare nella situazione appena descritta:

try {
  BufferedReader i = new BufferedReader(new FileReader("text.txt"));
}
catch(FileNotFoundException fnfe) {
  System.out.println("Il file indicato non è stato trovato");
  fnfe.printStackTrace();
}

In questo caso, il computer prova (try) ad eseguire l'istruzione contenuta nel primo blocco. Se durante il tentativo si verifica un'eccezione, quest'ultima viene catturata (catch) dal secondo blocco, che stampa a schermo un messaggio di errore e mostra lo stack di esecuzione del programma:

Il file indicato non è stato trovato
java.io.FileNotFoundException: file.txt (Impossibile trovare il file specificato)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:103)
at java.io.FileInputStream.<init>(FileInputStream.java:66)
at java.io.FileReader.<init>(FileReader.java:41)
at Untitled.main(Untitled.java:7)

Se invece non sorgono problemi, il Runtime Java ignora quanto è contenuto nel blocco catch e prosegue nella normale esecuzione.
Costrutto try - catch - finally
Il costrutto generale per la gestione delle eccezioni ha la seguente forma:

try {
  istruzione1();
  istruzione2();
  ....
}
catch(Exception1 e1) {
  // gestione dell'eventuale problema nato nel blocco try
}
catch(Exception2 e2) {
  // gestione dell'eventuale problema nato nel blocco try
}
finally {
  // codice da eseguire comunque al termine del blocco try
}

Il blocco try contiene un insieme di istruzioni che potrebbe generare eccezioni. Generalmen-te si racchiude all'interno di tale blocco un'intera procedura: se durante l'esecuzione della stessa una qualunque delle istruzioni genera un'eccezione, il flusso di esecuzione si interrom-pe, e la gestione passa al blocco catch incaricato di gestire l'eccezione appena sollevata.

La catch specifica quale eccezione si desidera gestire e quali istruzioni eseguire in quella cir-costanza. E' possibile ripetere più volte il blocco catch, in modo da permettere una gestione differenziata delle eccezioni generate dal blocco try. In Java è obbligatorio inserire una catch per ogni possibile eccezione, anche se naturalmente è possibile lasciare in bianco il corri-spondente blocco.

La clausola finally contiene un blocco di istruzioni da eseguire comunque dopo il blocco try, sia nel caso esso sia terminato senza problemi, sia nel caso abbia sollevata una qualche ecce-zione.

 

Gerarchia delle Eccezioni
Le eccezioni sono oggetti, ed hanno una loro gerarchia. In cima ad essa si l'oggetto Throwa-ble, che definisce costruttori e metodi comuni a tutte le sottoclassi. Tra questi metodi, vale la pena di segnalare i seguenti, che permettono di stampare a schermo o su file informazioni diagnostiche:

  • printStackTrace(): stampa su schermo lo stack di sistema, segnalando a quale punto del flusso di esecuzione l'eccezione è stata generata e come si è propagata.
  • toString(): produce una stringa che contiene le informazioni che caratterizzano l'eccezione.



Figura 1
- Gerarchia delle Eccezioni Java

La classe Throwable da origine ad una gerarchia enorme (il JDK 1.4 contiene 330 tra eccezioni e errori). Le eccezioni possono essere suddivise in due categorie: checked e unchecked. Le prime devono obbligatoriamente essere gestite all'interno di un blocco try-catch: oltre alla generica Exception, esistono eccezioni che segnalano malfunzionamenti di Input Output, problemi di rete, errori nella formattazione dei dati e così via.

La seconda famiglia, che comprende gli Error, le RuntimeException e le relative sottoclassi, non obbliga il programmatore a ricorrere ad una try-catch, dal momento che esse segnalano i malfunzionamenti per i quali non è previsto recupero. Si notino OutOfMemoryError e StackO-verflowError, due condizioni che tipicamente segnalano l'esaurimento delle risorse macchina e la necessità di terminare il programma.

L'organizzazione gerarchica delle eccezioni consente una gestione per categorie, secondo il principio della genericità. Se ad esempio viene generata IOException, essa può essere gestita sia con una istruzione del tipo

catch(IOException ioe)

o con una più generica

catch(Exception e)

E' anche possibile predisporre una gestione delle eccezioni per ordine crescente di generici-tà. Nell'esempio seguente, i primi tre catch gestiscono delle eccezioni ben precise, mentre l'ultimo gestisce tutte le eccezioni che non sono state gestite dai precedenti blocchi:

try {
  …
}
catch(NullPointerException npe) {}
catch(FileNotFoundException fnfe) {}
catch(IOExcetpion ioe) {}
catch(Exception e) {}

Si noti che la gestione di FileNotFoundException deve precedere quella di IOException, dato che la prima è una sottoclasse della seconda. Per la stessa ragione, la gestione di Exception deve per forza comparire in fondo all'elenco.
Propagazione delle eccezioni: istruzione throws
Il costrutto try - catch permette di gestire i problemi localmente, nel punto preciso in cui sono state generate. Tuttavia, in un'applicazione adeguatamente stratificata, può essere preferibile fare in modo che le classi periferiche lascino rimbalzare l'eccezione verso gli oggetti chiaman-ti, in modo da delegare la gestione dell'eccezione alla classe che possiede la conoscenza più dettagliata del sistema.

Invia messaggio ->
-> apri collegamento ->
-> scrivi caratteri
<- ECCEZIONE IN SCRITTURA
<- ECCEZIONE IN SCRITTURA
<- ECCEZIONE IN SCRITTURA



L'istruzione throws, se presente nella firma di un metodo, consente di propagare l'eccezione al metodo chiamante, in modo da delegare ad esso la gestione:

public void sendMessage(String message) throws IOException {
  // istruzioni che possono generare una IOException
}

Naturalmente la chiamata al metodo sendMessage, definito con clausola throws, dovrà essere posta all'interno di un blocco try - catch. In alternativa, il metodo chiamante potrà a sua volta delegare, attraverso una throws, la gestione dell'eccezione.
Lancio di eccezioni: costrutto throw
Fino ad ora si è visto come gestire metodi che possono generare eccezioni, o in alternativa come delegare la gestione delle stesse ad un metodo chiamate. Ma come si deve fare se si de-sidera generare in prima istanza un'eccezione? Si provi ad estendere l'esempio del metodo fattoriale: per creare una procedura robusta, è necessario segnalare un errore nel caso si pro-vi ad effettuare una chiamata con un parametro negativo, dal momento che la funzione fatto-riale non è definita in questo caso. Il sistema ideale per segnalare l'irregolarità di una simile circostanza è la generazione di una IllegalArgumentException, come nell'esempio seguente:

static int fattoriale(int n) throws IllegalArgumentException {
if(n<0)
throw new IllegalArgumentException("Il parametro deve essere positivo");

f = 1;
while(n>0) {
f = f * n;
n = n--;
}
return f;
}

L'istruzione throw richiede come argomento un oggetto Throwable o una sua sottoclasse. E' possibile utilizzare la throw all'interno di un blocco catch, qualora si desideri ottenere sia la gestione locale di una eccezione, sia il suo inoltro all'oggetto chiamante:

public void sendMessage(String message) throws IOException {
  try {
    // istruzioni che possono generare una IOException
  }
  catch(IOException ioe) {
    // gestione locale
    System.out.println("IOException nel metodo sendMessage");
    // inoltra l'eccezione al chiamante
    throw ioe;
  }
}

 

Catene di Eccezioni
Il meccanismo di inoltro delle eccezioni descritto nel paragrafo precedente può essere ulte-riormente raffinato grazie ad una funzionalità introdotta a partire da Java 1.4: le eccezioni concatenate. In un sistema adeguatamente stratificato, capita che un'eccezione catturata ad un certo livello sia stata generata a partire da un'altra eccezione, nata da un livello più pro-fondo. L'uso di differenti livelli di astrazione spesso rende incomprensibile un'eccezione di livello più basso:

Invia messaggio ->
-> apri collegamento ->
-> scrivi caratteri
<- ECCEZIONE IN SCRITTURA
<- ECCEZIONE IN COLLEGAMENTO
<- ECCEZIONE IN INVIO

 


Figura 2 - Uno scenario di generazione
di eccezioni a catena


Per questa ragione, le eccezioni (a partire dal Java 1.4= permettono, in fase di creazione, di specificare una causa, ossia l'oggetto Throwable che ha causato l'eccezione nel livello più al-to. Questo meccanismo può dar vita a vere e proprie catene di eccezioni, che forniscono una diagnostica molto dettagliata, utile a comprendere la vera natura di un problema.
Eccezioni definite dall'utente
Nonostante la enorme varietà di eccezioni già presenti in Java, il programmatore può facil-mente crearne di proprie, qualora desideri segnalare condizioni di eccezione tipiche di un proprio programma. Per creare una nuova eccezione è sufficiente dichiarare una sottoclasse di Exception (o di una qualunque altra eccezione esistente), e ridefinire uno o più dei seguenti costruttori:

  • Exception(): Crea un'eccezione.
  • Exception(String message): Crea un'eccezione specificando un messaggio diagno-stico.
  • Exception(Throwable cause): Crea un'eccezione specificando la causa.
  • Exception(String message, Throwable cause): crea un'eccezione specificando un messaggio diagnostico e una causa.

Ecco un esempio di eccezione personalizzata:

public class MyException extends Exception {

  public MyException () {
    super();
  }
  public MyException (String message) {
    super(message);
  }
  public Exception(String message, Throwable cause) {
    super(message, cause);
  }
  public Exception(Throwable cause) {
    super(cause);
  }
}

Nella maggioranza dei casi, tuttavia, è sufficiente creare una sottoclasse vuota:

public class MyException extends Exception {}

 

Esempio riepilogativo
Per riassumere un argomento così importante e delicato, è opportuno studiare un'esempio riepilogativo. Il seguente programma getta una IllegalArgumentException se si cerca lanciare la riga di comando con un numero di parametri diverso da uno; quindi apre il file specificato dall'utente e ne stampa il contenuto a video. Le operazioni di apertura, chiusura e lettura del file possono generare eccezioni: alcune di queste vengono gestite direttamente dal program-ma, altre vengono inoltrate dal metodo main al Runtime Java.

import java.io.*;

public class ProvaEccezioni {

  public static void main(String argv[]) throws IOException {
    if(argv.length!=1)
      throw new IllegalArgumentException("Uso:
                java ProvaEccezioni <filename>");

      BufferedReader i = null;
      try {
        // throws FileNotFoundException
        i = new BufferedReader(new FileReader(argv[0]));
        String s = i.readLine(); // throws IOException
        while(s != null) {
          System.out.println(s);
          s = i.readLine(); // throws IOException
        }
      }
      catch(FileNotFoundException fnfe) {
        System.out.println("Il file indicato non è stato trovato");
        fnfe.printStackTrace();
      }
      catch(IOException ioe) {
        System.out.println("Errore in lettura del file");
        ioe.printStackTrace();
      }
      finally {
        i.close(); // throws IOException
      }
   }
}

 

Invito all'approfondimento
La gestione delle eccezioni è un tema molto delicato nello sviluppo di software. In linguaggi come il C, molte funzioni segnalano una condizione eccezionale dando come risposta un numero negativo, il cui valore permette al programmatore di capire la natura del problema.

pid = fork();
if(pid < 0) {
printf("Errore");
exit(0);
}

Il meccanismo di gestione delle eccezioni di Java ha un grosso pregio rispetto a quello appe-na descritto: la segnalazione della condizione di errore utilizza un canale diverso rispetto a quello normalmente usato da una funzione per comunicare la risposta. In questo caso non si corre il rischio che l'utente tratti una segnalazione di errore come una risposta valida. Questa proprietà ha un'importanza maggiore di quanto si sarebbe portati a credere: un episodio re-almente accaduto permetterà di chiarire il concetto.

Il 3 giugno 1980, nel cuore della notte, la console del centro operativo del Commando Aereo Strategico di Offut nel Nebraska si attivò all'improvviso per segnalare l'arrivo di 2 Missili SLBM diretti contro gli Stati Uniti. Dopo 18 secondi, il sistema segnalò un ulteriore lancio di missili balistici. Immediatamente il personale del SAC si mise in contatto con il NORAD, che dichiarò di non aver rilevato nessun lancio; ma dopo pochi istanti, anche le console del Cen-tro di Comando Militare Nazionale, al Pentagono, presero a segnalare l'arrivo di un gruppo di SLBM. La situazione era di una gravità senza precedenti: in risposta venne avviata la pro-cedura di ritorsione. L'ufficiale responsabile del SAC ordinò a tutti gli equipaggi di prepara-re i B-52 al decollo nel più breve tempo possibile; i responsabili dei missili a terra vennero messi in stato di massima allerta; tutti gli aerei per il controllo della battaglia si preparano a decollare. Alle Hawaii, il Centro di Comando Aerotrasportato del Pacific Command prese il volo in modo da garantire la comunicazione con le navi da guerra statunitensi.

Nel frattempo, gli ufficiali più alti in grado del NORAD, del SAC e del Centro di Comando Militare Nazionale vennero convocati per una Conferenza di Valutazione della Minaccia: du-rante la Guerra Fredda, questa circostanza costituiva il primo passo formale verso la terza guerra mondiale.

Ma qualcosa non quadrava: il NORAD non aveva ricevuto nessuna segnalazione di attacco; inoltre, i dati apparsi sulle console del SAC e del Comando Nazionale non seguivano uno schema logico: i diversi centri di comando presentavano dati discordanti. Dopo 3 minuti e 12 secondi di febbrili consultazioni, lo stato di allerta venne abolito: l'episodio venne classificato come un falso allarme.

Per chiarire la causa dell'incidente, il NORAD decise di lasciare il sistema nella stessa confi-gurazione. Tre giorni dopo, alle 15.38, il SAC ricevette nuovamente indicazioni di un attacco ICBM. Nonostante fosse quasi certo che si trattasse di un nuovo falso allarme, le procedure di emergenza vennero ugualmente eseguite: ancora una volta, le Fortezze Volanti vennero preparate per quella che sarebbe potuta essere la loro ultima missione.

Finalmente venne fatta luce sul mistero: all'origine di questo grave incidente, c'era un chip 74175 difettoso in un computer Data General, una macchina che veniva utilizzata come mul-tiplexer per le comunicazioni. Questa macchina aveva il compito di raccogliere i risultati del-le analisi dai sensori, per poi inviarli al NORAD, al SAC, al Comando Nazionale e al Quar-tier Generale di Ottawa, in Canada. All'epoca, i collegamenti dedicati alle comunicazioni erano verificati attraverso l'invio continuo di messaggi filler: questi messaggi avevano lo stes-so formato dei messaggi di attacco, con uno zero nel campo che indicava la quantità di missili individuati. Il sistema non utilizzava alcun meccanismo per verificare l'esistenza di problemi nella fase di creazione del pacchetto: il difetto di un chip da poche centinaia di dollari aveva riempito a caso il campo "missili individuati", trasformando un innocuo messaggio di keep alive in un segnale di allarme che avrebbe potuto scatenare una guerra accidentale.


Figura 3
- Il Centro di Controllo del NORAD, presso il Monte Cheyenne

 

Conclusioni
In questo articolo abbiamo analizzato il meccanismo delle eccezioni, un costrutto molto usa-to all'interno del linguaggio Java. Oltre alla sintassi di base, abbiamo anche approfondito in che modo sia possibile creare eccezioni personalizzate per gestire situazioni anomale nei pro-pri programmi. Con questo articolo termina il corso base di Java. Un saluto a tutti coloro che lo hanno seguito fino in fondo.

Discutiamone insieme:
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it