Tutorial: Asserzioni in Java

di Claudio De Sio Cesari


-Introduzione alle Asserzioni

Dalla versione 1.4 di java, è stata introdotta una nuova e clamorosa caratteristica al linguaggio. Clamorosa perché si è dovuto addirittura modificare la lista delle parole chiave con una nuova: assert. Un’asserzione è un’istruzione che permette di testare eventuali comportamenti che un’applicazione deve avere. Ogni asserzione richiede che sia verificata un’espressione booleana che lo sviluppatore ritiene debba essere verificata, nel punto in cui è dichiarata. Se questa non è verificata, allora si deve parlare di bug. Le asserzioni possono quindi rappresentare un’utile strumento per accertarsi che il codice scritto si comporti così come ci si aspetta. Lo sviluppatore può disseminare il codice di asserzioni, in modo tale da testare la robustezza del codice in maniera semplice ed efficace. Lo sviluppatore può infine disabilitare la lettura delle asserzioni da parte della JVM, in fase di rilascio del software, in modo tale che l’esecuzione non venga in nessun modo rallentata. Moltissimi sviluppatori pensano che l’utilizzo delle asserzioni sia una delle tecniche di maggior successo per scovare bug. Inoltre, le asserzioni rappresentano anche un ottimo strumento per documentare il comportamento interno di un programma, favorendo la manutenibilità dello stesso.

-Sintassi

Esistono due tipi di sintassi per poter utilizzare le asserzioni:
1) assert espressione_booleana;
2) assert espressione_booleana: espressione_stampabile;
Con la sintassi 1) quando l’applicazione esegue l’asserzione valuta il valore dell’espressione_booleana. Se questo è true, il programma prosegue normalmente, ma se il valore è false viene lanciato l’errore AssertionError. Per esempio l’istruzione:

  assert b > 0;

è semanticamente equivalente a:

  if (!(b>0)) {
    throw new AssertionError();
  }


A parte l’eleganza e la compattezza del costrutto assert, la differenza tra le precedenti due espressioni è notevole. Le asserzioni rappresentano più che un’istruzione applicativa classica, uno strumento per testare la veridicità delle assunzioni che lo sviluppatore fa della propria applicazione. Se la condizione che viene asserita dal programmatore è falsa, l’applicazione terminerà immediatamente mostrando le ragioni tramite uno stack-trace (metodo printStackTrace() della classe Throwable). Infatti si è verificato qualcosa che non era previsto dallo sviluppatore stesso. È possibile disabilitarne la lettura delle asserzioni da parte della JVM, una volta rilasciato il proprio prodotto, al fine di non rallentarne l’esecuzione. Ciò evidenzia la differenza tra le asserzioni e tutte le altre istruzioni applicative.
La sintassi 2) permette di specificare anche un messaggio esplicativo tramite l’espressione_stampabile. Per esempio

  assert b > 0: b;

oppure

  assert b > 0: “il valore di b è ” + b;

oppure

  assert b > 0: getMessage();

o anche

  assert b > 0: “assert b > 0 = ” + (b > 0);

l’espressione_stampabile può essere una qualsiasi espressione che ritorni un qualche valore (quindi non è possibile invocare un metodo con tipo di ritorno void). La sintassi 2) permette quindi di migliorare lo stack-trace delle asserzioni.

 

-Progettazione per contratto

Il meccanismo delle asserzioni, deve il suo successo ad una tecnica di progettazione nota con il nome di “Progettazione per contratto” (“Design by contract”), sviluppata da Bertrand Meyer. Tale tecnica è una caratteristica fondamentale del linguaggio di programmazione sviluppato da Meyer stesso: l’Eiffel (per informazioni http://www.eiffel.com). Ma è possibile progettare per contratto, più o meno agevolmente, con qualsiasi linguaggio di programmazione. La tecnica si basa in particolare su tre tipologie di asserzioni: pre-condizioni, post-condizioni ed invarianti (le invarianti a loro volta si dividono in interne, di classe, sul flusso di controllo, etc…).
Con una
pre-condizione lo sviluppatore può specificare quale deve essere lo stato dell’applicazione nel momento in cui viene invocata un'operazione. In questo modo si rende esplicito chi ha la responsabilità di testare la correttezza dei dati. L’utilizzo dell’asserzione riduce sia il pericolo di dimenticare completamente il controllo, sia quello di fare troppi controlli (perché si possono abilitare e disabilitare). Dal momento che si tende ad utilizzare le asserzioni in fase di test e debugging, non bisogna mai confondere l’utilizzo delle asserzioni con quello della gestione delle eccezioni. Nel prossimo paragrafo verranno esplicitate delle regole da seguire per l’utilizzo delle asserzioni.
Con una post-condizione lo sviluppatore può specificare quale deve essere lo stato dell’applicazione nel momento in cui un'operazione viene completata. Le post-condizioni rappresentano un modo utile per dire cosa fare senza dire come. In altre parole è un altro metodo per separare interfaccia ed implementazione interna.
È infine possibile utilizzare il concetto di invariante, che se applicato ad una classe, permette di specificare vincoli per tutti gli oggetti istanziati. Questi possono trovarsi in un stato che non rispetta il vincolo specificato (detto “stato inconsistente”), solo temporaneamente durante l’esecuzione di qualche metodo, al termine del quale lo stato deve ritornare “consistente”.
La progettazione per contratto, è appunto una tecnica di progettazione, e non di programmazione. Essa permette per esempio di testare anche la consistenza dell’ereditarietà. Una sottoclasse infatti, potrebbe indebolire le pre-condizioni, e fortificare le post-condizioni e le invarianti di classe, al fine di convalidare l’estensione. Al lettore interessato ad approfondire le sue conoscenze sulla progettazione per contratto, consigliamo di acquistare il libro di Bertrand Meyer, “Object Oriented Software Construction”.

-Uso delle asserzioni

Per poter sfruttare l’utilità delle asserzioni all’interno dei nostri programmi, bisogna compilarli e mandarli in esecuzione utilizzando particolari accorgimenti. L’introduzione della parola chiave assert infatti, ha per la prima volta sollevato il problema della compatibilità all’indietro con le precedenti versioni di Java. Non è raro infatti trovare applicazioni scritte precedentemente all’uscita della versione 1.4 di Java, che utilizzano come nomi di variabili o metodi la parola assert. Spesso questo è dovuto proprio alla necessità di alcuni sviluppatori di simulare in Java il meccanismo delle asserzioni, fino ad allora mancante. Quindi, per compilare un’applicazione che fa uso delle asserzioni, bisogna stare attenti anche alla versione di Java che stiamo utilizzando:

- Note per la compilazione di programmi che utilizzano la parola assert

1) Se si utilizza una versione di Java precedente alla 1.4, non è possibile utilizzare le asserzioni, e assert non è nemmeno una parola chiave.
2) Se si utilizza la versione di Java 1.4 e si vuole sfruttare il meccanismo delle asserzioni in un programma, allora bisogna compilarlo con il flag “–source 1.4”, come nel seguente esempio:

  javac –source 1.4 MioProgrammaConAsserzioni.java

Se non utilizziamo il flag suddetto, allora il compilatore non considererà assert come parola chiave. Conseguentemente, programmi che utilizzano assert come costrutto non saranno compilati (perché il costrutto non sarà riconosciuto), e allo sviluppatore verrà segnalato con un warning, che dalla versione 1.4 assert è una parola chiave del linguaggio. I programmi che invece utilizzano assert come nome di variabili o metodi, saranno compilati correttamente, ma sarà segnalato lo stesso warning di cui sopra.
3) Se si utilizza la versione 1.5 di Java allora la situazione cambia nuovamente. Infatti se non si specifica il flag "-source", sarà implicitamente utilizzato il flag "-source 1.5". Se si vuole sfruttare il meccanismo delle asserzioni all’interno del programma, basterà quindi compilare senza utilizzare flag, come nel seguente esempio:

  javac MioProgrammaConAsserzioni.java

che è equivalente a

  javac - source 1.5 MioProgrammaConAsserzioni.java

ed anche a

  javac - source 5 MioProgrammaConAsserzioni.java

visto che la versione 1.5 di Java è stata pubblicizzata come "Java 5".
Se invece si vuole sfruttare la parola assert come identificatore di un metodo o di una variabile (magari perché il codice era stato scritto antecedentemente alla versione 1.4), bisognerà sfruttare il flag -source specificando una versione precedente alla 1.4. Per esempio:

  javac –source 1.3 MioVecchioProgramma.java

Purtroppo la situazione è questa, e bisogna stare attenti.

- Note per l’esecuzione di programmi che utilizzano la parola assert

Come più volte detto, è possibile in fase di esecuzione abilitare o disabilitare le asserzioni. Come al solito bisogna utilizzare dei flag, questa volta applicandoli al comando “java”, ovvero “–enableassertions” (o più brevemente “-ea”) per abilitare le asserzioni, e “–disableassertions” (o “-da”) per disabilitare le asserzioni. Per esempio:

  java –ea MioProgrammaConAsserzioni

Abilita da parte della JVM la lettura dei costrutti
assert. Mentre

  java –da MioProgrammaConAsserzioni

disabilita le asserzioni, in modo tale da non rallentare in alcun modo l’applicazione. Siccome le asserzioni sono di default disabilitate, il precedente codice è esattamente equivalente al seguente:

  java MioProgrammaConAsserzioni

Sia per l’abilitazione sia per la disabilitazione valgono le seguenti regole:
1) Se non si specificano argomenti dopo i flag di abilitazione o disabilitazione delle asserzioni, allora saranno abilitate o disabilitate le asserzioni in tutte le classi del nostro programma (ma non nelle classi della libreria standard utilizzate). Questo è il caso dei precedenti esempi.
2) Specificando invece il nome di un package seguito da tre puntini, si abilitano o si disabilitano le asserzioni in quel package e in tutti i sotto package. Per esempio il comando:

  java –ea –da:miopackage... MioProgramma

abiliterà le asserzioni in tutto le classi tranne quelle del package miopackage,
3) Specificando solo i tre puntini invece si abilitano o si disabilitano le asserzioni nel package di default (ovvero la cartella da dove parte il comando)
4) Specificando solo un nome di una classe invece si abilitano o si disabilitano le asserzioni in quella classe. Per esempio il comando:

  java –ea:... –da:MiaClasse MioProgramma

abiliterà le asserzioni in tutte le classi del package di default, tranne che nella classe MiaClasse.
N.B. : è anche possibile eventualmente abilitare o disabilitare, le asserzioni delle classi della libreria standard che si vuole utilizzare mediante i flag
“-enablesystemassertions” (o più brevemente “-esa”), e
“-disablesystemassertions” (o “-dsa”). Anche per questi flag valgono le regole di cui sopra.
N.B. : Per quanto riguarda la fase di esecuzione, non esistono differenze sul come sfruttare le asserzioni tra la versione 1.4 e 1.5… fortunatamente…
N.B. : in alcuni programmi critici, è possibile che lo sviluppatore si voglia assicurare che le asserzioni siano abilitate. Con il seguente blocco di codice statico:

  static {
    boolean assertsEnabled = false;
    assert assertsEnabled = true;
    if (!assertsEnabled)
      throw new RuntimeException(“Asserts must be enabled!”);
  }

è possibile garantire che il programma sia eseguibile solo se le asserzioni sono abilitate. Il blocco infatti, prima dichiara ed inizializza la variabile booleana assertsEnabled a false, per poi cambiare il suo valore a true se le asserzioni sono abilitate. Quindi se le asserzioni non sono abilitate, il programma termina con il lancio della RuntimeException, altrimenti continua. Ricordiamo che il blocco statico, viene eseguito un’unica volta nel momento in cui la classe che lo contiene viene caricata. Per questa ragione il blocco statico dovrebbe essere inserito nella classe del main per essere sicuri di ottenere il risultato voluto.

-Quando usare le asserzioni

Non tutti gli sviluppatori possono essere interessati all’utilizzo delle asserzioni. Un’asserzione non può ridursi ad essere un modo coinciso di esprimere una condizione regolare. Un’asserzione è invece il concetto fondamentale di una metodologia di progettazione per rendere i programmi più robusti. Nel momento in cui però, lo sviluppatore decide di utilizzare tale strumento, dovrebbe essere suo interesse utilizzarlo correttamente. I seguenti consigli derivano dall’esperienza e dallo studio dei testi relativi alle asserzioni dell’autore.
1) È spesso consigliato (anche nella documentazione ufficiale Sun), non utilizzare pre-condizioni, per testare la correttezza dei parametri di metodi pubblici. È invece raccomandato l’utilizzo delle pre-condizioni per testare la correttezza dei parametri di metodi privati, protetti o con visibilità a livello di package. Questo dipende dal fatto che un metodo non pubblico, ha la possibilità di essere chiamato da un contesto limitato, corretto e funzionante. Ciò implica che assumiamo che le nostre chiamate al metodo in questione sono corrette, ed è quindi lecito rinforzare tale concetto con un’asserzione. Per esempio supponiamo di avere un metodo con visibilità di package come il seguente:

  public class InstancesFactory {
    Object getInstance(int index) {
      assert (index == 1 || index == 2);
      switch (index) {
        case 1:
          return new Instance1();
        case 2:
          return new Instance2();
      }
    }
  }

Se questo metodo può essere chiamato solo da classi cha appartengono allo stesso package della classe InstancesFactory, allora non deve mai accadere che il parametro index sia diverso da 1 o 2, perchè tale situazione rappresenterebbe un bug.
Se invece il metodo getInstance(), fosse dichiarato public, allora la situazione sarebbe diversa. Infatti, un eventuale controllo del parametro index, dovrebbe essere considerato ordinario, e quindi da gestire magari mediante il lancio di un’eccezione:

 
 public class InstancesFactory {
    public Object getInstance(int index) throws Exception {
      if (index == 1 || index == 2) {
        throw new Exception(“Indice errato: ” + index);
      }
      switch (index) {
        case 1:
          return new Instance1();
        case 2:
          return new Instance2();
      }
    }
  }

L’uso di un’asserzione in tal caso, non garantirebbe la robustezza del programma, ma solo la sua eventuale interruzione, se fossero abilitate le asserzioni al runtime, non potendo a priori controllare la chiamata al metodo. In pratica una pre-condizione di questo tipo violerebbe il concetto object oriented di metodo pubblico.
2) È sconsigliato l’utilizzo di asserzioni laddove si vuole testare la correttezza di dati che sono inseriti da un utente. Le asserzioni dovrebbero testare la consistenza del programma con se stesso, non la consistenza dell’utente con il programma. L’eventuale input non corretto da parte di un utente è giusto che sia gestito mediante eccezioni, non asserzioni. Per esempio, condideriamo la seguente classe Data:

 
 public class Data {
    private int giorno;
    . . .
    public void setGiorno(int g) {
      assert (g > 0 && g <= 31): “Giorno non valido”;
      giorno = g;
    }
    . . .

dove il parametro g del metodo setGiorno() viene passato da un utente mediante un oggetto interfaccia, che rappresenta un interfaccia grafica:

  
...
  Data unaData = new Data();
  unaData.setGiorno(interfaccia.dammiGiornoInserito());
  unaData.setMese(interfaccia.dammiMeseInserito());
  unaData.setAnno(interfaccia.dammiAnnoInserito());
  ...

Come il lettore avrà intuito, l’utilizzo della parola chiave assert non è corretto. Infatti nel caso le asserzioni fossero abilitate in fase di esecuzione dell’applicazione, e l’utente inserisse un valore errato per inizializzare la variabile giorno, l’applicazione si interromperebbe con un AssertError! Ovviamente se le asserzioni non fossero abilitate allora nessun controllo impedirebbe all’utente di inserire valori errati. La soluzione ideale sarebbe quella di gestire la situazione tramite un’eccezione, per esempio:

  
public void setGiorno(int g) throws RuntimeException {
    if (!(g > 0 && g <= 31)) {
      throw new RuntimeException(“Giorno non valido”);
    }
    giorno = g;
  }

Ovviamente la condizione è ampiamente migliorabile…
3) L’uso delle asserzioni invece, ben si adatta alle post-condizioni ed alle invarianti. Per post-condizione intendiamo una condizione che viene verificata appena prima che termini l’esecuzione di un metodo (ultima istruzione). Segue un esempio:

  public class Connection {
    private boolean isOpen = false;

    
public void open() {
      // ...
      isOpen = true;
      // ...
      assert isOpen;
    }

    public void close() throws ConnectionException {
      if (!isOpen) {
        throw new ConnectionException("Impossibile chiudere connessioni non aperte!" );
      }
      // ...
      isOpen = false;
      // ...
      assert !isOpen;
    }
  }

Dividiamo le invarianti in interne, di classe e sul flusso di controllo.
Per invarianti interne intendiamo asserzioni che testano la correttezza dei flussi delle nostre classi. Per esempio il seguente blocco di codice:

  if (i == 0) {
  ...
  } else if (i == 1) {
  ...
  } else { // ma sicuramente (i == 2)
  ...
  }


può diventare più robusto con l’uso di un’asserzione:

   if (i == 0) {
  ...
  } else if (i == 1) {
  ...
  } else {
  assert i == 2 : “Attenzione i = ” + I + “!”;
  ...
  }

Maggiore probabilità di utilizzo di un tale tipo di invariante, è all’interno di una clausola default di un costrutto switch. Spesso lo sviluppatore sottovaluta il costrutto omettendo la clausola default, perchè suppone che il flusso passi sicuramente per un certo case. Per convalidare le nostre supposizioni, sono molto utili le asserzioni. Per esempio il seguente blocco di codice:

  switch(tipoAuto) {
    case Auto.SPORTIVA:
      ...
    break;
    case Auto.LUSSO:
      ...
    break;
    case Auto.UTILITARIA:
      ...
    break;
  }

può diventare più robusto con l’uso di un’asserzione:

  switch(tipoAuto) {
    case Auto.SPORTIVA:
      ...
    break;
    case Auto.LUSSO:
      ...
    break;
    case Auto.UTILITARIA:
      ...
    break;
    default:
      assert false : “Tipo auto non previsto : ” + tipoAuto;
  }

Per invarianti di classe, intendiamo particolari invarianti interne che devono essere vere per tutte le istanze di una certa classe, in ogni momento del loro ciclo di vita, tranne che durante l’esecuzione di alcuni metodi. All’inizio ed al termine di ogni metodo però, lo stato dell’oggetto deve tornare “consistente”. Per esempio un oggetto della seguente classe:

  public class Bilancia {
    private double peso;

  
  public Bilancia() {
      azzeraLancetta();
      assert lancettaAzzerata();
    }

  
  private void setPeso(double grammi) {
      assert grammi > 0; // pre-condizione
      peso = grammi;
    }

  
  private double getPeso() {
      return peso;
    }

  
  public void pesa(double grammi) {
      if (grammi < 0) {
          throw new RuntimeException(“Grammi < 0!”);
      }
      setPeso(grammi);
      mostraPeso();
      azzeraLancetta();
      assert lancettaAzzerata(); // invariante di classe
    }

  
  private void mostraPeso() {
      System.out.println(“Il peso è di ” + peso + “ grammi”);
    }

  
  private void azzeraLancetta() {
      setPeso(0);
    }

  
  private boolean lancettaAzzerata () {
      return peso == 0;
    }
  }

potrebbe dopo ogni pesatura, azzerare la lancetta (notare che i due soli metodi pubblici terminano con un’asserzione).
Per invarianti sul flusso di controllo intendiamo asserzioni che vengono posizionate in posti del codice che non dovrebbero mai essere raggiunte. Per esempio, se abbiamo un pezzo di codice che viene commentato in tal modo:

  public void metodo() {
    if (flag == true) {
      return;
    }
  // L’esecuzione non dovrebbe mai arrivare qui!
  }

Potremmo sostituire il commento con un asserzione sicuramente false:

  public void metodo() {
    if (flag == true) {
      return;
    }
    assert false;
  }

-Conclusioni:

Le asserzioni rappresentano un potente meccanismo per testare la robustezza delle nostre applicazioni. Tuttavia bisogna avere un po' di esperienza per sfruttarne a pieno le potenzialità. Infatti, la progettazione per contratto è un argomento complesso che va studiato a fondo per poter ottenere risultati corretti. Ciononostante anche l'utilizzo dei concetti più semplici come le post-condizioni, possono migliorare le nostre applicazioni.


HOME

CONTATTI