MokaByte 64 - Giugno 2002 
Assert in Java
tecniche e filosofia d'uso
di
Adrea Gini
La nuova release del JDK contiene una grossa novità: il supporto alle assertion. Si tratta di un costrutto molto semplice da imparare, che richiede tuttavia alcune attenzioni in fase di compilazione ed esecuzione del codice. Malgrado si tratti di un costrutto molto semplice, esso offre delle grosse possibilità, permettendo, in ultima istanza, di mettere in atto una forma semplificata di programmazione per contratto

Assert in Java: tecniche e filosofia d'uso
Una delle più importanti novità del JDK 1.4 rispetto alle precedenti versioni è l'introduzione di un servizio di Assert. Si tratta della più significativa modifica introdotta nel linguaggio dal 1997, data in cui vennero introdotte le classi interne. Questo caso tuttavia si distingue dal precedente per almeno due ragioni: in primo luogo l'introduzione delle Assert ha comportato significative modifiche nella JVM, cosa che rende il bytecode prodotto dal nuovo compilatore incompatibile con le precedenti JVM; in secondo luogo con le assert viene introdotto per la prima volta nel linguaggio Java un costrutto di meta programmazione, ossia un costrutto che invece di dire al computer "cosa deve fare" in un determinato momento, suggerisce "qualcosa che non dovrebbe mai succedere" ad un certo punto dell'esecuzione. L'esistenza di un simile costrutto permette di mettere in pratica una forma semplificata di programmazione per contratto, una tecnica di programmazione che aiuta a realizzare programmi più robusti imponendo la dichiarazione di precondizioni, postcondizioni ed invarianti.

 

Cosa sono le assert
Le assert sono istruzioni che verificano la verità di una condizione booleana, e provocano la terminazione del programma (mediante il lancio di un AssertionError) nel caso in cui tale condizione risulti falsa. Ad esempio l'istruzione:

assert a + b > 0;

provocherà la terminazione del programma qualora la somma dei valori a e b sia uguale o inferiore a zero. In prima istanza, la semantica dell'assert si riconduce ad una forma compatta per espressioni del tipo:

if(!(a+b>0))
throw new AssertException();

Al di la dell'eleganza di un costrutto compatto, esistono sostanziali differenze tra i due casi, sia sul piano tecnico che su quello filosofico. Da un punto di vista tecnico, il costrutto delle assert prevede la possibilità di disabilitare in blocco il controllo delle condizioni: come vedremo più avanti, le assert vengono usate essenzialmente in fase di test e debugging: durante la normale esecuzione, è possibile disabilitarle, eliminando in tal modo l'overhead legato alla loro gestione. Ma esiste anche una differenza assai sottile sul piano filosofico, che rende l'assert un qualcosa di completamente diverso da qualunque altro costrutto presente in Java. Contrariamente a quanto avviene con i costrutti standard dei linguaggi imperativi, una assert non rappresenta un ordine, ma un punto di vista: essa indica una condizione che il programmatore ritiene debba essere vera in un determinato momento dell'esecuzione di un programma. La violazione di tale condizione causerà la terminazione del programma, dal momento che si è verificato qualcosa che il programmatore non aveva previsto. Mai e in nessun caso una assert potrà contenere direttive che influenzino la normale esecuzione del programma.

L'utilizzo delle assert permette al programmatore di verificare la consistenza interna di un programma al fine di renderlo più stabile; nel contempo aggiunge espressività al codice, rendendolo più leggibile. Ma per comprendere a fondo la filosofia di utilizzo, può essere utile una breve introduzione.

 

Sherlock Holmes e la filosofia delle Assert
Nel racconto "L'avventura degli omini danzanti" Sherlock Holmes, il più famoso detective letterario che la storia ricordi, si trova a dover affrontare un caso di omicidio, per il quale viene accusata la persona sbagliata. L'apparenza a volte inganna, ma la logica, se usata nel giusto modo, può aiutare a rimettere le cose a posto:


Lo studio era un locale non molto grande, coperto su tre pareti dai libri, con uno scrittoio di fronte ad una finestra che dava sul giardino. Per prima cosa, dedicammo la nostra attenzione al corpo del povero gentiluomo, la cui massiccia figura giaceva in mezzo alla stanza. Le vesti in disordine indicavano che era stato bruscamente risvegliato dal sonno. Il proiettile, sparato dal davanti, era rimasto nel corpo dopo avere attraversato il cuore. Non c'erano tracce di polvere da sparo, né sulla vestaglia né sulle mani. Secondo il medico, la signora invece mostrava tracce di polvere sul viso ma non sulle mani.
"L'assenza di tracce di polvere sulle mani non significa nulla; avrebbe avuto molto significato, invece, la loro presenza", disse Holmes. "Se il proiettile non è difettoso e la polvere non schizza indietro, si possono sparare molti colpi senza che ne rimanga traccia. Ora suggerirei di rimuovere il corpo del signor Cubitt. Immagino, dottore, che lei non abbia recuperato il proiettile che ha ferito la signora?"
"Prima di poterlo recuperare occorre un complicato intervento chirurgico. Ma nella pistola ci sono ancora quattro proiettili. Due sono stati sparati, provocando due ferite, quindi il conto dei proiettili torna."
"Così sembrerebbe", convenne Holmes. "Forse può anche dirmi che fine ha fatto il proiettile che ha ovviamente colpito il bordo della finestra?"
Si era improvvisamente girato indicando, col lungo indice sottile, un foro che attraversava l'estremità inferiore del telaio della finestra, circa un pollice sopra il bordo.
"Per Giove!", esclamò l'ispettore. "Come diamine ha fatto a vederlo?"
"L'ho visto perché lo stavo cercando."
"Fantastico!", disse il dottore. "Lei ha senz'altro ragione, signore; e allora, ci deve essere stata una terza persona. Ma chi poteva essere, e come ha fatto ad andarsene?"
"E' questo il problema che dobbiamo risolvere", disse Holmes. "Ispettore Martin, lei ricorderà che le domestiche hanno dichiarato di aver sentito odore di polvere da sparo uscendo dalle loro stanze, e che le ho detto che si trattava di un elemento di estrema importanza?"
"Sì, lo ricordo; ma confesso di non aver capito il motivo della sua raccomandazione."
"Ci porta a desumere che, al momento dello sparo, tanto la finestra che la porta della stanza erano aperte. Altrimenti, il fumo dell'esplosione non si sarebbe potuto diffondere così rapidamente nella casa. Per questo, bisognava che nella stanza ci fosse corrente. Porta e finestra, però, sono rimaste aperte solo per pochi minuti."
"Come può provarlo?"
"Perché la candela non aveva sgocciolato."
"Magnifico!", esclamò l'ispettore. "Magnifico!"
"Essendo certo che, al momento della tragedia, la finestra era aperta, ho pensato che nella faccenda poteva essere coinvolta una terza persona, che aveva sparato dall'esterno. Un proiettile diretto contro questa persona avrebbe potuto colpire il telaio della finestra. Ho cercato e, voilà, c'era il segno del proiettile.


La tecnica investigativa di Holmes è basata sulla deduzione. "Se escludi l'impossibile, ciò che rimane, per quanto improbabile, non può che essere la verità", era solito dire. Tuttavia in informatica non è permesso escludere l'impossibile: manca la possibilità, come dimostrato dal Teorema di Rice, di realizzare programmi che dimostrino la correttezza logica di un generico altro programma. La frase di Holmes

"L'assenza di tracce di polvere sulle mani non significa nulla; avrebbe avuto molto significato, invece, la loro presenza"

si riflette in una celebre frase di Martin Fowler a proposito dei test basati sulle assert:

"I test rivelano la presenza di bugs, non la loro assenza"

Dal momento che non possiamo mai essere sicuri di non aver commesso errori, possiamo comunque seminare alcune trappole nei punti critici del codice, in modo da ottenere il maggior numero possibile di indicazioni qualora si verifichi un errore inaspettato.

Al pari di Holmes, possiamo dedurre l'esistenza di un errore dalla violazione di una condizione invariante (il numero dei proiettili sparati è superiore a quello dei proiettili di una sola pistola), o dalla verifica di una precondizione (la finestra aperta) unitamente alla invalidazione di una postcondizione (l'assenza di sgocciolamento della candela). Come vedremo, esiste un gran numero di situazioni in cui è utile mettere in atto questo tipo di controlli all'interno del codice.


Figura 1 - L'abilità di Sherlock Holmes nell'arte della deduzione logica
può venirci in aiuto durante la ricerca di un bug

 

Sintassi della assert
L'istruzione assert prevede due costrutti:

assert booleanExpression;
assert booleanExpression : message;

Il secondo di questi costrutti permette di specificare un messaggio da visualizzare qualora la condizione logica venga meno. Tale messaggio può contenere anche informazioni dettagliate sul caso che ha provocato il fallimento del programma, ad esempio

assert a + b > c : "La somma di " + a + " con " + b + " ha dato un risultato minore o eguale di" + c;

 

Compilazione ed esecuzione di codice contenente assert
L'uso delle assert richiede delle opzioni speciali in fase di compilazione e di esecuzione. L'uso di un esempio passo-passo dovrebbe mettere in chiaro tutto quello che è necessario sapere. Per prima cosa si proceda a copiare il seguente programma in un file dal nome AssertTest.java:

public class AssertTest {
  public static void main(String args[]) {
    byte b = 0;
    f
or ( int i = 0; i <= 64; i++) {
      assert i < 64 : "Errore di Overflow";
      b = (byte)(i * 2);
      System.out.println("b = " + b);
    
}
  }
}

Si noti che la assert in quinta riga richiede che il valore di i sia minore di 64, per evitare un errore di overflow nella istruzione successiva. Si noti anche che il ciclo for in quarta riga provocherà in ultima istanza la violazione di tale condizione.

Per fare in modo che il compilatore accetti codice contenente asserzioni, è necessario utilizzare lo speciale flag -source 1.4 da riga di comando. Tale soluzione è resa necessaria dal fatto di dover garantire la compatibilità con il codice per JDK 1.4, che permetteva di usare la parola "assert" come nome di variabile o di metodo.

javac -source 1.4 AssertTest.java

Per default le assertion sono disabilitate: se proviamo a lanciare il programma con il comando

java AssertTest

otterremo un output di questo tipo:

....
b = 122
b = 124
b = 126
b = -128

si noti che l'ultimo valore è negativo, dal momento che l'ultimo valore indotto dal ciclo for ha prodotto un overflow.

Se vogliamo abilitare il controllo delle assert in fase di esecuzione, dobbiamo usare il flag -ea

java -ea AssertTest

in questo caso il programma terminerà prima di raggiungere l'overflow:

....
b = 122
b = 124
b = 126
java.lang.AssertionError: Errore di Overflow
at AssertTest.main(AssertTest.java:5)
Exception in thread "main"

Sostanzialmente questo e' tutto ciò che è necessario sapere per utilizzare il costrutto Assert nei propri programmi. Ad ogni buon conto, i flag di attivazione dispongono di una serie di opzioni, che verranno descritte nel prossimo paragrafo.

 

Abilitazione e disabilitazione selettiva
Oltre al flag -ea (Enable Assertion), è disponibile un flag complementare -da (Disable Assertion). Per entrambi è possibile specificare un parametro che può assumere i seguenti valori:

  • Nessun valore
    le assertion vengono abilitate o disabilitate in tutte le classi (escluse le classi di sistema, disabilitate per default)

  • nomeDiPackage...
    le assertion vengono abilitate o disabilitate nel package indicato e in tutti i suoi sotto package.

  • ...
    Abilita o disabilita le assertion nel package di default.

  • nomeDiClasse
    Abilita o disabilita le assertions nella classe specificata
    In questo modo è possibile specificare in modo molto preciso e dettagliato la modalità di esecuzione. Ad esempio la seguente riga di comando esegue la classe AssertionTest, dopo aver abilitato le assertion nel package it.mokabyte.provaAssertion e nei suoi eventuali subpackage:

    java -ea:it.mokabyte.provaAssertion... AssertionTest

E' possibile replicare ciascuno di questi flag, in modo da ottenere il risultato desiderato. Il seguente comando lancia la classe AssertionTest dopo aver abilitato la le assertion nel package it.mokabyte.provaAssertion e nei suoi eventuali subpackage, con l'esclusione del sottopackage subPackage1 e della classe Class1A:

java -ea:it.mokabyte.provaAssertion... -da:it.mokabyte.provaAssertion.subPackage1... -da:it.mokabyte.provaAssertion.Class1A AssertionTest

I flag -ea e -da permettono di abilitare o disabilitare le assertion su qualunque package, compresi i package di sistema. Tuttavia, quando si usano i flag senza parametro, le assertion sono disabilitate sulle classi di sistema. Per gestire in modo esplicito l'abilitazione o la disabilitazione delle assert nelle classi di sistema, è possibile ricorrere ai flag -esa (Enable System Assertions) e -dsa (Disable System Assertions).

 

Conclusioni
Questo articolo ha introdotto il costrutto Assert, introdotto in Java a partire dal JDK 1.4. Come si è potuto constatare, si tratta di un costrutto dalla sintassi molto semplice, che richiede tuttavia qualche piccola attenzione in fase di compilazione ed esecuzione del codice. Il mese prossimo analizzeremo in profondità i principali scenari di utilizzo della assert, come costrutto per il controllo di precondizioni, postcondizioni ed invarianti.

 

Bibliografia
Questo documento ha dato il via al processo che ha portato all'introduzione delle assertion in Java.
http://www.jcp.org/jsr/detail/41.jsp

Gli appassionati di archeologia informatica possono sfogliare le specifiche originali di Oak, il linguaggio di James Gosling antenato di Java, che inizialmente includeva il supporto alle assert:
http://java.sun.com/people/jag/green/OakSpec0.2.ps

Un articolo su Java World sulle Assert:
Part 1: Understand the mechanics of Java's new assertion facility
http://www.javaworld.com/javaworld/jw-11-2001/jw-1109-assert.html
Part 2: Understand the methodology impact of Java's new assertion facility
http://www.javaworld.com/javaworld/jw-12-2001/jw-1214-assert.html?

Il teorema di Rice asserisce che qualunque proprietà non ovvia di un formalismo di calcolo è indecidibile. Una versione della dimostrazione del teorema può essere trovata su:

http://www.math.ohio-state.edu/~cgon/rices.html

La raccolta completa di romanzi e racconti di Conan Doyle, che costituisce il cosiddetto Canone, può essere consultata al seguente indirizzo:
http://www.bakerstreet221b.de/canon/

L'immagine stereotipa di Sherlock Holmes, con il berretto da caccia, la mantellina e la pipa Calabash, non rende giustizia nè alla descrizione letteraria del personaggio, nè all'opera dei numerosi illustratori che, dalla fine dell'800, hanno dato vita a rappresentazioni del detective di Baker Street. Una storia completa dell'iconografia di Sherlock Holmes può essere trovata all'indirizzo:
http://www.holmesonscreen.com/Definitive.htm

Chi fosse interessato invece alla più completa raccolta di illustrazioni sul detective di Baker Street, può consultare la Pinacoteca Holesiana:
http://www.bakerstreet221b.de/gallery.htm

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it