A caccia di bachi

Nell'eseguire i casi di prova può succedere che si ottengano risultati inattesi o addirittura che il programma non termini.

Scoperto il malfunzionamento dobbiamo attivarci per identificare le possibili cause e correggere il codice incriminato. Questa attività è detta debugging.

Oltre alla tecnica elementare del tracciamento con messaggi sullo standard output, Java 6 mette a disposizione meccanismi più eleganti e sofisticati (stack tracing, logging e asserzioni), ed Eclipse offre un potente ambiente integrato (debugger) per esaminare passo-passo l'esecuzione del codice offrendo la possibilità di correggere il programma mentre viene eseguito.




Messaggi di tracciamento

La tecnica del tracciamento è quella più banale per cercare gli errori e probabilmente l'avete già adottata in questi mesi.

Il tracciamento consiste nell'inserire opportuni messaggi di stampa per evidenziare il valore di alcune variabili nei punti dove l'errore sembra manifestarsi, oppure anche solo per controllare che certe porzioni di codice vengano effettivamente eseguite.

Il codice inserito per il tracciamento è solitamente del tipo:

   System.out.println("messaggio di tracciamento");

Nel seguire il flusso di esecuzione di un programma con messaggi di tracciamento è buona regola visualizzare i valori dei parametri ricevuti in ingresso da ogni metodo invocato e il valore che sta per essere restituito all'uscita dal metodo.




Problemi col tracciamento

Il problema principale è che questi messaggi vengono introdotti solo per il debug e quindi devono essere tolti prima di rilasciare il programma, col rischio di:
  • doverli reinserire ogni volta che il problema si ripresenta
  • dimenticarsi di rimuoverne alcuni

Un problema secondario è che i messaggi inseriti in punti diversi del programma dovrebbero essere differenti, altrimenti potrebbero essere confusi.




Stack Tracing

Per risolvere il problema secondario descritto sopra, in Java basta stampare una traccia dello stack dei record di attivazione, che descrive come il programma sia giunto in un certo punto.

Il codice inserito per il tracciamento dello stack è solitamente del tipo:

   (new Throwable("messaggio di tracciamento")).printStackTrace(System.out);

Permane però il problema principale della tecnica di tracciamento, cioè che tutte le modifiche apportate al codice originale devono essere rimosse manualmente prima del rilascio.




Logging

A partire da Java 1.4, la classe Logger permette di inserire messaggi di tracciamento che possono essere attivati e disattivati più facilmente (senza dover ricorrere a soluzioni ad hoc comunque possibili, come l'introduzione di metodi ausiliari).

L'idea è che invece di stampare direttamente sul flusso System.out si utilizzi un oggetto di logging.

Il codice inserito per il logging è solitamente del tipo:

import java.util.logging.*;

   public class <nomeClasse>{

   // Assegna a log un oggetto di logging
   static Logger log = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);

   ... 
       // Il metodo info() invia all'oggetto di logging
       // il messaggio passato per argomento
       log.info("messaggio di tracciamento");
   ...
Per impostazione predefinita, i messaggi di tracciamento inviati all'oggetto di logging vengono visualizzati (inviandoli sullo standard error), ma basta invocare (ad esempio all'inizio del main della classe)

   log.setLevel(Level.OFF);
per disattivare la visualizzazione dei messaggi di logging.

L'uso della classe Logger prevede quindi che i messaggi di tracciamento possano eventualmente restare nel codice rilasciato, purché disattivati.




Le asserzioni

Molti errori sono dovuti alla violazione di alcune ipotesi ritenute implicite dal programmatore, ma non garantite di fatto dal codice scritto (es. accredito di importi negativi su un conto).

Un'asserzione è una condizione logica che si ritiene essere valida in un preciso punto del programma.

A partire da Java 1.4 è stato aggiunto il comando assert che verifica se un'asserzione è violata e solleva un'opportuna eccezione quando questo accade.

Però è possibile decidere al momento in cui il programma viene eseguito se abilitare o disabilitare le asserzioni (come default sono disabilitate) e se non sono abilitate l'interprete le ignora senza compromettere minimamente la performance del sistema (invece i comandi condizionali richiederebbero comunque la valutazione dell'espressione).

Per eseguire un programma con le asserzioni abilitate utilizzate il comando

   java -enableassertions nomeClasse
o più brevemente:

   java -ea nomeClasse




Usare le asserzioni

La sintassi del comando assert è la seguente:
   assert booleanExpression : expression;
Dove expression (opzionale) è una qualsiasi espressione che produca un valore di qualsiasi tipo purché non void (tipicamente può essere un messaggio di tracciamento).

Dal punto di vista semantico il comando visto sopra è equivalente a:

   if (!booleanExpression) throw new AssertionError(expression);

Si valuta booleanExpression e se questa risulta falsa si solleva un'eccezione di tipo AssertionError che reca come messaggio (la rappresentazione come stringa di) il valore prodotto da expression.

Le asserzioni devono essere usate con cautela, tenendo in mente che servono specialmente nella fase di progettazione del codice, ma saranno disabilitate in fase di utilizzo dell'applicazione. Ad esempio è buona norma:

  • Non utilizzarle al posto di controlli che debbano garantire il corretto funzionamento del programma (una volta disabilitate il programma si comporterebbe in maniera imprevista).

  • Utilizzarle per controllare che i parametri in ingresso a metodi privati siano corretti, ma non utilizzarle per controlli analoghi su metodi pubblici (che invece devono essere fatti esplicitamente con comandi condizionali non disabilitabili).

  • Utilizzarle per annotare parti di codice che riteniamo non sia possibile raggiungere.

  • Non utilizzarle con espressioni che provochino side-effect (altrimenti il programma si comporterebbe diversamente a seconda che le asserzioni siano abilitate o no).

  • Utilizzarle per controllare la consistenza di alcuni invarianti sullo stato di certi oggetti (es. per verificare il corretto ordinamento di una collezione di oggetti prima e dopo l'esecuzione di certe operazioni critiche).