Java/Gestione delle eccezioni

Wikibooks, manuali e libri di testo liberi.

Indice

[modifica] Lanciare le eccezioni

Un programma può essere soggetto ad errori, niente e nessuno è infallibile.
Gli errori possono essere causati da input errati e da qualsiasi motivo non previsto dal programmatore. Da ora chiameremo l'errore "eccezione" dove si intende che vi è una situazione critica e/o non prevista nel normale flusso di compilazione o esecuzione del programma: è diverso da "errore" dove si intende una carenza logica o di sintassi, strutturale del programma, alla quale non può esserci alcun rimedio.

Se l'anomalia o eccezione si verificano, fanno bloccare il programma, causandone probabilmente un'anormale terminazione. Possiamo evitare la terminazione e blocco del programma gestendo le eccezioni. Per gestire le eccezioni si usano le seguenti parole chiavi:

  • throw
  • try {}
  • catch (){}
  • finally {}
  • throws

Poniamo per esempio di dover fare un programmino per la divisione di interi.
Possiamo farlo ma dobbiamo pensare che la divisione per zero non è ammessa o che l'utente possa mettere inconsapevolmente una stringa al posto di un numero o all'interno del codice vi è una chiamata ad un elemento nullo o altre cose impreviste. Sono tutti casi dove si potrebbe generare una eccezione.

Quando si generano eccezioni in realtà vengono creati degli oggetti. Le eccezioni sono degli oggetti veri e propri e vengono istanziati dalla JVM quando c'è qualcosa che non va nel programma. Le eccezioni sono istanze delle sottoclassi di Throwable: Error ed Exception e sottoclassi di queste. Le anomalie di tipo Error sono così gravi e fatali, per il programma, tanto che non si possono gestire e non ce ne occuperemo.

Le eccezioni possono essere di tipo generico o di tipo specifico, possono cioè essere eccezioni standard o riguardare un problema specifico. Sono oggetti della classe Exception le eccezioni standard e sono oggetti delle sottoclassi di Exception le eccezioni specifiche. Oltre queste le eccezioni possono essere anche di un terzo tipo, eccezioni di tipo definito dal programmatore, ovvero oggetti di classi scritte dal programmatore, non presenti nelle classi standard.

Le eccezioni si generano per volontà esplicita del programmatore o ad opera della jvm autonomamente.

Il programmatore può appositamente generare una eccezione usando la parola chiave throw. Verrà lanciata l'eccezione quando si incorrerà in tale costrutto. Occorre specificare che tipo di eccezione (oggetto) si lancia. Esempio:

  throw new Exception();

Se solleviamo una eccezione con la clausola throw siamo costretti a gestirla altrimenti il compilatore segnala delle irregolarità e non trasformerà il sorgente in bytecode.

Quando le eccezioni vengono lanciate dalla JVM il programma termina, chiudendosi, e la stessa jvm ci avvertirà del motivo della chiusura mettendo a video un messaggio. Questo messaggio è esplicativo del perché e dove si è verificato il problema. Tale messaggio (una "traccia di memoria": StackTrace) indica il tipo di errore o meglio, eccezione, il nome del metodo e il numero della riga in cui si è determinata.

La Jvm può lanciare una eccezione autonomamente, sia in fase di compilazione che di esecuzione, è come se mettesse lei stessa la clausola throw ed il suo oggetto e tipo, nel codice.

Nel programmino della divisione di interi la jvm lancia una eccezione quando viene chiesta la divisione per zero ( che in java non è permessa con i numeri interi).

La JVM, in maniera autonoma, può lanciare eccezioni sia in fase di compilazione sia in fase di esecuzione. Alcuni tipi cioè li lancia in fase di compilazione altri in fase di esecuzione. Vi è, in Java, da una parte l'obbligo e dall'altra la possibilità volontaria di gestire le eccezioni. Siamo obbligati dove alcuni oggetti e classi richiedono espressamente una gestione delle eccezioni, e quetso avviene in fase di compilazione. Occorre la gestione prima altrimenti non la JVM si rifiuta di compilare, diciamo quindi in fase di compilazione, queste eccezioni si dicono "checked", verificate, in fase di compilazione appunto. E' invece opzionale gestirle, è comunque meglio farlo, quando le eccezioni possono scaturire da una variante anomala e inaspettata del normale flusso di esecuzione, in fase di esecuzione quindi e queste si dicono "unchecked", non verificate preventivamente. Vediamole.

[modifica] In fase di compilazione

Il compilatore avverte preventivamente la possibilità di sollevamento delle eccezioni dalle istruzioni del programma sorgente e si accerta e si assicura che queste siano gestite. Se trova una possibile eccezione non compila: non otterremo il file.class ma una stampata a video di richiesta di gestione delle istanze che le causano. Queste si chiamano "eccezioni controllate". Quindi dobbiamo gestirle per forza. Anche quando il programmatore mette la clausola throw per creare una eccezione volontariamente, si rientra in questo gruppo, per cui si è obbligati a gestirla. Questo tipo di eccezioni (oggetti) appartengono a Exception e tutte le sue sottoclassi con l'esclusione del filone delle RuntimeException.

[modifica] In fase di esecuzione

In fase di esecuzione vengono lanciate tipi di eccezioni (oggetti) appartenenti a RuntimException e sue sottoclassi. L'eccezione viene lanciata solo quando si verifica una situazione anomala al normale funzionamento del programma (che normalmente funziona). Quì non siamo obbligati, ma se sappiamo dove e quale potrebbe essere il problema, l'anomalia, è consigliabile gestirla, oppure scrivere il programma in modo che non possano verificarsi anomalie. Per esempio scrivere il programmino della divisione con un controllo preventivo in modo che l'utente non metta lo zero come divisore. Queste si chiamano "eccezioni non controllate", non controllate perché il compilatore non ci avverte della loro possibile insorgenza e né fa controlli ed è solo quando si esegue il byte-code (il file.class) che la jvm le lancia. È consigliabile gestirle solo quando si è sicuri della loro possibile nascita e da quale metodo possono scaturire, perché altrimenti queste eccezioni possono essere lanciate ovunque all'interno del codice ed è dispersivo catturarle tutte. Gestirle in termini probatori e generali significa diminuire la leggibilità del codice, aumentare la laboriosità nel crearlo, appesantirlo. Alternativa è scrivere codice in maniera tale che non ci sia criticità e possibili anomalie di esecuzione.

Esempio di eccezione lanciata dalla JVM in esecuzione:

public class Div0 {
 
    public static void main(String[] args) {
 
        int dividendo = 10;
        int divisore = 0;
        int operazione= dividendo / divisore;
        System.out.println(operazione);
    }
 
}

Con terminazione anomala del programmino e questo output:

Exception in thread "main" java.lang.ArithmeticException: / by zero
        at Div0.main(Div0.java:18)
Java Result: 1

[modifica] Gestire le eccezioni

Si è parlato solo ed esclusivamente di creare o meglio, lanciare, eccezioni, con il risultato di terminare il programma con un messaggio esplicativo dell'errore da parte della jvm. Per evitare la terminazione del programma, le eccezioni, vanno gestite.

Gestendo le eccezioni assicuriamo al programma una robustezza e continuità di esecuzione. Eventuali input errati o situazioni eccezionali non farebbero più disastri ma passerebbero senza causare interruzioni.

Con il blocco try{} possiamo circoscrivere e delimitare il codice che potrebbe dare eccezioni. Con il blocco catch(){} possiamo catturare e gestire l'eccezione che viene scaturita dal try{}. Il blocco catch(){} deve sempre seguire il try{}. Con il blocco finally{} possiamo fare eseguire comunque una istruzione sia che sia stata sollevata e catturata una eccezione oppure no. Tutti questi blocchi, in caso di necessità, si possono annidare.

Vediamoli meglio.

All'interno del blocco try{} si inseriscono le istruzioni che devono o possono generare una o più eccezioni.

All'interno del blocco catch(){} si inserisce il tipo di eccezione e le istruzioni per gestirla, con l'inserimento di parametri ed un corpo. È sintatticamente simile e funzionalmente usabile quasi come un metodo. Il parametro deve essere il tipo di eccezione da catturare e gestire. Il catch(){} cattura l'eccezione verificata nel try{} solo se è compatibile con quella indicata dal suo parametro.

Se vi sono più tipi di eccezioni in un unico try{} è possibile utilizzare più catch(){} specifici, tanti quanti sono i tipi di eccezione generabili da quel codice, oppure un unico catch(){} con un parametro di tipo di eccezione generico, che cattura tutti i tipi, al prezzo di una minore precisione e maggiore genericità, se usiamo questo, altri catch(){} seguenti cattureranno niente e quindi sono superflui, non servono.

All'interno di finally{} si inseriscono istruzioni che devono essere processate comunque e sempre, eccezione o meno. finally{} è facoltativo. Se si verifica l'eccezione finally{} eseguirà il suo codice dopo quello del catch(){}, se non si verifica eseguirà il suo codice dopo quello del try{} (nessuna eccezione vuol dire di conseguenza nessuna cattura e gestione di questa: perciò si salta il catch(){} ), quindi lo eseguirà sempre.

La sintassi è questa:

try{
   istruzioni che possono lanciare eccezioni
        }
  catch (ClasseDellaEccezione nomeoggetto) {
            istruzioni per gestirla
        }

Sintassi con finally:

try{
   istruzioni che possono lanciare eccezioni
        }
  catch (ClasseDellaEccezione nomeoggetto){
            istruzioni per gestirla
        }
  finally{
        istruzioni che si faranno comunque eccezione o meno
        }

Si veda il codice esemplificato nella sezione "Esempi" del libro.

[modifica] Delegare la gestione

Abbiamo la possibilità di gestire le eccezioni in un altro posto rispetto a dove si verificano e ciò può esserci utile in varie occasioni. Vale a dire che deleghiamo la gestione delle eccezioni altrove senza trattarle nello stesso posto da dove vengono lanciate. “Altrove” è quella parte di codice che richiama le istruzioni passibili di eccezioni ed è "quella parte" quella che va messa dentro il try {} . Per delegare la gestione delle eccezioni altrove dobbiamo usare il costrutto throws.

throws si usa appena dopo i parametri del metodo, all'interno del quale si può sollevare una eccezione, con l'indicazione della classe dell'eccezione. Vi possono essere più classi di eccezioni separate da virgole, quindi dallo stesso metodo possono lanciarsi eccezioni diverse.

All'interno di questo metodo potremo inserire le nostre istruzioni senza darci la pena di gestire le eccezioni (quindi niente try-catch), dovremo poi farlo da un "altra parte".

class Miaclasse {
 
public miometodo() throws Classeccezione{
codice che può generare eccezioni ...;
 
}}


L'"altra parte" è il metodo chiamante ed è questo che va messo nel try-catch e gestito, come si vede dall'esempio sotto: all'interno del metodo main della classe Prova c'è il chiamante "mia.miometodo()" il quale deve andare nel costrutto try-catch.

Sintassi:

public class Prova {
 
    public static void main(String[] args) {
 
        try{
 
Miaclasse mia= new Miaclasse();
mia.miometodo();    
 
         }catch(Classeccezione e){e.printStackTrace();}
      }}
 
class Miaclasse {
 
public miometodo() throws Classeccezione{
 codice che può generare eccezioni ...;
      }}

Come si vede sopra dalla classe "Miaclasse" abbiamo delegato la gestione delle eccezioni alla classe "Prova", Le eccezioni vengono lanciate in "Miaclasse" ma vengono gestite in "Prova".

Il metodo chiamante può essere una istruzione del corpo di un altro metodo e se questo metodo a sua volta ha l'istruzione throws allora l'eccezione viene di nuovo trasferita.

Si può arrivare così risalendo al metodo principale che per ultimo almeno deve avere all'interno il blocco try-catch altrimenti l'eccezione non viene gestita e il programma si termina.

Attenzione a non confondere le clausole throws e throw, sono assolutamente diverse: throws trasferisce la gestione dell'eccezione al metodo chiamante, throw lancia l'eccezione. Occhio alla "s" finale.


[modifica] Creare nuovi tipi di eccezioni

Le eccezioni sono degli oggetti oppure istanze di classi. Queste classi sono definite già nelle API (application programming interface) e in pratica sono delle "librerie" cioè dei programmi ausiliari che sono necessari a tutti gli altri. Java ci fornisce una vasta gamma di classi eccezione ed è meglio usare queste. Vi sono, nonostante questo, alcuni particolari casi dove l'eccezione che si potrebbe verificare non è contemplata nelle librerie ufficiali della Sun. In questi casi possiamo crearne di nostre. Per cerare una nostra classe di eccezioni dobbiamo estendere una già esistente. Scieglieremo la classe esistente da estendere considerando se dobbiamo controllare l'eccezione da subito in compilazione o dopo, in esecuzione, cioè estenderemo la nostra classe da Exception o da RuntimeException. Altro criterio che dobbiamo avere per sciegliere la classe dalla quale estendere è che questa si deve avvicinare per tipo e caratteristiche il più possibile a quella che vogliamo creare noi. Esempio di sintassi:

public class Prova{
 
     public static void main(String[] args) {
 
        try{
 
         codice che in fase di compilazione 
             solleva eccezioni non presenti nelle API standard...;
 
        }catch(Miaeccez e){e.printStackTrace();}
    }
}
 
class Miaeccez extends Exception{
     }

oppure:

public class Prova{
 
     public static void main(String[] args) {
 
        try{
 
         codice che in fase di esecuzione potrebbe
             sollevare eccezioni non presenti nelle API standard...;
 
        }catch(Miaeccez e){e.printStackTrace();}
    }
}
 
class Miaeccez extends RuntimeException{
    }

Strumenti personali