Articoli Manifesto Tools Links Canali Libri Contatti ?
Linguaggi / Java

Design delle Eccezioni in Java e J.D.K. 1.4

Abstract
Il meccanismo delle Eccezioni è un elemento chiave nel design e sviluppo di software basati su Java anche se spesso sottovalutato o considerato causa di problemi. La nuova distribuzione della Sun per la Standard Edition (J2SE 1.4) potenzia le caratteristiche del framework di base con nuove feature che aiutano lo sviluppatore nello scrivere codice robusto e pulito.
Data di stesura: 02/07/2002
Data di pubblicazione: 23/03/2003
Ultima modifica: 04/04/2006
di Stefano Fago Discuti sul forum   Stampa

In collaborazione con ObjectWay
La gestione degli errori, in qualsiasi linguaggio di programmazione, è un elemento delicato e spesso critico: in esso confluiscono tanto considerazioni legate alla risorsa umana quanto problematiche tecniche.
Un primo punto di discussione è la stesura, di codice per il processo di error handling: il problema, a volte non banale, risiede nel fatto che il codice è esso stesso possibile fonte di nuovi bug ed implica una sensibile fase di testing e refactoring in presenza di un numero cospicuo di righe.
Un'altra problematica sorge dalla frequente pigrizia che si evidenzia negli sviluppatori per la gestione degli errori: l'error handling non viene affrontata con completezza, anche laddove sia possibile, o risulta posticipata a momenti in cui dovrebbe essere attuabile un'analisi più fine; questi momenti purtroppo potrebbero non presentarsi mai!
Problematiche tecniche, come quella della resource exhaustion, sono ugualmente condizionanti del processo di sviluppo di un software. Un esempio tipico è la situazione di stallo dovuta all'esaurimento della memoria: come è possibile attuare il recovery di un errore, con una chiamata ad una procedura, se non è istanziabile il frame da dedicare alla sua esecuzione?
Il classico approccio dei valori di ritorno speciali, tipico della programmazione strutturata, può non essere utile: nei casi dove le sfumature d'errore sono molteplici o dove gli stati plausibili per un'astrazione coprono l'intero range di valori ammessi, è impossibile individuare un valore differenziabile dagli altri come indicatore di errore.

Gli Errori secondo Java: le Eccezioni.

Nel linguaggio Java queste problematiche sono state affrontate offrendo una famiglia di classi ed un opportuno framework per la gestione degli errori. Quest'ultimo si basa sull'idea di lancio (throwing) delle eccezioni: dal momento in cui l'errore si presenta, questo viene propagato all'indietro, nello stack delle chiamate, fino a quando non si verifica un'azione di recovery o la conclusione del processo.
[Figura 1]

Figura 1

La classe java.lang.Throwable è la root class di una gerarchia di elementi che sono i soli preposti ad esprimere malfunzionamenti ed errori in un software: gli elementi che non appartengono alla gerarchia dei Throwable non possono essere usati come errore!
La classe Throwable si specializza in due rami fondamentali: java.lang.Exception e java.lang.Error. Le sottoclassi dell'astrazione Error sono generalmente riservate agli errori della J.V.M. e sono comunemente evitati nel design dell'error handling applicativo. Si noti che non risulta alcun divieto o limite esplicito nell'usare un'istanza di Error al posto di un'istanza di Exception, anche se una tale scelta si può tradurre facilmente in situazioni pericolose o di difficile gestione!
La classe java.lang.Exception si specializza in due tipologie dette eccezioni CHECKED ed eccezioni UNCHECKED : quest'ultima famiglia vede come root element la classe java.lang.RuntimeException.
[Figura 2]

Figura 2

Questa classificazione è necessaria per distinguere tra eccezioni gestite dal runtime ed eccezioni applicative che, invece, devono essere gestite dallo sviluppatore.
Il lancio di eccezioni è possibile nel corpo di un metodo grazie alla keyword throw, mentre la presenza di checked exception obbliga la dichiarazione della clausola throws nella signature del metodo.
Per entrambe le tipologie di eccezioni, il supporto fornito dal linguaggio si completa nella struttura sintattica del TRY-CATCH-FYNALLY espressa di seguito.
try {
  // codice che lancia eccezioni
} catch ( exception_type_1 id1) {
  // gestione della  exception_type_1...
} catch ( exception_type_2 id2 ) {
  // gestione della exception_type_2...
} ...
  catch ( exception_type_n idn ) {
  // gestione della exception_type_n...
} finally {
  // codice... 
}
La struttura presentata rende possibile la gestione di più errori che possono verificarsi in una porzione di codice racchiuso nel blocco try: grazie alla tipizzazione delle eccezioni verrà scatenata l'opportuna azione di risposta, espressa come blocco catch di competenza.
La clausola finally permette di eseguire del codice indipendentemente dal verificarsi o meno di eccezioni: grazie a questo meccanismo è possibile liberare risorse critiche alla fine del loro utilizzo o lasciare in uno stato consistente parte dell'ambiente alterato dall'elaborazione eseguita nel blocco try.

Difficoltà nell'uso delle Eccezioni

Nella realtà quotidiana il framework legato alle eccezioni non è stato propriamente capito ed utilizzato. Uno degli aspetti che più influenza l'uso delle eccezioni è sicuramente la produttività della risorsa umana: molti sviluppatori hanno difficoltà nel gestire A.P.I. di base per la continua necessità del catching di eccezioni, così come si hanno sensibili perdite di tempo nel concepire l'opportuna politica da adottare quando si scrive un prodotto. Collegato a questa problematica è un errore comunemente commesso, specie dai neofiti del linguaggio Java: le funzionalità espresse dal software vengono rese troppo vincolanti per la sensibile presenza di clausole throws che costringono chi sviluppa a continui catch. Il risultato è che in pochissimo tempo i clienti della nostra astrazione attueranno un pericolosissimo meccanismo di ignoring delle eccezioni!
L'abitudine dei catcher generici è un'ulteriore trappola in cui si rischia di cadere agli inizi dello sviluppo in Java: la possibilità di avere un blocco catch universale sembra risolvere molti problemi ma, in realtà, non potersi affidare alla tipizzazione ed al naturale meccanismo di differenziazione dei catcher complica il design finale e, a volte, porta al degrado delle performance del prodotto.
Un'altra cattiva prassi nel gestire gli errori è quella per cui nella signature di un metodo, la clausola throws presenta un elenco di eccezioni: la sintassi Java permette di enumerare diverse tipologie di errori per uno stesso metodo, ma le conseguenze di un abuso di questa caratteristica sono disastrose. Il problema si presenta per chi utilizza il prodotto e dovrà scegliere se creare numerosi blocchi catch o attuare il throwing delle stesse eccezioni, riproponendo la medesima problematica ad un altro livello! Il tutto viene a complicarsi in presenza di ereditarietà: ricordiamo che un metodo overridden in una classe derivata può dichiarare il throws delle sole eccezioni elencate nella clausola throws del metodo originale o di una loro sottoclasse.
Quello che risulta, in generale, è la mancanza di un design delle eccezioni che vengono viste più come un fastidio che un elemento per la creazione di software robusto; è necessario usarle perché la A.P.I. di Java è ricca di tali elementi, ma nello sviluppo il loro uso è evitato in modo più o meno efficace o viene sottostimato!

Design delle eccezioni: linee guida

Una parte delle problematiche legate alla gestione degli errori in Java derivano dal non considerare degli elementi concettuali caratterizzanti del mondo delle exception:
  1. Java è un linguaggio orientato agli oggetti e le eccezioni sono oggetti: in quanto tali possiedono un'identità, un tipo, un comportamento ed uno stato;
  2. Il termine eccezione non è casuale ma è stato voluto per ricordare che quanto si vuole esprimere è una condizione eccezionale nel flusso esecutivo di un software, non la descrizione di un flusso alternativo;
  3. Una checked exception dovrebbe essere indicativa di un problema recuperabile, mentre una unchecked exception esprime un errore non gestibile del programma.
  4. La scelta tra checked ed unchecked exception dovrebbe essere generalmente guidata da una logica di design by contract in cui la signature di un metodo esprime un ben preciso contratto tra il cliente dell'astrazione e l'astrazione stessa. In quest'ottica l'uso improprio della classe dovrebbe essere segnalato da una sottoclasse di RuntimeException, come violazione del contratto, mentre una condizione anormale in cui potrebbe ritrovarsi lo sviluppatore dovrebbe essere segnalata con un'apposita eccezione applicativa.
Guidati da queste semplici considerazioni si riesce a dedurre un design semplice ed efficace in elaborati di piccole dimensioni. La difficoltà ad attuare un opportuno design delle eccezioni in presenza di software professionali è una problematica di difficile risoluzione. Nella realtà quotidiana sono l'esperienza e l'intuito dello sviluppatore senior ad arricchire le abilità tecniche che portano a produrre la soluzione più idonea allo sviluppo in corso. In quest'ottica è possibile evidenziare delle linee guida che aiutino chi programma ad affrontare i problemi menzionati, per ottenere un design delle eccezioni:
  1. È utile evitare blocchi di catch vuoti. Nei casi in cui risultasse inevitabile è meglio commentare il motivo che ha portato all'adozione di un catch vuoto. In alcuni casi è opportuno utilizzare dei flag per indicare il verificarsi dell'errore anche se l'ignoring fa si che non risulti propagato.
  2. le eccezioni che si propagano da metodi pubblici devono appartenere allo stesso package della classe a cui appartiene il metodo.
  3. In un dato package, devono esserci tante tipologie di eccezioni quante le differenti caratteristiche di malfunzionamento o errore.
  4. Se una checked exception è lanciata da un metodo di un dato package, essa non deve essere propagata dal chiamante in un secondo package. L'eccezione dovrebbe essere trasformata in un opportuno stato di ritorno, in un'opportuna checked exception dello stesso package o in una unchecked exception riconoscibile dal sistema.
  5. È necessario che vengano lanciate eccezioni appropriate al livello di astrazione: i layer ad un più alto livello devono attuare il catch delle exception di più basso livello e lanciare eccezioni relative, o comunque significative nello stesso livello di astrazione.
  6. Promuovere l'uso delle eccezioni già presenti nella A.P.I. di Java. Scegliere tra elementi preesistenti rende il proprio prodotto facile da imparare perché vi è un riferimento a convenzioni già familiari agli sviluppatori.
  7. Non abusare dell'uso di eccezioni: il meccanismo di lancio ma soprattutto di creazione delle eccezioni è, dal punto di vista delle performance, costoso e non va sottovalutato. Approcci più comuni come quello del flag o valore di ritorno risultano ancora validi se opportunamente utilizzati. Nel Java Collections Framework è possibile vedere un esempio di questa idea in relazione agli iteratori: il metodo next() lancia un'eccezione di runtime se si cerca di ottenere un elemento successivo all'ultimo di una data collezione. In realtà questa eccezione può non apparire mai sfruttando il metodo hasNext(), che restituisce un booleano, all'interno di un loop che utilizzi l'iteratore.
  8. Se un metodo può lanciare molte eccezioni è necessario valutare la creazione di un'eccezione applicativa proprietaria che permetta di ospitare le indicazione di errore espresse dalle altre exception. L'obiettivo è quello di limitare a 3 il numero di errori presenti nella clausola throws.
  9. Promuovere la failure atomicity dei metodi: con questo termine s'intende la situazione per cui il fallimento di un metodo dovrebbe lasciare l'oggetto dell'elaborazione nello stato precedente all'invocazione del metodo stesso.
  10. L'importanza della clausola finally non è da sottovalutare e va utilizzata opportunamente; sono individuabili due principali modalità d'uso della finally:
    pre_cond();               // Se pre_cond ha successo, post_cond
    try {                     // verrà comunque eseguita...
      //  codice ...          // Se pre_cond non ha successo, post_cond
    }                         // non verrà eseguita...
    finally { 
      post_cond();
    }
    Object val = null;        // Il risultato di pre_cond è posto in val... 
    
    try {                     // Si entrerà comunque nel blocco finally, ma
      val = pre_cond();       // la post_cond verrà eseguita solo se val          
    }                         // non è nullo!
    finally { 
      if (val != null)
        post_cond();
    }
    Una particolarità della clausola finally è la sua priorità dei valori di ritorno rispetto al blocco catch eventualmente eseguito: se in un blocco finally viene scatenata un'eccezione o se si termina con un valore di ritorno, sarà quest'ultimo elemento ad essere propagato all'esterno del metodo e non quanto lanciato o ritornato nel dato blocco catch.
Un approccio al design dell'error handling prevede un uso privilegiato di eccezioni unchecked promuovendo la scelta fatta in linguaggi come Python e C# dove il concetto di eccezione controllata non è presente. L'obiettivo principale è quello di evitare le problematiche delle checked exception e favorire la creazione di codice pulito in cui la presenza di un opportuno catcher può essere posticipata sino al momento in cui è più facile gestire l'errore. Questa ipotesi di lavoro, anche se interessante e valida, non sempre è attuabile e sembra rispondere più alle esigenze di uno sviluppo pervasivo che alla soluzione di problematiche da affrontare con adeguata preparazione ed opportuna disciplina.

Merlin (J.D.K 1.4) e le Eccezioni.

Nella distribuzione più recente del linguaggio Java(J.D.K. 1.4) sono state introdotte delle importanti novità che da una parte sembrano una parziale risposta ai problemi sinora esposti e dall'altra potenziano il meccanismo preesistente.
La prima grande innovazione è espressa dal refactoring della classe Throwable. Sono stati introdotti nuovi costruttori:
  • Throwable()
  • Throwable(String message)
  • Throwable(String message, Throwable cause)
  • Throwable(Throwable cause)
e nuovi metodi:
  • public Throwable getCause()
  • public Throwable initCause(Throwable cause)
  • public StackTraceElement[] getStackTrace()
  • public void setStackTrace(StackTraceElement[] stackTrace)
Questi elementi permettono una particolare politica di traduzione delle eccezioni definita Exception Chaining. Il termine traduzione sta ad indicare la possibilità di convertire un'eccezione in un'altra che abbia maggiore utilità al fine del dubugging ed error handling. Quando tale meccanismo non è distruttivo, si mantiene traccia dell'eccezione inizialmente scatenata con il risultato che, in successive iterazioni, si ottiene una catena di exception. Questa politica consente di fornire gradi di significatività sempre crescenti per l'errore da gestire, partendo dalla prima eccezione che generalmente è un errore di basso livello, poco intelligibile dall'utente ad un più alto livello di astrazione.
[Figura 3]

Figura 3

Vediamo un esempio d'uso dei nuovi costruttori e metodi.
try {
  lowLevelOp();
} catch( LowLevelException le) {
  // nuovo costruttore per l'Exception Chaining
  throw new HighLevelException(le);
}

try {
  lowLevelOp();
} catch( LowLevelException le) {
  // inizializzazione successiva della causa della nuova eccezione
  throw new HighLevelException().initCause(le);
}
Questa stessa idea offre un naturale meccanismo di wrapping, e successiva trasformazione, delle checked exception in unchecked exception per facilitare lo sviluppo e incrementare la produttività del developer che non deve preoccuparsi del catch di eccezioni controllate. Nelle distribuzioni precedenti alla 1.4 un modo per realizzare questo obiettivo è lo sviluppo di un semplice wrapper, simile a quello che segue.
public  class ExceptionAdapter extends RuntimeException
{
  private Exception originalException;

  ExceptionAdapter(Exception e)
  {
    originalException = e;
  }
  
  public void printStackTrace()
  {
    printStackTrace(System.err);
  }
  
  public void printStackTrace(java.io.PrintStream pstream)
  {
    synchronized(pstream) {
      originalException.printStackTrace(pstream);
    }
  }

  public void printStackTrace(java.io.PrintWriter pwriter)
  {
    synchronized(pwriter) {
      originalException.printStackTrace(pwriter);
    }
  }
  
  public Exception getOriginalException()
  {
    return originalException;
  }
}
Questa classe può essere usata in presenza di checked exception per il meccanismo di traduzione:
} catch(IOException e) {
  throw new ExceptionAdapter(e);
}
La nuova exception potrà essere il soggetto di un ulteriore catching dove sarà possibile considerare l'eccezione originale che potrebbe essere riproposta con un comando di throw.
Un'ulteriore novità del J.D.K. 1.4 è la presenza di una nuova classe: java.lang.StackTraceElement che rappresenta un singolo frame nello stack prodotto in conseguenza del throw di un'eccezione. È possibile ottenere un array di istanze di questa classe grazie all'invocazione del metodo getStackTrace(), presente nella nuova classe Throwable: in questo modo lo stack delle chiamate in cui l'errore si è propagato è disponibile con differenti elementi che possono essere singolarmente consultati.
Una modalità d'uso può essere la seguente:
catch (Exception ex) {
  StackTraceElement [] elements = ex.getStackTrace();

  if (elements.length > 0) {
    System.out.println(elements[0].getClassName());
    System.out.println(elements[0].getMethodName());
    System.out.println(elements[0].getLineNumber());
    System.out.println(elements[0].getFileName());                        
  }
}
Le istanze di java.lang.StackTraceElement possono essere usate come argomento del metodo setStackTrace(...) che permette di stabilire tanto lo stack trace che verrà restituito con il metodo getStackTrace() quanto il risultato dell'invocazione del metodo printStackTrace(): in questo modo il meccanismo di traduzione viene a completarsi e può rispondere alle differenti necessità di manipolazione delle eccezioni. Può risultare interessante, a tal proposito, l'idea di sfruttare il setting dello stack trace per decidere, in un'eccezione proprietaria, quali elementi risulteranno trasmissibili e, quindi, visibili da un dato client remoto!

Conclusioni

Il meccanismo delle eccezioni in Java è sicuramente un tool completo per l'error handling, soprattutto alla luce delle innovazioni a cui è andato in contro e a cui probabilmente sarà soggetto con le prossime release. E'altrettanto vero che sussistono delle difficoltà nell'apprendere e sfruttare correttamente gli elementi legati alla gestione delle eccezioni e al design delle stesse, specie quando sono customizzate. Il meccanismo di traduzione del J.D.K. 1.4 esprime nuove potenzialità ma questo non evita la necessità di un'opportuna disciplina e preparazione: abusare dell'Exception Chaining e della Exception Traslation può essere causa di problemi significativi che spaziano dal consumo di memoria al degrado delle performance!

Informazioni sull'autore

Stefano Fago, classe 1973. Diplomato in ragioneria, ha conseguito il Diploma di Laurea in Informatica con un progetto legato alle interfacce grafiche soft-realtime in Java. Dopo esperienze in Alcatel ed Elea, ha svolto attività di consulenza come Software Developer e Trainer alla ObjectWay S.p.A. sede di Milano. Attualmente Software Designer presso la sezione Innovazione e Attività Progettuali di BPU Banca. Appassionato del linguaggio Java e di tutte le tecnolgie Object Oriented. Polistrumentista dilettante.

È possibile consultare l'elenco degli articoli scritti da Stefano Fago.

Altri articoli sul tema Linguaggi / Java.

Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.