Eccezioni Dott. Ing. Leonardo Rigutini Dipartimento Ingegneria dell’Informazione Università di Siena Via Roma 56 – 53100 – SIENA Uff. 0577233606 [email protected] www.dii.unisi.it/~rigutini/ Eccezioni Gestione errori Un fallimento nel programma può avere diverse cause: Due aspetti nella gestione dei fallimenti: Dati errati in ingresso Errori di programmazione Ecc … Individuazione : rilevare un fallimento Ripristino : ripristinare il programma in modo che possa continuare (gestire l’errore) Il problema maggiore nella gestione degli errori è che solitamente il punto in cui viene individuato l’errore non è accoppiato al punto di ripristino, cioè il punto dove viene gestito l’errore Gestione errori Per esempio: se nella classe Integer utilizziamo il metodo parseInt per trasformare una String in un int, ma la stringa che attualmente stiamo esaminando non rappresenta un numero (es. “pippo”), abbiamo un errore (a run-time) ma il metodo parseInt non è in grado di decidere cosa fare: chiedere all’utente ? Terminare bruscamente il programma ? Il più delle volte quindi, l’errore verificatosi dentro un metodo deve essere riportato all’esterno del metodo stesso: Ritornando un valore particolare (per esempio false , -1) Lanciando una ecezione Ritorno di un valore particolare E’ possibile notificare l’avvento di un errore dentro una funzione facendo in modo che tale funzione ritorni un valore se tutto viene eseguito in maniera corretta, un altro altrimenti: I problemi di questo approccio : Es. il metodo drive(int km) di Auto, ritorna 0 se è finita la benzina, > 0 altrimenti. diventa necessario ogni volta controllare i valori di ritorno delle funzioni per verificare se sono occorsi errori o meno. In ogni funzione è necessario controllare che ogni istruzione (o quasi) sia eseguita in maniera corretta Inoltre ritornando un tipo di dato semplice (int o boolean) nessuna informazione può essere estratta dall’errore: La cosa da fare sarebbe di creare una classe apposita che sia ritornata da ogni metodo e che memorizzi le informazioni sugli errori Lanciare un eccezione L’alternativa all’uso di valori di ritorno delle funzioni, è di lanciare particolari eventi che segnalano in maniera asincrona il verificarsi dell’errore: Le eccezioni sono classi particolari di Java che memorizzano l’errore e vengono lanciate e catturate all’interno del programma: Le eccezioni Lanciare un eccezione quando avviene un errore Catturare l’eccezione per gestire l’errore Per lanciare una eccezione si usa la parola riservata throw Eccezioni Il java fornisce una serie di classi built-in per gestire le eccezioni, tutte derivate dalla classe Exception: Exception IOException EOFException FileNotFoundException MalformedURLException UnknownHostException ClassNot Found Exception CloneNot Supported Exception RuntimeException Aritmetic Exception ClassCast Exception NullPointer Exception … Es. Supponiamo di progettare la funzione drive() della classe Auto in modo che notifichi il fatto che vogliamo percorrere più km di quanti ne possiamo fare con la benzina rimasta nel serbatoio class Auto { // public void drive(double km) { double c= km/consumo; if (c > carburante) throw new Exception(“Not enough gas”); carburante=-c; } } Lancia una eccezione generica Exception Eccezioni In alternativa è possibile ovviamente derivare classi per le eccezioni che riguardano il nostro progetto Per esempio noGasException: class noGasException extends Exception { } class Auto { // public void drive(double km) { double c= km/consumo; if (c > carburante) throw new noGasException (“Not enough gas”); carburante=-c; } } Oggetti Exception Essendo una classe, ogni eccezione può essere memorizzata in una variabile oggetto: class noGasException extends Exception { } variabile E memorizza un oggetto eccezione di tipo noGasException class Auto { // public void drive(double km) { double c= km/consumo; noGasException E; if (c > carburante) { E=new noGasException (“Not more gas”); carburante=0; throw E; } carburante=-c; } } Classe Error Esiste una seconda categoria di errori interni che vengono segnalati lanciando oggetti di tipo Error. Questi errori sono errori fatali che accadono di rado e non sono controllabili dal programmatore. Ad esempio l’errore OutOfMemoryError, che viene lanciato quando non vi è più memoria disponibile. Tali situazioni non sono gestibili dal programmatore Lanciare una eccezione Quando viene lanciata una eccezione, il metodo termina immediatamente la propria esecuzione, come se fosse stato eseguito un return . L’esecuzione non procede nel metodo che aveva invocato quella funzione, ma nel gestore dell’eccezione. E’ buona regola non abusare del lancio di eccezioni: consideriamo la funzione readLine della classe BufferedReader. Questa funzione legge una riga (fino a \n) da un input stream. Questa funzione non ritorna una eccezione di tipo EOFException quando arriva in fondo allo stream. Perché? La risposta è che terminare lo stream non è una cosa eccezionale, cioè se si legge un file, è ovvio che prima o poi si arriva alla fine del file e quindi trovare EOF non è da considerarsi errore Eccezioni controllate e non-controllate Le eccezioni Java rientrano in due categorie, chiamate eccezioni controllate e non controllate Eccezioni controllate – quando chiamate un metodo che lancia una eccezione controllata, dovete specificare come gestire l’eccezione nel caso essa venga lanciata. Ad esempio tutte le eccezioni IOException sono eccezioni controllate. Eccezioni non controllate – il compilatore non richiede che teniate traccia delle eccezioni non controllate come NullPointerException, NumberFormateException ecc … Più in generale tutte le eccezioni che appartengono alle sottoclassi di RuntimeException sono eccezioni non controllate, mentre tutte la altre sottoclassi di Exception sono controllate. Eccezioni controllate e non controllate Perché ci sono due tipi di eccezioni? Un eccezione controllata descrive un problema che prima o poi può accadere, indipendentemente da quanto sia stato scritto bene il codice Le eccezioni non controllate, invece, rappresentano un errore in programmazione. Quindi diventa inutile forzare la gestione di una eccezione di questo tipo, poiché nel caso si verifichi, è necessario modificare il codice. Ad esempio: La fine inattesa di un file (EOFException) non dipende da cause che sono sotto il nostro controllo (errore sul disco o errore di rete) e quindi si è forzati a prevedere una routine di gestione di tali situazioni Un nullPointerException, invece, segnala un accesso ad una variabile non inizializzata (null) e quindi un errore nel codice che cerca di utilizzare un riferimento null. Il compilatore non verifica che gestiate un nullPointerException poiché dovreste scrivere codice che eviti l’accesso a riferimenti null Eccezioni controllate e non controllate In realtà queste categorie non sono perfette: non è colpa del programmatore se un utente inserisce un numero non corretto, ma l’eccezione NumberFormatException, lanciata da Integer.parseInt(String) è un eccezione non controllata Vedrete che la maggior parte delle eccezioni controllate accodono nella gestione dei dati in ingresso o in uscita, che è un fertile terreno per guasti esterni che non dipendono dal codice: Un file può essere stato rimosso o corretto La rete può essere disattivata Un server può essere non disponibile Ecc … Eccezioni controllate Quando viene utilizzato un metodo che lancia una eccezione controllata, è necessario specificare cosa fare: Rimandare la gestione della eccezione al metodo chiamante Catturare l’eccezione e gestirla in locale Rimandare l’eccezione al chiamante – se stiamo implementando un metodo in cui viene lanciata una eccezione controllata (o che utilizza un metodo che lancia una eccezione controllata) e non siamo in grado di gestirla, tale eccezione viene passata direttamente al metodo chiamante. Questa operazione viene fatta post-ponendo alla dichiarazione del metodo che stiamo implementando la parola di codice throws Rimandare l’eccezione al chiamante Es class noGasException extends Exception { } Ogni metodo che chiama drive() deve gestire l’eccezione di tipo noGasException class Auto { // public void drive(double km) throws noGasException { double c= km/consumo; if (c > carburante) { carburante = 0; throw new noGasException (“Not enough gas”); } carburante=-c; } } Rimandare l’eccezione al chiamante class Auto { public int checkGas() throws noGasException { if (carburante <=0 ) { throw new noGasException (“Not more gas”); } return carburante; } // public void drive(double km) throws noGasException { double c= km/consumo; if (checkGas()) { Il metodo checkGas lancia carburante = 0; un eccezione noGasException } e deve essere gestita in drive(): carburante=-c; drive rimanda la gestione alla } funzione che chiamerà drive() } Rimandare l’eccezione al chiamante Quando un metodo rimanda più eccezioni controllate, vanno elencate dopo throws separate con virgole: public void drive(double km) throws IOException, noGasException { … checkGas() <- lancia noGasException readFile() <- lancia IOException … } Rimandare l’eccezione al chiamante Ovviamente se viene fatto un throws di un tipo di eccezione, tutte i tipi di eccezioni figli sono compresi: public void drive(double km) throws Exception { … checkGas() <- lancia noGasException readFile() <- lancia IOException … } Tutte le eccezioni Sono prese da throws Exception Catturare le eccezioni Una eccezione prima o poi dovrà essere gestita. Normalmente accade che le eccezioni vengono lanciate in classi di basso livello e gestite nelle classi di alto livello, dove si ha una conoscenza dell’ambiente maggiore Per gestire una eccezione è necessario catturarla ed implementare il codice che deve essere eseguito una volta catturata. Se devono essere gestite due eccezioni, è necessario specificare il codice per ogni tipo di eccezione che viene catturata oppure catturare una eccezione padre di entrambe (ereditarietà). Catturare le eccezioni Per catturare le eccezioni si usa il costrutto: try { linee di codice del programma } catch (eccezione_tipo1 E1) { linee di codice per la gestione dell’eccezione E1 } catch (eccezione_tipo2 E2) { linee di codice per la gestione dell’eccezione E2 } … Le istruzioni contenute nel blocco di codice delimitato da try { e } sono eseguite come normale linee di codice. Appena avviene in esse una eccezione (generata localmente o derivante da qualche funzione interna al try ) , il controllo viene passato al ramo catch() relativo e vengono eseguite le istruzioni delimitate da { } Catturare le eccezioni class Auto { public int drive(double km) throws noGasException { double c= km/consumo; if (carburante <= c ) { carburante = 0; throw new noGasException (“No more gas”); } La clausola throws noGasException carburante=-c; return carburante; non è più necessaria, poiché } l’eccezione è gestita localmente nel } try - catch class Autodromo { public void run() { Auto Ferrari=new Auto(); Se il metodo drive lancia un try { eccezione noGasException Ferrari.drive(200); essa viene catturata dal catch Ferrari.stop(); } catch (noGasException E) { System.err.println(“E’ finita la benzina!!”); } } } Informazioni sulle eccezioni La classe Exception di Java prevede una serie di funzioni utiliìssime in caso di debug. Ha la possibilità di memorizzare un messaggio in fase di lancio della eccezione (nel costruttore). printStackTrace() – stampa lo stack della CPU al momento della eccezione (lista di funzioni attualmente nello stack di sistema) getMessage() – stampa il messaggio memorizzato dentro l’eccezione. E’ consigliato gestire sempre in maniera esplicita le eccezioni, eventualmente stampando sullo stdout il messaggio e lo sack: non mettere a tacere le eccezioni per nessun motivo, poiché se si verifica un errore non ce ne accorgiamo Catturare le eccezioni class Auto { public int drive(double km) throws noGasException { double c= km/consumo; if (carburante <= c ) { carburante = 0; throw new noGasException (“Not more gas”); } carburante=-c; return carburante; } } class Autodromo { public void run() { Auto Ferrari=new Auto(); try { Ferrari.drive(200); Ferrari.stop(); } Come sappiamo che è catch (noGasException E) { avvenuta una eccezione?? } } } Catturare le eccezioni class Auto { public int drive(double km) throws noGasException { double c= km/consumo; if (carburante <= c ) { carburante = 0; throw new noGasException (“Not more gas”); } carburante=-c; return carburante; } } class Autodromo { public void run() { Auto Ferrari=new Auto(); Stampa lo stack della try { Ferrari.drive(200); applicazione sullo stderr. Ferrari.stop(); Notare l’uso della variabile } noGasException E e la catch (noGasException E) { chiamata ad un metodo E.printStackTrace(); ereditato da Exception } } } La clausola finally A volte si ha bisogno di eseguire comunque delle istruzioni prima di lasciare il comando al gestore delle eccezioni. Il costrutto finally { } permette di specificare una serie di istruzioni che devono essere eseguite comunque sia che non si verifichi nessuna eccezione, sia che si sia verificata una eccezione. Un esempio classico è la chiusura di un file: se abbiamo aperto un file e si verifica una eccezione (una delle tante che possono essere catturate), è necessario chiudere il file prima di gestire l’eccezione. La clausola finally class Auto { public int drive(double km) throws noGasException { double c= km/consumo; if (carburante <= c ) { carburante = 0; throw new noGasException (“Not more gas”); } carburante=-c; return carburante; } } La stampa di “Ferrari class Autodromo { ciao!!” viene sempre public void run() { eseguita, sia che avvenga Auto Ferrari=new Auto(); l’eccezione sia che non try { avvenga. Ferrari.drive(200); Ferrari.stop(); } finally { System.out.println(“Ferrari ciao!”); } } } Note di cronaca Il 4 giugno 1996, il razzo Arianne sviluppato dall’ESA virò dalla sua rotta dopo circa 40 secondi dal lancio e dovette essere distrutto in volo per evitare pericoli La causa che innescò questo incidente fu un eccezione non gestita! Il missile conteneva due sensori (uno di riserva) che elaboravano dati e li trasformavano in informazioni riguardanti la posizione del missile. Uno dei sensori misurò una forza di accelerazione maggiore e tale valore espresso in virgola mobile doveva essere memorizzato in un intero a 16 bit. Il linguaggio ADA, utilizzato nei dispositivi, genera una eccezione nel caso di simili cast ma i programmatori avevano deciso che tale situazione non sarebbe mai accaduta e non avevano gestito l’eccezione Note di cronaca Quando avvenne il trabocco, venne lanciata l’eccezione e poiché non c’era il gestore, il sensore si spense. Il computer allora attivò il sensore di riserva, che lanciò la stessa eccezione e si spense anche lui. I progettisti non avevano previsto che due sensori si spegnessero insieme dato che le probabilità di un simile evento sono remotissime. A quel punto il razzo era privo delle informazioni sulla propria posizione e sulla rotta. … BUM !!