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.
|