Un programma Java può andare in errore per molti motivi:
- Errori di programmazione (divisione per zero, cast non permesso, etc)
- Errori di sistema (connessione remota chiusa, memoria non disponibile, etc)
- Errori di utilizzo (input non corretti, file inesistente, etc)
Java ha una gerarchia di classi per rappresentare le varie tipologie di errore, dislocate in package diversi, a seconda del tipo di errore.
La superclasse di tutti gli errori è la classe Throwable, nel package java.lang.
La superclasse Throwable ha due sottoclassi dirette:
- Error, errori fatali dovuti a condizioni incontrollabili e imprevedibili (risorse di sistema necessarie alla JVM, incompatibilità di versioni, etc). In genere, i programmatori non devono preoccuparsi di gestire questi errori, perché, come detto, non dipendono dal programma scritto.
- Exception, i programmatori possono gestire o non gestire questo tipo di errori, dipende dalle sottoclassi.
Le exception, chiamate in genere eccezioni, a sua volta, sono suddivise in:
- eccezioni gestibili (o checked)
- eccezioni non gestibili (o unchecked): sono tutte le sottoclassi della classe RunTimException.
La differenza sta nel fatto che, durante la compilazione, le checked devono per forza essere gestite, altrimenti danno errore di compilazione; le unchecked no, perché sono causate da circostanze non prevedibili, per esempio un valore null su cui è richiamato un metodo. Anche tutto quello che sta sotto Error è di fatto unchecked, e quindi non obbligatorio da controllare.
Esempio: scrivendo questo codice
File file = new File("javaFile123.txt"); if (file.createNewFile()) { System.out.println("New File is created!"); } else { System.out.println("File already exists."); }
l’istruzione file.createNewFile() da errore di compilazione perché deve essere gestita l’eccezione IOException.
Esempio:
Eseguendo questo programma:
public class Prova { public int divisione(int d, int s) { int t = d/s; return t; } public static void main(String[] args) { Prova p = new Prova(); p.divisione(10, 0); } }
Viene lanciata questa eccezione:
Exception in thread "main" java.lang.ArithmeticException: / by zero at util.Prova.divisione(Prova.java:11) at util.Prova.main(Prova.java:19)
La ArithmeticException, dovuta ad una divisione per zero, è una sottoclasse della RunTimeException, quindi una eccezione che viene lanciata quando si esegue il programma.
Quando un metodo va in eccezione, è interrotto nel punto in cui è stato creato l’errore. L’eccezione viene quindi rilanciata (“propagata”) al metodo chiamante, e cosi via fino ad arrivare al main e terminare l’applicazione, con l’eventuale messaggio di errore.
Per gestire una eccezione si usa la keyword try, in cui racchiudere il codice che può generare eccezioni, seguito da tante clausole catch quante sono le eccezioni da gestire.
try { ... } catch (ClasseEccezione oggettoEccezione) { ... } catch (ClasseEccezione oggettoEccezione) { .... }
Se il codice all’interno del blocco try non lancia nessuna eccezione, le clausole catch verranno ignorate. Altrimenti, verrà eseguita la clausola catch corrispondente al tipo eccezione generata.
Esempio:
try { int a[] = new int[5]; a[5] = 30 / 0; } catch (ArithmeticException e) { System.out.println("Arithmetic Exception "); } catch (ArrayIndexOutOfBoundsException e) { System.out.println("ArrayIndexOutOfBounds Exception "); } catch (Exception e) { System.out.println("Parent Exception "); } System.out.println("rimanente parte del codice");
output:
dentro finally codice rimanente...
questo perché la divisione con zero causa una ArithmeticException che viene catturata dal catch corrispondete, stampa un messaggio e poi vengono eseguite le istruzioni dopo l’ultimo catch, ossia il programma non si blocca.
Per stampare un messaggio di errore più dettagliato, con lo stack delle chiamate ai metodi in cui si è verificata l’eccezione, viene usato il metodo printStackTrace().
Nel caso si abbia necessità di eseguire alcune istruzioni da eseguire sempre, sia nel caso in cui tutto vada bene che nel caso di errore, si usa la clausola finally.
Sintassi:
try { <istruzioni-try>; } catch (<classe-eccezione1><id1>){ <istruzioni-catch1>; } ... catch (<classe-eccezioneN><idN>){ <istruzioni-catchN>; } finally { <istruzioni-finally>; }
Esempio:
try { int data = 25 / 5; System.out.println(data); } catch (NullPointerException e) { System.out.println(e); } finally { System.out.println("dentro finally"); } System.out.println("codice rimanente...");
stamperà, sia che vada tutto bene o che venga lanciata una eccezione:
dentro finally codice rimanente...
Propagazione delle eccezioni
Immaginiamo di avere una classe così definita:
public class DBEngine { ... altri metodi della classe... public void executeSQL(String sqlstm) { try { } catch (SQLException sqle) { } } // fine metodo } // fine classe
Se gestiamo l’eccezione con il try/catch, l’insorgere di qualche problematica rimarrà confinata all’interno del metodo stesso.
Ci potrebbero essere dei casi in cui si vuole informare chi ha chiamato il metodo, il chiamante, dell’esistenza di questi errori. Allora, è sufficiente aggiungere alla firma del metodo la parola chiave throws (lancia) seguita dal tipo di eccezione che viene “rilanciata”; il metodo quindi termina immediatamente e passa il controllo al gestore delle eccezioni. Le istruzioni successive non vengono eseguite:
public void myMethod (String str) throws Exception { … }
Chiunque chiami questo metodo è costretto a dover gestire l’eccezione: o la rilancia al livello superiore tramite throws o la gestisce con try–catch.
Esempio:
try { myMethod ("salve mondo"); } catch(Exception sqle) { ... fai qualcosa con l’eccezione verificatasi }
Nella clausola catch si deve catturare un’eccezione del tipo dichiarato nella throws del metodo, o di un tipo che stia più in alto nella scala gerarchica.
Un metodo può lanciare più eccezioni, di tipo diverso:
public void read(BufferedReader in) throws IOException, ClassNotFoun-dException
Se nessuna delle eccezioni è adeguata al nostro caso, Java ci da la possibilità di crearne una nuova.
Eccezioni Personalizzate
Quando definiamo nuovi oggetti, può capitare di avere necessità di creare nuovi tipi di eccezioni. E’ sufficiente estendere la classe Exception e ridefinire, eventualmente, i suoi costruttori.
Esempio:
Vogliamo che, quando si chiama il metodo compute(int a) e a>10, lanciare una eccezione personalizzata. Per crearla è sufficiente estendere la classe Exception e fare l’override del metodo toString, ed eventualmente creare metodi e costruttori adatti al nostro caso.
class MyException extends Exception { int id; public MyException(int x) { id = x; } public String toString() { return "CustomException[" + id + "]"; } } public class Sampleee { static void compute(int a) throws MyException { if (a > 10) throw new MyException(a); System.out.println("No error in prog. no exception caught"); }