
- Introduzione al concetto di eccezioni
E’ possibile
definire un’eccezione come un situazione
imprevista che il flusso di un’applicazione può incontrare. È possibile gestire
un’eccezione in Java, imparando ad utilizzare cinque semplici parole chiave:
try, catch, finally, throw e throws. Sarà anche possibile creare eccezioni personalizzate
e decidere non solo come, ma anche in quale parte del codice gestirle, grazie
ad un meccanismo di propagazione estremamente potente. Questo concetto è implementato
nella libreria Java mediante la classe Exception e le sue sottoclassi.
Un esempio di eccezione che potrebbe verificarsi all’interno di un programma
è quella relativo ad un divisione tra due variabili numeriche nella quale la
variabile divisore ha valore 0. Come è noto infatti, tale operazione non è fattibile.
È invece
possibile definire un errore come
una situazione imprevista non dipendente da un errore commesso dallo sviluppatore.
A differenza delle eccezioni quindi, gli errori non sono gestibili. Questo concetto
è implementato nella libreria Java mediante la classe Error e le sue sottoclassi.
Un esempio di errore che potrebbe causare un programma è quello relativo alla
terminazione delle risorse di memoria. Ovviamente, questa condizione non è gestibile.
- Gerarchie e categorizzazioni
Nella
libreria standard di Java, esiste una gerarchia di classi che mette in relazione,
la classe Exception e la classe
Error. Infatti, entrambe
queste classi estendono la superclasse Throwable. La seguente
figura mostra tale gerarchia:

N.B. :
un’ulteriore categorizzazione delle eccezioni, è data dalla divisione delle
eccezioni in checked ed unchecked exception. Ci si riferisce alle RuntimeException (e le sue sottoclassi) come unchecked
exception. Tutte le altre eccezioni (ovvero tutte quelle che non derivano da
RuntimeException), vengono dette
checked exception. Se si utilizza un metodo che lancia una checked exception
senza gestirla da qualche parte, la compilazione non andrà a buon fine. Da qui
il termine checked exception (in italiano eccezioni controllate).
Come abbiamo
già detto, non bisognerà fare confusione tra il concetto di errore (problema
che un programma non può risolvere) e di eccezione (problema non critico gestibile).
Il fatto che sia la classe Exception sia la classe
Error, estendano
una classe che si chiama “lanciabile” (Throwable), è dovuto al meccanismo con cui la Java Virtual Machine
reagisce quando si imbatte in una eccezione-errore. Infatti, se il nostro programma
genera un’eccezione durante il runtime, la JVM istanzia un oggetto dalla classe
eccezione relativa al problema, e “lancia” l’eccezione appena istanziata (tramite
la parola chiave throw). Se il nostro codice non “cattura” (tramite
la parola chiave catch) l’eccezione, il gestore automatico della JVM interromperà
il programma generando in output informazioni dettagliate su ciò che è accaduto.
Per esempio, supponiamo che durante l’esecuzione un programma provi ad eseguire
una divisione per zero. La JVM istanzierà un oggetto di tipo ArithmeticException (inizializzandolo
opportunamente) e lo lancerà. In pratica è come se la JVM eseguisse le seguenti
righe di codice:
ArithmeticException
exc = new ArithmeticException();
throw exc;
Tutto avviene “dietro le quinte”, e sarà trasparente al lettore.
- Meccanismo per la gestione delle eccezioni
Come già
asserito in precedenza, lo sviluppatore ha a disposizione alcune parole chiave
per gestire le eccezioni: try, catch, finally, throw e throws. Se bisogna sviluppare una parte di codice che potenzialmente
può scatenare un’eccezione è possibile circondarlo con un blocco try seguito da
uno o più blocchi catch. Per esempio:
public class Ecc1 {
public static void main(String args[])
{
int a = 10;
int b = 0;
int c = a/b;
System.out.println(c);
}
}
Questa
classe può essere compilata senza problemi, ma genererà un’eccezione durante
la sua esecuzione, dovuto all’impossibilità di eseguire una divisione per zero.
In tal caso, la JVM dopo aver interrotto il programma produrrà il seguente output:
Exception in thread "main" java.lang.ArithmeticException:
/ by zero
at Ecc1.main(Ecc1.java:6)
Un messaggio
di sicuro molto esplicativo, infatti sono stati evidenziati:
-
il tipo di eccezione (java.lang.ArithmeticException)
-
un messaggio descrittivo (/ by zero)
-
il metodo in cui è stata lanciata l’eccezione (at Ecc1.main)
-
il file in cui è stata lanciata l’eccezione (Ecc1.java)
-
la riga in cui è stata lanciata l’eccezione (:6)
L’unico
problema è che il programma è terminato prematuramente. Utilizzando le parole
chiave try e catch sarà possibile
gestire l’eccezione in maniera personalizzata:
public class Ecc2 {
public static void main(String args[])
{
int a = 10;
int b = 0;
try {
int c = a/b;
System.out.println(c);
}
catch (ArithmeticException exc)
{
System.out.println("Divisione
per zero...");
}
}
}
Quando
la JVM eseguirà tale codice incontrerà la divisione per zero della prima riga
del blocco try, lancerà l’eccezione
ArithmeticException che verrà catturata
nel blocco catch seguente. Quindi
non sarà eseguita la riga che doveva stampare la variabile c, bensì la stringa “Divisione per zero...”, con la quale
abbiamo gestito l’eccezione, ed abbiamo permesso al nostro programma di terminare
in maniera naturale. Come il lettore avrà sicuramente notato, la sintassi dei
blocchi try – catch è piuttosto
strana, ma presto ci si fa l’abitudine, perché è presente più volte praticamente
in tutti i programmi Java. In particolare il blocco catch deve dichiarare
un parametro (come se fosse un metodo) del tipo dell’eccezione che deve essere
catturata. Nell’esempio precedente il reference exc, puntava proprio
all’eccezione che la JVM aveva istanziato e lanciato. Infatti tramite esso,
è possibile reperire informazioni proprio sull’eccezione stessa. Il modo più
utilizzato e completo per ottenere informazioni su ciò che è successo, è invocare
il metodo printStackTrace() sull’eccezione
stessa:
int a =
10;
int b = 0;
try {
int c = a/b;
System.out.println(c);
}
catch (ArithmeticException exc)
{
exc.printStackTrace();
}
Il metodo
printStackTrace() produrrà in
output i messaggi informativi (di cui sopra) che il programma avrebbe prodotto
se l’eccezione non fosse stata gestita, ma senza interrompere il programma stesso.
È ovviamente fondamentale che si dichiari, tramite il blocco catch, un’eccezione
del tipo giusto. Per esempio, il seguente frammento di codice:
int a =
10;
int b = 0;
try {
int c = a/b;
System.out.println(c);
}
catch (NullPointerException exc)
{
exc.printStackTrace();
}
produrrebbe
un’eccezione non gestita, e, quindi, un’immediata terminazione del programma.
Infatti, il blocco try non ha mai
lanciato una NullPointerException, ma una ArithmeticException.
Come per i metodi, anche per i blocchi catch i parametri possono essere polimorfi. Per esempio, il
seguente frammento di codice:
int a =
10;
int b = 0;
try {
int c = a/b;
System.out.println(c);
}
catch (Exception exc) {
exc.printStackTrace();
}
contiene
un blocco catch che gestirebbe
qualsiasi tipo di eccezione, essendo Exception, la superclasse
da cui discende ogni altra eccezione. Il reference exc, è in questo
esempio, un parametro polimorfo.
È anche possibile far seguire ad un blocco try, più blocchi catch, come nel seguente esempio:
int a = 10;
int b = 0;
try {
int c = a/b;
System.out.println(c);
}
catch (ArithmeticException exc)
{
System.out.println("Divisione
per zero...");
}
catch (NullPointerException exc)
{
System.out.println("Reference
nullo...");
}
catch (Exception exc) {
exc.printStackTrace();
}
In
questo modo il nostro programma risulterebbe più robusto, e gestirebbe diversi
tipi di eccezioni. Male che vada (ovvero il blocco try lanci un’eccezione
non prevista), l’ultimo blocco catch gestirà il problema.
N.B. : è ovviamente fondamentale l’ordine dei blocchi catch. Se avessimo:
int a = 10;
int b = 0;
try {
int c = a/b;
System.out.println(c);
}
catch (Exception exc) {
exc.printStackTrace();
}
catch (ArithmeticException exc)
{
System.out.println("Divisione
per zero...");
}
catch (NullPointerException exc)
{
System.out.println("Reference
nullo...");
}
allora
gli ultimi due catch sarebbero superflui
e il compilatore segnalerebbe l’errore nel seguente modo:
C:\Ecc2.java:12: exception java.lang.ArithmeticException
has already been caught
catch (ArithmeticException
exc) {
^
C:\Ecc2.java:15: exception java.lang.NullPointerException
has already been caught
catch (NullPointerException
exc) {
^
2 errors
È anche
possibile far seguire ad un blocco try, oltre a blocchi catch, un altro blocco definito dalla parola
chiave finally, per esempio:
public class Ecc4
{
public static void main(String args[])
{
int a = 10;
int b = 0;
try {
int c = a/b;
System.out.println(c);
}
catch (ArithmeticException exc)
{
System.out.println("Divisione
per zero...");
}
catch (Exception exc) {
exc.printStackTrace();
}
finally {
System.out.println("Tentativo
di operazione");
}
}
}
Ciò che
è definito in un blocco finally, viene eseguito in qualsiasi caso, sia se viene lanciata
l’eccezione, sia se non viene lanciata. Per esempio, è possibile utilizzare
un blocco finally quando esistono
operazioni critiche che devono essere eseguite in qualsiasi caso. L’output del
precedente programma è:
Divisione per zero...
tentativo di operazione
Se invece
la variabile b fosse settata
a 2 piuttosto cha
a 0, allora l’output
sarebbe:
5
tentativo di operazione
Un classico
esempio (più significativo del precedente) in cui la parola finally è spesso utilizzata
è il seguente:
public void insertInDB() {
try
{
cmd.executeUpdate(“INSERT INTO…”)
catch (SQLException exc) {
exc.printStackTrace();
}
finally
{
connection.close();
}
}
Il metodo
precedente, tenta di eseguire una “INSERT” in un database, tramite le interfacce
JDBC offerte dal package java.sql. Nell’esempio cmd è un oggetto Statement e connection è un oggetto di tipo Connection. Il comando
executeUpdate() specifica come
parametro una stringa con codice SQL per inserire un certo record in una certa
tabella di un certo database. Se ci sono problemi (per esempio sintassi SQL
scorretta, chiave primaria già presente, etc…) la JVM lancerà una SQLException, che verrà
catturata nel relativo blocco catch. In ogni caso, dopo il tentativo di inserimento, la
connessione al database deve essere chiusa. (per una breve introduzione su JDBC,
rimandiamo il lettore all’indirizzo, http://www.claudiodesio.com/java/jdbc.htm).
N.B. : è possibile anche far seguire ad un blocco try, direttamente un blocco finally. Quest’ultimo verrà eseguito sicuramente dopo l’esecuzione del blocco try, sia se l’eccezione viene lanciata, sia se non viene lanciata. Comunque, se l’eccezione venisse lanciata, non essendo gestita con un blocco catch, il programma terminerebbe anormalmente.
N.B. : A questo
punto in molti si potrebbero chiedere il perché gestire le eccezioni con blocchi
try – catch, piuttosto
che utilizzare dei semplici “if”. La risposta sarà implicitamente data nei prossimi
paragrafi.
- Eccezioni personalizzate e propagazione dell'eccezione
Ci sono
alcune tipologie di eccezioni che sono più frequenti e quindi più conosciute
dagli sviluppatori Java. Si tratta di:
NullPointerException : probabilmente
la più frequente tra le eccezioni. Viene lanciata dalla JVM, quando per esempio
viene chiamato un metodo su di un reference che invece punta a null.
ArrayIndexOutOfBoundsException : questa eccezione
viene ovviamente lanciata quando si prova ad accedere ad un indice di un array
troppo alto.
ClassCastException : eccezione particolarmente
insidiosa. Viene lanciata al runtime quando si prova ad effettuare un cast ad
un tipo di classe sbagliato.
Queste
eccezioni appartengono tutte al package java.lang. Inoltre, se si utilizzano altri package come java.io, bisognerà
gestire spesso le eccezioni come IOException e le sue sottoclassi (FileNotFoundException, EOFException, etc…). Stesso
discorso con la libreria java.sql e l’eccezione SQLException, il package java.net e la ConnectException e così via.
Lo sviluppatore imparerà con l’esperienza come gestire tutte queste eccezioni.
È però altrettanto probabile che qualche
volta occorra definire nuovi tipi di eccezioni. Infatti, per un particolare
programma. potrebbe essere una eccezione anche una divisione per 5. Più verosimilmente,
un programma che deve gestire in maniera automatica le prenotazioni per un teatro,
potrebbe voler lanciare un’eccezione nel momento in cui si tenti di prenotare
un posto non più disponibile. In tal
caso la soluzione è estendere la classe Exception, ed eventualmente aggiungere membri e fare override
di metodi come toString(). Segue un esempio:
public class PrenotazioneException
extends Exception {
public PrenotazioneException() {
// Il costruttore di Exception chiamato inizializza la
// variabile privata message
super(“Problema con la prenotazione”);
}
public String toString() {
return getMessage() + “: posti
esauriti!”;
}
}
La “nostra”
eccezione, contiene informazioni sul problema, e rappresenta una astrazione
corretta. Tuttavia la JVM, non può lanciare automaticamente una PrenotazioneException nel caso si
tenti di prenotare quando non ci sono più posti disponibile. La JVM infatti,
sa quando lanciare una ArithmeticException ma non sa quando
lanciare una PrenotazioneException. In tal caso
sarà compito dello sviluppatore lanciare l’eccezione. Esiste infatti la parola
chiave throw (in inglese
“lancia”), che permette il lancio di un’eccezione tramite la seguente sintassi:
PrenotazioneException exc = new PrenotazioneException();
throw exc;
o equivalentemente
(dato che il reference exc poi non sarebbe più utilizzabile):
throw new PrenotazioneException();
Ovviamente
il lancio dell’eccezione dovrebbe seguire un controllo condizionale come il
seguente:
if (postiDisponibili == 0) {
throw new PrenotazioneException();
}
Il codice
precedente ovviamente farebbe terminare prematuramente il programma a meno di
gestire l’eccezione come segue:
try {
//controllo sulla disponibilità
dei posti
if (postiDisponibili == 0) {
//lancio dell’eccezione
throw new PrenotazioneException();
}
//istruzione eseguita
// se non viene lanciata l’eccezione
postiDisponibili--;
}
catch (PrenotazioneException exc){
System.out.println(exc.toString());
}
Il lettore
avrà sicuramente notato che il codice precedente non rappresenta un buon esempio
di gestione dell’eccezione: dovendo utilizzare la condizione if, sembra infatti
superfluo l’utilizzo dell’eccezione. In effetti è così! Ma ci deve essere una
ragione per la quale esiste la possibilità di creare eccezioni personalizzate
e di poterle lanciare. Questa ragione è la “propagazione dell’eccezione” per i metodi chiamanti. La potenza della
gestione delle eccezioni è dovuta essenzialemente a questo meccanismo di propagazione.
Per comprenderlo bene, affidiamoci coma la solito ad un esempio.
Supponiamo di avere la seguente classe:
public class
Botteghino {
private int postiDisponibili;
public Botteghino() {
postiDisponibili = 100;
}
public void prenota() {
try {
//controllo
sulla disponibilità dei posti
if (postiDisponibili
== 0) {
//lancio dell’eccezione
throw new PrenotazioneException();
}
//metodo che realizza la prenotazione
//
se non viene lanciata l’eccezione
postiDisponibili--;
}
catch (PrenotazioneException exc){
System.out.println(exc.toString());
}
}
}
La classe
Botteghino astrae in maniera
semplicistica, un botteghino virtuale che permette di prenotare i posti in un
teatro. Ora consideriamo la seguente classe eseguibile (con metodo main) che utilizza
la classe Botteghino:
public class GestorePrenotazioni
{
public static void main(String [] args)
{
Botteghino botteghino = new Botteghino();
for (int i = 0; i < 101; ++i){
botteghino.prenota();
System.out.println(“Prenotato posto
n° ” + i);
}
}
}
Per una
classe del genere, il fatto che l’eccezione sia gestita all’interno della classe
Botteghino, rappresenta un problema. Infatti l’output del programma sarà:
Prenotato posto n°
1
Prenotato posto n° 2
...
Prenotato posto n° 99
Prenotato posto n° 100
Problema con la prenotazione: posti esauriti!
Prenotato posto n° 101
che ovviamente
contiene una contraddizione. Gestire eccezioni è sempre una operazione da fare,
ma non sempre bisogna gestire eccezioni laddove si presentano. In questo caso,
l’ideale sarebbe gestire l’eccezione nella classe GestorePrenotazioni, piuttosto
che nella classe Botteghino:
public class GestorePrenotazioni
{
public static void main(String [] args)
{
Botteghino botteghino = new Botteghino();
try {
for (int i = 1; i <= 101; ++i){
botteghino.prenota();
System.out.println(“Prenotato
posto n° ” + i);
}
}
catch (PrelievoException exc) {
System.out.println(exc.toString());
}
}
}
Tutto
ciò è fattibile grazie al meccanismo di propagazione
dell’eccezione di Java. Per compilare la classe Botteghino però, non basta
rimuovere il blocco try – catch dal metodo prenota, ma bisogna anche utilizzare la parola chiave throws nel seguente
modo:
public void prenota() throws PrelievoException
{
//controllo sulla disponibilità
dei posti
if (postiDisponibili == 0) {
//lancio dell’eccezione
throw new PrenotazioneException();
}
//metodo che realizza la prenotazione
// se non viene lanciata l’eccezione
postiDisponibili--;
}
In questo
modo otteremo il seguente desiderabile output:
Prenotato posto n°
1
Prenotato posto n°
2
. . .
Prenotato posto n°99
Prenotato posto n°100
Problema con la prenotazione: posti esauriti!
Se non
utilizzassimo la clausola throws nella dichiarazione del metodo, il compilatore non compilerebbe
il codice precedente. Infatti, segnalerebbe che il metodo prenota potrebbe lanciare
l’eccezione PrelievoException (che è evidente al compilatore per la
parola chiave throw), e che questa,
non viene gestita. In particolare il messaggio di errore restituito sarebbe
simile al seguente:
GestorePrenotazioni2.java:5:
unreported exception PrenotazioneException; must be caught or declared to be
thrown
N.B. : Questo messaggio è una ulteriore prova delle caratteristiche di robustezza
di Java.
Con la clausola throws nella dichiarazione
del metodo, in pratica è come se avvertissimo il compilatore che siamo consapevoli
che il metodo possa lanciare al runtime la PrelievoException, e di non “preoccuparsi”,
perché gestiremo in un’altra parte del codice l’eccezione.
N.B. : Se un metodo “chiamante” vuole utilizzare un altro metodo “daChiamare”
che dichiara con una clausola throws il possibile lancio di un certo tipo di eccezione, allora,
il metodo “chiamante”, o deve gestire l’eccezione con un blocco try – catch che include
la chiamata al metodo “daChiamare”, o deve dichiarare anch’esso una clausola
throws alla stessa
eccezione. Ad esempio, ciò vale per il metodo main della classe
GestorePrenotazioni.
N.B. : Molti metodi della libreria standard sono dichiarati con clausola throws a qualche eccezione.
Per esempio molti metodi delle classi del package java.io, dichiarano
clausole throws alla IOException (eccezione
di input - output). Appare ancora più chiaro ora la categorizzazione tra eccezioni
checked ed unchecked: le checked exception devono essere per forza gestite per
poter compilare, le uncheked no, dato che si presentano solo al runtime.
N.B. : E’ possibile dichiarare nella clausola throws anche più di
una eccezione, separando le varie tipologie con virgole, come nel seguente esempio:
public void prenota()
throws PrelievoException,
NullPointerException { . . .
- Precisazione sull’override:
Quando
si fa override di un metodo, non è possibile specificare clausole throws ad eccezioni
che il metodo base non ha nella propria clausola throws. È comunque
possibile da parte del metodo che fa override, dichiarare una clausola throws ad eccezioni
che sono sottotipi di eccezioni che il metodo base, ha nella sua clausola throws. Per esempio:
public class ClasseBase {
public void metodo() throws java.io.IOException
{ }
}
class SottoClasseCorretta1 extends ClasseBase {
public void metodo() throws java.io.IOException
{}
}
class SottoClasseCorretta2 extends ClasseBase
{
public void metodo() throws java.io.FileNotFoundException
{}
}
class SottoClasseCorretta3 extends ClasseBase
{
public void metodo() {}
}
class SottoClasseScorretta extends ClasseBase
{
public void metodo() throws java.sql.SQLException
{}
}
La classe
ClasseBase ha un metodo
che dichiara nella sua clausola throws una IOException. La classe SottoClasseCorretta1 fa override
del metodo e dichiara la stessa IOException nella sua clausola throws. La classe
SottoClasseCorretta2 fa override
del metodo e dichiara una FileNotFoundException, che è sottoclasse di IOException nella sua clausola
throws. La classe
SottoClasseCorretta3 fa override
del metodo e non dichiara clausole throws. Infine la classe SottoClasseScorretta, fa override del metodo e dichiara una SQLException nella sua clausola
throws, e ciò è illegale.