Home
Le asserzioni di Java 9

27 Aprile 2018

Le asserzioni di Java 9

di

Un aspetto specifico di uno dei linguaggi di programmazione più usati al mondo, illustrato ad appassionati e specialisti.

[Pubblichiamo un estratto da Java 9, libro di presentazione della versione più recente del linguaggio omonimo, che ha visto numerose novità nella nona e nuova versione. La lettura è consigliata a chi si occupa di programmazione]. In Java un’asserzione è rappresentata, definita, dalla keyword assert che – usata nelle due forme visibili qui sotto – permette di verificare se una data espressione è vera o falsa.

assert expression_1;
assert expression_1 : expression_2;

Se la valutazione dell’espressione risulterà falsa, il programma verrà interrotto con un’eccezione di tipo AssertionError; viceversa, se sarà vera, l’esecuzione del programma continuerà normalmente, perché vuol dire che il codice sta funzionando come previsto. La seconda sintassi, rispetto alla prima, consente di costruire un’asserzione indicando, oltre a expression_1, anche un’altra espressione (expression_2) che sarà utilizzata come argomento del costruttore della classe AssertionError, se la prima espressione restituirà il valore false, per visualizzare un messaggio che indicherà il motivo del fallimento dell’asserzione medesima.

Abilitazione delle asserzioni

Di default, le asserzioni sono disabilitate, per motivi di performance. Per abilitarle si deve usare l’opzione -enableassertions (o lo shortcut equivalente -ea) con il comando java che avvia l’applicazione di interesse.

-enableassertions [:[packagename]… | :classname]

In pratica possiamo usare l’opzione nei seguenti modi:

  • senza argomenti, per esempio -enableassertions; le asserzioni saranno abilitate in tutti i package e le classi;
  • con l’argomento packagename…, per esempio -enableassertions:LibroJava9…; le asserzioni saranno abilitate nel package indicato e nei suoi sotto-package;
  • con l’argomento , per esempio -enableassertions:…; le asserzioni saranno abilitate nel package senza nome (unnamed package) nella corrente directory di lavoro;
  • con l’argomento classname, per esempio -enableassertions:LibroJava9.Capitolo11.Assertions; le asserzioni saranno abilitate solo per la classe Assertions.
Abilitazione con NetBeans

Con l’IDE NetBeans è possibile abilitare le asserzioni per un programma Java in questo modo.

  • Fare clic destro col mouse sul nome di un progetto nell’area a sinistra della finestra dell’IDE denominata Projects e dal menu contestuale attivare la voce Properties;
  • Nella finestra Project Properties attivata, fare clic, nell’area Categories, sulla voce Run;
  • Nell’area di testo con etichetta VM Options inserire l’opzione -enableassertions (o -ea) in accordo con la sintassi appena evidenziata.
  • Fare clic sul pulsante OK per confermare quanto indicato.
Disabilitazione delle asserzioni

Per disabilitare esplicitamente le asserzioni si deve usare l’opzione -disableassertions (o lo shortcut -da) con il comando java che avvia l’applicazione di interesse:

-disableassertions [:[packagename]… | :classname]

In pratica possiamo usare l’opzione nei seguenti modi:

  • senza argomenti, per esempio -disableassertions; le asserzioni saranno disabilitate in tutti i package e le classi;
  • con l’argomento packagename…, per esempio -disableassertions:LibroJava9…; le asserzioni saranno disabilitate nel package indicato e nei suoi sotto-package;
  • con l’argomento , per esempio -disableassertions:…; le asserzioni saranno disabilitate nel package senza nome (unnamed package) nella corrente directory di lavoro;
  • con l’argomento classname, per esempio -disableassertions:LibroJava9.Capitolo11.Assertions; le asserzioni saranno disabilitate solo per la classe Assertions.

La possibilità di disabilitare esplicitamente le asserzioni ha una certa utilità quando la relativa opzione è usata in modo congiunto all’opzione di abilitazione delle stesse. Così, per esempio, per abilitare il package indicato e tutti i suoi sotto-package ma per disabilitare una specifica classe potremo usare entrambe le opzioni, scritte nel seguente modo: -enableassertions:LibroJava9… -da -enableassertions:LibroJava9.Capitolo11.Assertions.

Comuni casi di utilizzo

Le asserzioni sono un meccanismo di rilevazione della correttezza funzionale di un programma che può aiutare il programmatore durante la fase del suo sviluppo. Esse rappresentano quindi uno strumento utile a verificare che il codice si sta comportando in modo atteso, ossia servono a rilevare che quel codice risulti “vero” e dunque il programma sta funzionando correttamente. Da questo punto di vista, quindi, le asserzioni sono un fondamentale strumento di defensive programming perché il programmatore cerca di difendersi, in anticipo, da possibili comportamenti inaspettati del suo applicativo. Esse dunque non vanno confuse con il meccanismo di gestione delle eccezioni, le quali invece devono essere intese per la rilevazione di anomalie di funzionamento di un programma che potrebbero accadere e che sono tipicamente “comunicate” all’utente utilizzatore. Nella sostanza è possibile pensare alle asserzioni come a un meccanismo di rilevazione di errori “interni” all’applicativo (sono cioè errori sotto un possibile controllo del programmatore) mentre alle eccezioni come a un meccanismo di rilevazione di errori “esterni” all’applicativo medesimo (sono cioè errori, anomalie funzionali, al di fuori di un possibile controllo del programmatore come, per esempio, un input utente invalido, l’assenza di un file, un problema di rete e così via). Le asserzioni sono tipicamente utilizzate per modellare delle:

  • precondizioni (precondition); indicano condizioni che devono essere vere quando un metodo è invocato ossia sono una sorta di requisito ex ante per la sua corretta “attivazione” e dunque esecuzione;
  • postcondizioni (postcondition); indicano delle condizioni che devono essere vere quando un metodo cessa il suo compito elaborativo ossia sono una sorta di requisiti ex post alla sua elaborazione (per esempio, stabiliscono se un metodo restituisce il valore attesto);
  • invarianti interne (internal invariant); indicano condizioni che devono essere sempre vere in un certo punto del codice di un programma;
  • invarianti del controllo del flusso di esecuzione (control flow invariant); indicano delle condizioni che devono essere sempre vere perché un determinato punto del codice non sia mai raggiunto;
  • invarianti di classe (class invariant); indicano delle condizioni che un’istanza di una classe deve sempre soddisfare (devono essere sempre vere) dopo la sua creazione e sia prima che dopo l’invocazione dei suoi metodi.

(Ricordo che un‘invariante, nell’ambito dell’informatica, è una condizione, o asserzione logica, che deve essere sempre vera durante l’esecuzione di una determinata porzione del codice di un programma). Vediamo ora alcuni comuni casi di utilizzo delle asserzioni.

…
enum Colors { RED, GREEN, BLUE }
class Paint
{
    public static void paint(Colors c)
    {
        // INTERNAL INVARIANT
        // Asserisco che potrò colorare solo con i colori RED, GREEN e BLUE.
        // Se, per esempio, amplierò l'enumerazione con la costante YELLOW
        // e poi la utilizzerò per l'argomento c, la chiamata di paint genererà
        // un AssertionError perché non è prevista la "colorazione" con quel colore
        // e dunque la condizione dell'invariante non è più vera, ossia non è più
        // vero che c può avere come valore solo RED, GREEN o BLUE.
        switch (c)
        {
            case RED:
                System.out.println("Disegno con il rosso…"); break;
            case GREEN:
                System.out.println("Disegno con il verde…"); break;
            case BLUE:
                System.out.println("Disegno con il blu…"); break;
            default:
                assert false : "Colore non legale [ " + c + " ]";
        }
    }
}
class Char
{
    public Character toUpper(Character c) // metodo PUBBLICO
    {
        // Qua una precondizione con assert non dovrebbe essere usata, perché
        // la verifica degli argomenti nei metodi pubblici dovrebbe essere parte
        // del "contratto" del metodo e questo "contratto" deve sempre essere
        // rispettato, dato che le asserzioni possono essere, a discrezione,
        // abilitate e soprattutto disabilitate; quel "contratto", in caso,
        // per l'appunto, di una disabilitazione delle asserzioni sarebbe violato.
        // Utilizziamo, dunque, il meccanismo di gestione delle eccezioni:
        if (Character.isDigit(c))
            throw new IllegalArgumentException("Carattere illegale [ " + c + " ]");
        Character c_conv = convert(c);
        // POSTCONDITION
        // Asserisco che l'oggetto c_conv sia sempre valorizzato
        // il metodo cioè può ritornare solo se tale condizione è soddisfatta.
        assert c_conv != null : "Nessuna conversione occorsa";
        return c_conv;
    }
    private char convert(Character c) // metodo PRIVATO
    {
        // PRECONDITION
        // Asserisco che l'argomento c sia sempre valorizzato;
        // il metodo, cioè, può svolgere il suo compito elaborativo solo se tale
        // condizione è soddisfatta.
        assert c != null : "Conversione non attuabile";
        return Character.toUpperCase(c);
    }
}
class Square
{
    private int side;
    public Square(int side) { this.side = side; }
    public int area()
    {
        assert isComputable() : "Square non valido";
        return side * side;
    }
    public int perimeter()
    {
        assert isComputable();
        return side * 4;
    }
    // CLASS INVARIANT
    // Asserisco la validità dello stato di un oggetto Square.
    // In pratica deve sempre avere un lato maggiore o uguale a 0
    // qualsiasi operazione si effettui.
    // Tipicamente il check di un invariante di classe è effettuato da un metodo
    // privato della classe di cui si testa lo stato e restituisce un valore
    // booleano che esprime, per l'appunto, il risultato di quel check:
    private boolean isComputable()
    {
        return side >= 0; // campo da verificare…
    }
}
class Numbers
{
    private enum NEP { EVEN, ODD };
    // Numeri iniziali…
    // Qua diamo per scontato che i numeri siano sempre o pari o dispari.
    private int[] even_numbers =  { 2, 4, 6, 8 };
    private int[] odd_numbers = { 1, 3, 5, 7 };
    private List<Integer> alnr = new ArrayList<>();
    public int[] getEvenNumbers() { return even_numbers; }
    public int[] getOddNumbers() { return odd_numbers; }
    // Se quest'algoritmo interno è errato oppure lo sono gli algoritmi dei metodi
    // isEven e isOdd, il flusso di esecuzione del codice in swapEven e in swapOdd
    // raggiungerà il punto che diamo per scontato non si dovrebbe mai toccare
    // e dunque sarà sollevata un'eccezione AssertionError.
    private void filter(int[] from, int[] to, NEP nep)
    {
        alnr.clear();
        for(int n : to)
        {
            if (nep == NEP.EVEN)
            {
                if (isEven(n)) alnr.add(n);
            }
            else if (nep == NEP.ODD)
            {
                if (isOdd(n)) alnr.add(n);
            }
        }
        from = new int[alnr.size()];
        for (int i = 0; i < from.length; i++)
            from[i] = alnr.get(i);
        if(nep == NEP.EVEN) even_numbers = from;
        else if(nep == NEP.ODD) odd_numbers = from;
    }
    public void evenNumbersSupplier(int[] en)
    {
        filter(even_numbers, en, NEP.EVEN);
    }
    public void oddNumbersSupplier(int[] on)
    {
        filter(odd_numbers, on, NEP.ODD);
    }
    private void swap(int[] data, int from, int to)
    {
        int temp;
        temp = data[from];
        data[from] = data[to];
        data[to] = temp;
    }
    // CONTROL FLOW INVARIANT
    // Asserisco che il controllo del flusso di esecuzione non potrà mai raggiungere
    // il punto indicato dall'assert; qua i numeri possono solo essere pari…
    public void swapEven(int from, int to)
    {
        if (isEven(even_numbers[from]) && isEven(even_numbers[to]))
        {
            swap(even_numbers, from, to);
            return;
        }
        assert false : "Linea di codice in swapEven illegalmente raggiunta";
    }
    // CONTROL FLOW INVARIANT
    // Asserisco che il controllo del flusso di esecuzione non potrà mai raggiungere
    // il punto indicato dall'assert; qua i numeri possono solo essere dispari…
    public void swapOdd(int from, int to)
    {
        if (isOdd(odd_numbers[from]) && isOdd(odd_numbers[to]))
        {
            swap(odd_numbers, from, to);
            return;
        }
        assert false : "Linea di codice in swapOdd illegalmente raggiunta";
    }
    private boolean isEven(int n) { return n % 2 == 0; }
    private boolean isOdd(int n) { return !isEven(n); }
}
public class Snippet_11_1
{
    public static void main(String[] args)
    {
        new Paint().paint(Colors.RED); // Disegno con il rosso…
        System.out.println(new Char().toUpper('a')); // A
        System.out.println(new Square(10).area()); // 100
        Numbers numbers = new Numbers();
        numbers.swapEven(0, 1);
        numbers.swapOdd(0, 1);
        numbers.getEvenNumbers(); // 4, 2, 6, 8
        numbers.getOddNumbers(); // 3, 1, 6, 8
        numbers.evenNumbersSupplier(new int[] { 8, 9, 2 });
        numbers.swapEven(0, 1);
        numbers.getEvenNumbers(); // 2, 8
        numbers.oddNumbersSupplier(new int[] { 8, 9, 7 });
        numbers.swapOdd(0, 1);
        numbers.getOddNumbers(); // 7, 9
    }
}
Rappresentazione low-level delle asserzioni

Per comprendere ancor più compiutamente il meccanismo delle asserzioni può essere interessante studiare come il compilatore ha “trasformato” il codice dello snippet appena mostrato (a tal fine mostriamo per semplicità solo un estratto del decompilato della classe Char).

class Char
{
    static final /* synthetic */ boolean $assertionsDisabled;
    Char() { super();}
    public Character toUpper(Character c)
    {
        …
        if (!$assertionsDisabled && c_conv == null)
        {
            throw new AssertionError((Object) "Nessuna conversione occorsa");
        }
        return c_conv;
    }
    private char convert(Character c)
    {
        if (!$assertionsDisabled && c == null)
        {
            throw new AssertionError((Object) "Conversione non attuabile");
        }
        return Character.toUpperCase(c.charValue());
    }
    static
    {
        $assertionsDisabled = !Char.class.desiredAssertionStatus();
    }
}

Nella sostanza il compilatore crea un campo $assertionsDisabled che durante l’inizializzazione della classe Char (nell’inizializzatore static) è popolato con un valore booleano che indica se le asserzioni sono abilitate oppure disabilitate (viene a questo scopo utilizzato il metodo public boolean desiredAssertionStatus(), dichiarato nella classe Class<T>, package java.lang, modulo java.base). Dopodiché sostituisce ogni occorrenza delle statement assert con delle statement if dove verifica, tramite $assertionsDisabled se le asserzioni sono non disabilitate e in caso affermativo verifica anche la relativa condizione dell’asserzione che, se non soddisfatta, farà infine generare un’apposita eccezione di tipo AssertionError (per esempio, c_conv != null sarà sostituita con c_conv == null che rappresenterà il “non soddisfacimento” della condizione indicata).

Ottimizzazioni non richieste

Come visto, le statement assert, sebbene “trasformate” dal compilatore, sono comunque mantenute nel .class di una classe che le utilizza. Ciononostante, il compilatore JIT di Oracle può compiere delle ottimizzazioni per ragioni di miglioramento delle performance, grazie alle quali eliminerà completamente le statement di $assertionsDisabled check se le asserzioni sono disabilitate.

L'autore

  • Pellegrino Principe
    Pellegrino Principe lavora come ricercatore informatico e capo sviluppatore software presso il Drappello Analisi e Studi della Sezione E-Government del Comando Generale della Guardia di Finanza di Roma dove è anche docente in Linguaggi di Programmazione. È appassionato dei linguaggi con una sintassi “C-like”.

Iscriviti alla newsletter

Novità, promozioni e approfondimenti per imparare sempre qualcosa di nuovo

Gli argomenti che mi interessano:
Iscrivendomi dichiaro di aver preso visione dell’Informativa fornita ai sensi dell'art. 13 e 14 del Regolamento Europeo EU 679/2016.