JavaBase

Nota Alla fine del documento trovate le versioni scaricabili

Alberto Lovato

Java Base

Un'introduzione sintetica alle basi del linguaggio Java

Copyright 2006 Alberto Lovato

Creative Commons License

Questa opera è pubblicata sotto una Licenza Creative Commons.

L'ultima versione di questa guida è disponibile presso http://luomoradioattivo.blogspot.com/2010/04/java-base.html.

Indice

Capitolo 1 - Introduzione

La presente vuole essere una guida rapida alle basi di Java, orientata a chi già conosce il C. La trattazione sarà il più possibile sintetica, con molti brevi esempi, in modo da permettere a chi legge di assimilare velocemente i vari argomenti.

I dettagli si potranno trovare nella documentazione ufficiale di Sun, scaricabile o consultabile da java.sun.com.

Nel sito è possibile reperire, oltre alla documentazione delle API, degli ottimi tutorial su argomenti specifici.

Il primo programma Java

È tradizione che il primo programma che si presenta in una introduzione a un linguaggio di programmazione sia il cosiddetto "Hello, world!", che stampa sullo standard output (stdout) il suddetto saluto:

public class CiaoMondo

{

public static void main(String[] args)

{

System.out.println("Hello, world!");

}

}

Questo codice va salvato in un file di nome CiaoMondo.java - una classe public (nel seguito si dirà cosa significa) deve essere dichiarata in un file con lo stesso nome, case compreso.

Ora, ammesso che abbiate già scaricato e installato il JDK Sun, preferibilmente in versione 1.5 o superiore, basta compilare il sorgente con il comando:

javac CiaoMondo.java

Tuttavia questo non genererà un file direttamente eseguibile, bensì il file CiaoMondo.class - un file per ogni classe dichiarata - contenente del codice intermedio, o bytecode, che verrà in seguito eseguito dalla Java Virtual Machine (JVM) tramite il comando:

java CiaoMondo

L'alternativa: gcj

In alternativa potete usare il gcj - se avete un sistema in cui gira - il quale è in grado di generare un file eseguibile in luogo del bytecode:

gcj --main=CiaoMondo CiaoMondo.java -o ciao

Convenzioni

Nel solito sito potete trovare le coding conventions ufficiali di Sun, che stabiliscono come dovrebbe essere scritto un sorgente java.

Queste convenzioni esistono allo scopo di uniformare lo stile che usano i programmatori, in modo da minimizzare lo sforzo di leggere del codice scritto da altri. Tuttavia questi sono solo dei suggerimenti, al compilatore java non importa come è formattato o indentato il file sorgente.

Questa stessa guida - come molti altri documenti, compresi alcuni dal sito della Sun - non segue perfettamente le convenzioni Sun, in particolare per quanto riguarda le parentesi graffe in apertura di classi e metodi. Secondo me è molto più leggibile una scrittura del tipo:

class Lista

{

// corpo

}

in luogo di:

class Lista {

// corpo

}

Questo comunque va un po' a gusti personali - vedi diatriba Linus Torvalds/GNU, file Documentation/CodingStyle nel source tree di linux.

Il mio umilissimo consiglio è di usare il buon senso, in modo da scrivere del codice leggibile, con commenti sintetici e non inutili, cercando di spezzare lunghe linee con dei ritorni a capo e di inserire ogni tanto una linea vuota, per distinguere varie sezioni di codice - proprio come si fa per rendere più leggibile un lungo documento di testo.

Per quanto riguarda i nomi delle classi è convenzione che siano scritti in "camel case", ossia con la prima lettera di ogni sottoparola maiuscola, così:

class PrimaClasse

I nomi di campi e metodi (vedi il capitolo 3) invece dovrebbero essere scritti sempre in camel case, ma con la prima lettera minuscola:

String nomeFile;

int caratteriRimanenti;

boolean isEmpty()

{

...

Capitolo 2 - Differenze (e affinità) con il C

Tipi primitivi

I tipi primitivi in Java sono:

Tipo Spazio occupatoNumerici: byte 8 bit short 16 bit int 32 bit - sempre, a differenza del C long 64 bit float 32 bit double 64 bit Caratteri Unicode: char 16 bit Logici - true o false: boolean dipende dall'implementazione

I tipi numerici sono tutti con segno.

Tutti questi tipi primitivi possono essere "incapsulati" in degli oggetti (vedi il prossimo capitolo), ad esempio un int può essere incapsulato in un oggetto della classe Integer.

Esistono inoltre due classi, BigInteger e BigDecimal, che consentono di operare su numeri interi e decimali di precisione arbitraria - ovviamente con prestazioni di molto inferiori alle operazioni su tipi primitivi.

Dichiarazione di variabili

Una variabile in java può essere dichiarata in (quasi) qualsiasi punto, all'occorrenza:

for(int i = 0; i < 10; i++)

...

Inoltre java, essendo più "intelligente" di altri linguaggi, non permette di nascondere variabili:

{

int i;

...

{

int i; // ERRORE - variabile gia' definita

...

}

}

Nuovo ciclo for

Java 1.5 ha introdotto un nuovo ciclo for, simile al "foreach" di altri linguaggi. Ad esempio, se prima per stampare un array di 100 int si doveva scrivere:

for(int i = 0; i < 100; i++)

System.out.println(array[i]);

ora basta scrivere (":" si legge "in"):

for(int numero : array)

System.out.println(numero);

Nel capitolo sulle collezioni vedremo l'altro uso del nuovo for.

Stringhe

In java le stringhe sono oggetti di classe String, e sono trattate in modo speciale rispetto agli altri.

Ad esempio è possibile concatenare stringhe mediante gli operatori + e +=:

String s = "";

s += "ciao";

Altro esempio:

String a = "Tizio",

b = "Caio",

c = "Sempronio";

System.out.println( a + ", " + b + " e " + c );

Inoltre tutte le classi sono dotate del metodo toString(), derivato da Object (vedi capitolo sull'ereditarietà), che fornisce una rappresentazione in forma di stringa dell'oggetto in questione.

Tale metodo viene chiamato (quasi) ogni volta ci sia bisogno di una stringa; ad esempio il codice:

Integer n = 5;

System.out.println("n vale " + n);

stampa "n vale 5" (notare uso autoboxing).

Da ciò si può vedere che il metodo printf, presente anche in java nella classe java.io.PrintStream, può essere sostituito dalla semplice concatenazione di stringhe, a meno che non si desideri una formattazione particolare.

Array

Un'altro tipo di oggetto molto particolare è l'array; ecco un esempio, la creazione di un array di 5 int, inizializzati a 0:

int[] numeri = new int[5];

Si possono anche specificare gli elementi di un'array all'atto dell'allocazione; lo statement:

numeri = new int[] {1,55,36};

crea un'array di tre elementi, 1, 55 e 36. Anche in java l'indice del primo elemento è 0, quindi nell'ultimo esempio numeri[0] è 1.

Una differenza fondamentale con gli array del C è il controllo in esecuzione dell'indice, quindi - sempre in riferimento all'ultimo esempio - il codice che segue lancia una ArrayIndexOutOfBoundsException:

numeri[3] = 6;

Questo controllo può avere effetti negativi sulle prestazioni, ma il fatto di non rischiare più un buffer overflow - causa di molti problemi di sicurezza e stabilità - dovrebbe convincere tutti della bontà della sua adozione.

Allocazione della memoria

Quando in java si dichiara un oggetto, ad esempio con:

Long l;

non si fa altro che dichiarare un riferimento - in pratica un puntatore, da usare però direttamente, "al posto" dell'oggetto. La creazione (allocazione) attuale dell'oggetto avviene con l'operatore new:

l = new Long(7);

I tipi primitivi e le "reference" a oggetti vengono allocate sullo stack - come le variabili locali in C - mentre con new si creano oggetti e array sullo heap - come per le aree di memoria allocate con malloc in C.

Liberare la memoria

E quando gli oggetti non servono più? Vengono abbandonati, riassegnando la/le reference a qualche altro oggetto o a null:

l = new Long(7*7); // il Long di valore 7 creato prima viene "abbandonato"

Quando un oggetto non è più raggiungibile, ovvero quando nessuna reference "punta" ad esso, diventa inutile, spazzatura, e all'occorrenza può essere cancellato dalla garbage collection (gc), un meccanismo che si attiva ogni qualvolta la situazione della memoria lo richieda. La gc può anche essere avviata esplicitamente dal programma, con System.gc().

Commenti

I commenti possono essere scritti su una o più linee, come in C/C++:

i++; // commento inutile

/* la seguente

funzione

bla, bla... */

public void do()

{

...

La novità sta nei commenti javadoc:

/** Rappresenta una persona. */

class Persona

{

...

/** Variabile importante. */

public double bigVar;

...

/** Calcola il minimo tra due numeri.

* @param n1,n2 i numeri da confrontare.

* @return il minimo tra n1 e n2.

*/

public int min(int n1, int n2)

{

...

Questi commenti, che iniziano con /**, possono essere estratti dal sorgente mediante il tool javadoc, incluso nel JDK di Sun, per produrre una documentazione nello stesso formato di quella delle API ufficiali; è bene abituarsi a scriverli ogni volta che si ha a che fare con campi particolarmente importanti e metodi non banali, anche prima di scrivere il corpo della classe o del metodo.

All'interno di questi commenti è possibile usare tag HTML; ad esempio, per forzare un 'a capo' nel documento prodotto da javadoc si usa il tag <BR>.

Overloading

In java è possibile l'overloading dei metodi, ovvero la scrittura per una stessa classe di più metodi con lo stesso nome, ma con diversa lista di argomenti, come:

public int max(int n1, int n2) { ... }

public float max(float n1, float n2) { ... }

Ciò che distingue i due metodi è solo la lista di argomenti, non il tipo di ritorno.

Varargs

Da java 5 è possibile dichiarare come ultimo parametro di un metodo, liste di argomenti di lunghezza variabile; ad esempio un metodo che calcola la media di 0 (?) o più numeri di tipo float può essere dichiarato così:

// '...' indica lunghezza variabile

public static float media(float... dati)

{

float res = 0f;

for(float num : dati)

res += num;

return res / dati.length;

}

e chiamato con un numero variabile di argomenti:

media(); // ritorna NaN, Not a Number

media(23.2f, 45.3f, 6.9f);

...

Dal codice del metodo si vede che dati è a tutti gli effetti un array.

Capitolo 3 - La programmazione OO

I linguaggi di programmazione hanno come scopo principale quello di aiutare a "tradurre" le entità del mondo reale e i loro comportamenti in strutture informatiche quali variabili, liste, funzioni, puntatori, "struct", ecc. La programmazione a oggetti, OOP, rende molto più naturale creare questa "mappa", permettendo di manipolare oggetti che assomigliano molto alla loro controparte del dominio che si vuole rappresentare.

Classi

Una classe è praticamente un tipo di dato, paragonabile (alla lontana) alle struct del C. Una istanza (variabile) di una classe è un oggetto.

Una classe adatta a rappresentare una persona potrebbe essere definita così:

class Persona

{

String nome,

cognome,

5 indirizzo;

int nCivico;

Persona(String nome,

String cognome,

10 String indirizzo,

int nCivico)

{

this.nome = nome;

this.cognome = cognome;

15 this.indirizzo = indirizzo;

this.nCivico = nCivico;

}

}

Nota: i numeri ad inizio riga servono solo come riferimento, non sono codice java!

Dalla linea 3 si possono vedere dei campi di tipo String e uno di tipo intero.

In linea 8 si vede in azione il costruttore. Il costruttore ha lo stesso nome della classe e si occupa di inizializzare l'oggetto - viene chiamato quando si alloca memoria con new.

Se non si definisce il costruttore per una classe, Java ne fornisce uno predefinito, senza argomenti.

La parola chiave this è semplicemente un riferimento all'oggetto corrente; in questo caso viene usato per distinguere i campi dell'oggetto dai parametri passati al costruttore, per i quali ho voluto usare gli stessi nomi.

Operare sugli oggetti

Un programma java inizia la sua esecuzione dal metodo main di una classe. Tutte le classi, ad esclusione delle classi annidate, possono avere un main; ciò è utile per testare la classe durante lo sviluppo, anche se allo scopo esistono strumenti più sofisticati, come JUnit.

In questo caso possiamo inserirlo nella definizione di Persona o in un'altra classe a cui sia visibile:

public static void main(String[] args)

{

Persona p = new Persona("Mario",

"Rossi",

5 "via Di Qua",

5);

System.out.println("Il mio nome e' " + p.nome);

}

Nella prima linea si vede la segnatura del metodo main, che usando varargs può essere sostituita da public static void main(String... args).

In linea 3 viene allocata sullo stack la variabile p - un riferimento a Persona - alla quale viene subito assegnato un oggetto, creato sullo heap.

In linea 8 si accede direttamente al campo nome di p; in generale questa non è una buona cosa - ad esempio si può volere che una volta creato un oggetto Persona i suoi campi non siano modificabili, mentre con la classe definita sopra nulla vieta di scrivere qualcosa come:

p.nome = "Carlo";

esattamente come succede con le struct del C.

In questo caso particolare la cosa può risultare abbastanza innocua, ma quando si ha a che fare con un programma di una certa dimensione è meglio non andare a toccare direttamente la struttura interna di un oggetto. C'è quindi bisogno di un meccanismo per controllare l'accesso a campi e metodi.

Nascondere l'implementazione

Modifichiamo leggermente la classe precedente:

class Persona

{

private String nome,

cognome,

indirizzo;

private int nCivico;

public Persona(...

In questo modo i campi di una Persona possono essere letti e modificati solamente dai metodi della classe stessa, sono privati.

Il costruttore invece decidiamo di renderlo pubblico - non è obbligatorio - in modo da permettere a tutti di creare oggetti di tipo Persona.

Un costruttore privato ha senso per esempio se si vuole controllare il numero di oggetti creati; allo scopo si userà un metodo statico:

private static int numero = 0;

private Persona(...

...

/** Crea al massimo 10 persone */

public static Persona creaPersona(String nome, ...)

{

if(numero == 10)

throw new TroppePersoneException();

numero++;

return new Persona(nome, ...);

}

In questo modo il costruttore è accessibile solo all'interno della classe stessa; altre classi che vogliano creare oggetti di tipo Persona lo possono fare solo usando (fino a 10 volte) il metodo creaPersona:

Persona pino = Persona.creaPersona("Pino", ...);

Interfacce

Il senso del discorso precedente è che è opportuno esporre meno codice possibile.

Ciò che conta non è l'implementazione, ma l'interfaccia di una classe, e infatti in java esistono le interfacce.

Ad esempio si supponga di voler definire il comportamento di una pila di numeri senza specificarne l'implementazione.

Una pila deve permettere come minimo due operazioni, push e pop; questo fatto si può descrivere così:

interface PilaDiNumeri

{

public void push(int n);

public int pop();

}

Per implementare un'interfaccia si usa implements:

class P implements PilaDiNumeri

{

/* codice... */

public P()

{

/* codice... */

}

public void push(int n)

{

/* codice... */

}

public int pop()

{

/* codice... */

}

/** Ritorna l'elemento in cima

* alla pila, senza rimuoverlo.

*/

public int top()

{

/* codice... */

}

}

Notate l'aggiunta di un costruttore e del metodo top - una volta implementati tutti i metodi dell'interfaccia si può arricchire a piacimento la classe.

Inoltre una classe può implementare più interfacce:

class P

implements PilaDiNumeri, Iterable

Nel prossimo capitolo si comprenderà la vera utilità delle interfacce, mentre in quello sulle collezioni si vedrà un'implementazione di pila generica.

Campi e metodi statici

Quando un campo o un metodo è dichiarato static "fa parte" della classe, e non di un oggetto in particolare.

Come esempio si consideri il precedente metodo creaPersona; non è possibile chiamarlo riferito a un oggetto, così:

/* NO!

Persona alfio = gianni.creaPersona(... */

/* si' */

Persona alfio = Persona.creaPersona("Alfio", ...

Un altro esempio di metodo statico è main, il metodo che viene eseguito quando si passa il nome di una classe al comando java:

java Persona

Costanti ed enumerazioni

Quando una variabile è final non può essere modificata:

final float PI = 3.14f;

/* NO!

PI = 5; */

Nel caso si vogliano usare delle costanti per specificare delle modalità, come in C, dovrebbero essere incluse in una classe fatta più o meno così:

/** Specifica le possibili modalita'

* di scrittura di un file.

*/

class Mode

{

public final Mode

RO = new Mode(), // read only

RW = new Mode(), // read - write

WO = new Mode(); // write only

private Mode() {}

}

In questo modo è possibile passare a qualche metodo un valore di tipo Mode, che può assumere solo i valori RO, RW, WO, invece di qualsiasi valore intero, come si usa in C.

Questo era necessario fino a java 1.5, che ha introdotto le enum; la singola linea:

enum Mode { RO, RW, WO }

si comporta esattamente come la classe precedente.

Classi annidate

Una classe può essere definita all'interno di un'altra classe o di un metodo:

class FileDiTesto

{

...

class LineSeparator

{

...

Una classe di questo genere viene chiamata inner class o nested class.

Una inner class può essere anonima, nel caso ci sia bisogno di una classe "usa e getta"; come esempio il metodo seguente, definito dall'interfaccia Iterable, ritorna un Iterator, creato "al volo":

Iterator iterator()

{

return new Iterator()

{

boolean hasNext()

{ ... }

Object next()

{ ... }

void remove()

{ ... }

}

}

Iterator è un'interfaccia, che in questo caso viene implementata in una classe senza nome, di cui si ritorna un'istanza.

Il classpath

Quando si chiede all'interprete java di eseguire una classe, ad esempio con

java Persona

java esamina il classpath, una lista di directory o di file zip o jar in cui cercare il corrispondente file class; un esempio di classpath può essere

.:main.jar:/usr/local/javaclasses

Il classpath può essere definito tramite la variabile d'ambiente CLASSPATH o passato ai tool java mediante l'opzione -cp:

java -cp . Persona

in assenza di indicazioni la directory corrente, ".", viene sempre inclusa, quindi nel comando precedente l'opzione -cp è superflua.

I file jar

Vi sarete accorti che il compilatore java produce un file .class per ogni classe che trova nel sorgente, il che può essere scomodo, soprattutto nel caso in cui il vostro programma debba essere distribuito, o scaricato come applet.

Per ovviare al problema nel JDK è incluso il tool jar, che consente di archiviare in un singolo file tutti i .class - e tutti i file di supporto come immagini, suoni, ecc. - relativi a un programma.

Prima di tutto è necessario creare un file di testo, detto manifest, per indicare a jar, tra l'altro, quale è la classe principale che deve essere eseguita; nel caso di prima il file - supponiamo di chiamarlo "mf" - dovrà contenere la riga:

Main-Class: Persona

A questo punto si può impacchettare il tutto con:

jar cvf p.jar -m mf Persona.class

ed eseguirlo con:

java -jar p.jar

I package

Le classi java possono essere organizzate gerarchicamente in package. Ad esempio supponiamo che in una directory inclusa nel classpath - e che quindi sia visibile a java - ci sia un'altra directory lib, in cui è contenuto il seguente file Ciao.java:

package lib;

public class Ciao

{

public static void ciao()

{

System.out.println("ciao ciao");

}

}

La prima linea dichiara che la classe Ciao fa parte del package lib.

Dopo aver compilato il file possiamo usare la classe Ciao da qualsiasi programma, direttamente con:

lib.Ciao.ciao();

oppure importando la classe, in testa al file sorgente, un po' come si fa in C con la direttiva #include:

import lib.Ciao;

...

Ciao.Ciao();

...

La libreria standard di java si usa proprio così, importando le classi desiderate, ad esempio:

import java.util.*; // importa tutte le classi del package java.util

Il package java.lang viene importato automaticamente, quindi si può usare la classe String, per esempio, senza importare alcunché.

Prima abbiamo visto come controllare l'accesso ai membri di una classe tramite public e private. Se non si usano queste keyword, ad esempio:

int max(...) { ... }

il metodo o il campo è accessibile solo all'interno dello stesso package della classe in cui è dichiarato.

Se in un file sorgente non viene dichiarato il package le classi contenute in quel file apparterranno al package di default, ossia al package di cui fanno parte le classi presenti nella stessa directory - a meno che queste non abbiano dichiarato di appartenere ad un altro package.

Capitolo 4 - Estendere classi

Una delle caratteristiche della programmazione OO è l'ereditarietà. Personalmente trovo che questo nome sia un po' fuorviante, un termine più preciso sarebbe estensione - non a caso java usa la parola extends.

Estendere una classe significa semplicemente aggiungere dei membri alla stessa per creare una nuova classe, più "ricca", oppure ridefinirne alcuni metodi:

class Studente

extends Persona

{

private int matricola;

5

public Studente(String nome,

String cognome,

String indirizzo,

int nCivico,

10 int matricola)

{

super(nome,

cognome,

indirizzo,

15 nCivico);

this.matricola = matricola;

}

}

La linea 2 indica che uno Studente estende (è) una Persona. In linea 4 è stato aggiunto il numero di matricola, inizializzato in linea 17. In linea 12 viene chiamato il costruttore della superclasse - super è un riferimento alla superclasse, analogo a this.

In java non è possibile estendere più di una classe per volta, ma è permessa l'implementazione di più interfacce:

class Studente

extends Persona

implements Consumatore, Giocatore

{

...

Overriding

Cosa succede se nella classe derivata si definisce un metodo con lo stesso nome (e stessi parametri) di un metodo già presente nella superclasse? Semplicemente il metodo nuovo nasconde il vecchio:

class Persona

{

...

void stampa()

{

System.out.println("Persona " + nome );

}

}

class Studente

extends Persona

{

...

void stampa()

{

System.out.println("Studente " + nome );

}

}

/** classe principale */

class Main

{

public static void main(String[] args)

{

Persona sandro =

new Studente("Sandro", ...);

sandro.stampa();

}

}

Questo codice stampa: "Studente Sandro"; se stampa() non fosse definito in Studente, verrebbe chiamato il metodo definito in Persona, che stamperebbe "Persona Sandro".

Polimorfismo

Come detto in precedenza, uno Studente è una Persona, e può essere trattato come tale:

Persona alfio = new Studente(...);

ovviamente in questo caso lo Studente alfio perde le sue peculiarità, e sarà accessibile solo tramite l'interfaccia della classe persona, a meno di non usare il casting:

Studente alfio2laVendetta = (Studente) alfio;

Questo uso della superclasse o dell'interfaccia in luogo della classe viene chiamato polimorfismo (di sottotipo).

Object

La classe Object definisce alcuni metodi che vengono ereditati e/o ridefiniti dalle sue sottoclassi, ovvero tutte le classi.

Senza dubbio il metodo più usato è toString(), che fornisce una rappresentazione dell'oggetto in forma di stringa. Come esempio possiamo (ri)definire il suddetto metodo per la classe Persona:

public String toString()

{

return nome + " " + cognome +

", residente in " + indirizzo + ", " + nCivico + ".";

}

Classi e metodi final

Una classe final non può essere estesa. Ad esempio un tentativo di estendere la classe finale java.util.Scanner di java 1.5 produce un errore in fase di compilazione:

class S extends Scanner // NO!

{

...

Anche un metodo può essere final, intendendo con questo che non può essere ridefinito.

protected

Oltre ad avere "visibilità" public, private o di default (package access), un membro di una classe può avere accesso protected, ovvero può essere usato solo nelle sottoclassi oppure nello stesso package della classe.

Classi e metodi astratti

Come si è visto, le interfacce permettono di specificare il comportamento di una classe senza implementarlo. Una classe astratta è una via di mezzo tra le interfacce e le classi concrete, poiché implementa solo una parte dei metodi, lasciando gli altri abstract:

abstract class S

{

void metodo1()

{

System.out.println("Sono metodo1.");

}

abstract void metodo2();

}

Prima di poter usare dei metodi abstract vanno implementati da una sottoclasse, non necessariamente concreta:

abstract class T extends S

{

void metodo2()

{

System.out.println("Sono metodo2.");

}

abstract void metodo3();

}

class U extends T // finalmente una classe "usabile"!

{

void metodo3()

{

System.out.println("Sono metodo3.");

}

}

Non si possono creare oggetti di una classe astratta, ma si può fare così:

/* oggetto di classe U usato tramite

l'interfaccia di S (metodo3 non accessibile) */

S s = new U();

Capitolo 5 - Creare e gestire le eccezioni

Nei linguaggi "tipo C" tipicamente le funzioni segnalano eventuali errori tramite il valore di ritorno.

In java (e non solo) si preferisce usare un meccanismo diverso, le eccezioni. Quando un metodo deve segnalare una situazione anomala, o che comunque richiede una certa attenzione, lancia un'eccezione:

// qualcosa e' andato storto!

throw new MiaEccezione();

Come si può vedere un'eccezione è una classe come le altre, derivata da java.lang.Exception:

public class MiaEccezione

extends Exception

{}

La cosa più importante per un'eccezione è il nome, che dovrebbe essere significativo.

Gestire le eccezioni

Un metodo che lancia un'eccezione, che non sia una RuntimeException, deve dichiararlo:

public int read()

throws IOException // notare la 's'

{ ... }

Un metodo che chiama read() può gestire l'eccezione o lanciarla a sua volta. Se la lancia deve dichiararlo:

public int metodo1()

throws IOException

{

...

n = read(); // puo' generare IOException

...

}

In alternativa, se è opportuno, può "catturarla":

public int metodo2()

{

...

try {

5 n = read();

} catch(IOException e) {

System.err.println("Generata una eccezione di I/O!");

e.printStackTrace();

} catch(Exception e) {

10 System.err.println("Generata una eccezione!");

e.printStackTrace();

} finally {

System.err.println("Blocco try terminato!");

}

15 }

La parola try, in linea 4, indica l'inizio del blocco di codice da controllare; se il codice genera un'eccezione il blocco catch corrispondente al suo tipo viene eseguito. Nel blocco catch si deve gestire al meglio l'eccezione, ad esempio liberando risorse, stampando messaggi di errore, uscendo con System.exit(1), eccetera - al limite si può anche rilanciare l'eccezione con throw.

Da notare che con la clausola catch(Exception e) vengono catturate tutte le eccezioni. Il blocco opzionale finally viene eseguito per ultimo.

Ignorare le eccezioni

Le eccezioni possono venire ignorate usando una clausola tipo:

catch(Exception e) {}

o lanciandole "fuori" dal main, alla console:

public static void main(String[] args)

throws Exception

{

...

n = read();

...

}

Naturalmente questa pratica va usata solo per brevi programmi di test, non per codice "di produzione".

Capitolo 6 - Collezioni di oggetti

Molto spesso si ha necessità di conservare degli oggetti in una struttura dati. La scelta migliore per semplicità e velocità è l'array, ma non sempre è applicabile.

La libreria java.util fornisce delle collezioni pronte all'uso, strutturate in una gerarchia che prevede tre interfacce principali - List, Set e Map - e varie implementazioni.

Un Set (insieme) è un contenitore che mantiene una sola copia dell'oggetto, mentre Map è un'associazione (funzione) chiave - valore.

Ecco un esempio di lista:

List capitali = new ArrayList();

capitali.add("Roma");

capitali.add("Parigi");

5 capitali.add("Londra");

System.out.println(capitali);

È opportuno usare le interfacce invece delle implementazioni (linea 1).

In linea 7 viene chiamato il metodo toString(), che fornisce una rappresentazione del contenuto della lista. Se si prova a fare lo stesso con un array si vede che toString() non fornisce la stessa informazione; in questo caso si possono usare i metodi statici toString(...) o deepToString(...) della classe Arrays.

Generics e autoboxing

Una collezione contiene Object, quindi quando si recuperano gli oggetti è necessario il casting - continuando l'esempio precedente:

String it = (String) capitali.get(0);

Java 1.5 ha introdotto generics e autoboxing, che permettono un uso più diretto e sicuro delle collezioni:

List<String> capitali = new ArrayList<String>();

In questo modo capitali non è più una lista di oggetti, ma di stringhe, quindi il casting non serve.

L'autoboxing (e specularmente unboxing) invece consente di creare liste di tipi primitivi:

List<Integer> primi = new ArrayList<Integer>();

primi.add(2); // autoboxing

e più in generale di usare un tipo primitivo quando viene richiesto un tipo wrapper (Integer, Float, ecc.) o Object o Number (superclassi).

Iteratori e nuovo for

Il modo standard di accedere agli elementi di una collezione è tramite un Iterator:

Iterator i = capitali.iterator();

while(i.hasNext())

System.out.println(i.next());

In java 5 una collezione si può visitare molto più agevolmente con l'enhanced for, già visto in azione per gli array:

for(String c : capitali)

System.out.println(c);

Costruiamo una pila

In conclusione di questo capitolo vediamo un semplice esempio di implementazione di pila generica - nel main() un esempio d'uso:

import java.util.*;

/** Pila generica.

* Usa la nuova (1.5) interfaccia Queue, implementata da LinkedList.

*/

public class Pila<T> // notare il "segnaposto" per il tipo

{

private LinkedList<T> pila;

/** Crea una pila vuota

*/

public Pila()

{

pila = new LinkedList<T>();

}

/** Mette un elemento in testa alla pila.

* @param el l'elemento da inserire.

*/

public void push(T el)

{

pila.addFirst(el);

}

/** Estrae un elemento dalla pila.

* @return L'elemento estratto.

* @throws NoSuchElementException se la pila e' vuota.

*/

public T pop()

{

return pila.remove(); // Queue, Java 1.5+

}

public String toString()

{

return pila.toString();

}

/** MAIN - per test */

public static void main(String... args)

{

// crea una pila vuota di Integer

Pila<Integer> p = new Pila<Integer>();

/* mette sulla pila 23, 0, -22

usa autoboxing, java 1.5 o superiore */

p.push(23);

p.push(0);

p.push(-22);

System.out.println(p);

System.out.println(p.pop());

System.out.println(p);

p.pop();

p.pop(); // ora la pila e' vuota!

System.out.println(p);

p.pop(); // lancia NoSuchElementException

}

}

Capitolo 7 - Leggere e scrivere file

In java.io ci sono due gruppi principali di classi, uno che lavora con i byte, e uno che gestisce i caratteri unicode.

Queste classi sono abbastanza generiche da permettere di lavorare con molte entità diverse - file, connessioni di rete, buffer in memoria, pipe, ecc. - anche se in questo capitolo ci si concentrerà sui file.

Scanner

Il modo più semplice per leggere un file di testo è quello di usare la nuova (da java 1.5) classe java.util.Scanner.

Come esempio il seguente programma stampa a video il file di testo passato come primo argomento:

import java.io.*;

import java.util.*;

class JCat

{

public static void main(String[] args)

throws Exception // ATTENZIONE!

{

Scanner sc = new Scanner(new File(args[0]));

sc.useDelimiter("\n");

while(sc.hasNext())

System.out.println(sc.next());

}

}

Scanner è un Iterator, come si può vedere dalle ultime due linee.

RandomAccessFile

Un'altra classe un po' "alternativa" è java.io.RandomAccessFile, molto più familiare delle classi "canoniche" per chi proviene dal C, e adatta ad aprire file in lettura e scrittura ("rw") o in sola lettura ("r"):

RandomAccessFile f =

new RandomAccessFile("file.txt", "rw");

f.writeUTF("File ad accesso casuale.\n");

f.seek(0); // va all'inizio

f.writeUTF("abcd"); // sostituisce 'File' con 'abcd'

f.close();

Reader e Writer

Reader e Writer sono al vertice di due gerarchie di classi che si occupano rispettivamente della lettura e della (sovra)scrittura di char.Partiamo direttamente con un esempio, la lettura e stampa del file "nomefile.txt":

String s;

BufferedReader reader =

new BufferedReader(

new FileReader("nomefile.txt") );

5

while( (s = reader.readLine()) != null )

System.out.println(s);

reader.close();

Con le linee 2-4 è stato creato un oggetto FileReader a partire dal nome del file da leggere, che a sua volta viene usato per creare un BufferedReader. Questo "wrap" serve ad attivare una cache, che permette un notevole incremento di prestazioni.

Vediamo ora l'output:

PrintWriter writer =

new PrintWriter(

new BufferedWriter(

new FileWriter("nomefile.txt",

true))); // non sovrascrivere, accoda

writer.print("Questo ");

writer.println("e' un file di testo.");

In questo caso si è aperto il file in "append" (java 1.4+). È stato usato PrintWriter per facilitare la scrittura del testo.

InputStream e OutputStream

Quando è necessario lavorare con i byte le classi derivate da Reader e Writer non sono più adatte: servono quelle delle famiglie InputStream e OutputStream (oppure, solo per i file, RandomAccessFile).

I nomi delle classi e il loro funzionamento sono tuttavia molto simili - ad esempio la classe corrispondente a FileReader è FileInputStream. Esistono anche delle classi per la conversione, in una direzione:

reader = new InputStreamReader(is);

writer = new OutputStreamWriter(os);

Due classi non hanno corrispondenti nella famiglia "char oriented", DataInputStream e DataOutputStream:

DataOutputStream do =

new DataOutputStream(

new BufferedOutputStream(

new FileOutputStream("nomefile.dat",

5 true))); // append

do.writeInt(43210);

do.close();

In questo pezzo di codice viene aperto un file su cui viene scritto il valore dell'intero 43210 (4 byte).

Similmente si lavora con DataInputStream.

Standard input

System.in è un InputStream collegato allo standard input. Vediamo allora come ottenere dall'utente un numero intero immesso da tastiera:

Scanner stdin =

new Scanner(System.in);

int i;

System.out.print("Inserisci un intero: ");

while(true)

try {

i = stdin.nextInt();

break; // niente eccezione, abbiamo il numero

} catch(InputMismatchException e) {

System.out.println("Devi inserire un int!");

stdin.next(); // "scarta" l'elemento

}

System.out.println("Hai immesso l'intero: " + i);

Attenzione, in Italia il punto è un LocalGroupSeparator - vedi java.text.DecimalFormatSymbols.getGroupingSeparator() - che separa le migliaia: 123.456 diventa così un int, non un float!

L'immissione di stringhe è ancora più semplice:

Scanner stdin = new Scanner(System.in);

stdin.useDelimiter("\n"); // prendiamo tutta la riga

System.out.print("Inserisci il tuo nome: ");

String s = stdin.next();

System.out.println("Il tuo nome e': " + s);

Comprimere gli stream

Un OutputStream si può comprimere in modo trasparente, passandolo al costruttore di java.util.zip.GzipOutputStream:

BufferedOutputStream out=

new BufferedOutputStream(

new GzipOutputStream(

// attenzione, sovrascrive!

new FileOutputStream("out.dat")));

Per leggere da uno stream compresso con GZIP si può usare GzipInputStream.

Esistono anche classi analoghe per la gestione di file in formato ZIP.

La classe File

La classe java.io.File ha un nome forse fuorviante, dato che non rappresenta un file, ma un percorso - chiamarla "Path" sarebbe stato forse più appropriato. Una volta creato un oggetto File, ad esempio con:

File f = new File(args[0]);

posso usarlo per sapere se tale percorso esiste:

System.out.println("Il file " + args[0] +

( f.exists() ? "" : " non") +

" esiste");

oppure creare la gerarchia di directory nel percorso:

f.mkdirs();

eccetera.

Salvare oggetti

Supponiamo di dover scrivere nel file "out.dat" un'istanza della seguente classe - notate che per usare questo meccanismo è necessario implementare Serializable:

class OggettoStrano

implements Serializable

{

String nome;

transient String pass;

...

}

Nota: se si desidera che alcuni campi dell'oggetto non vengano salvati, magari perché contenenti dati sensibili o che non ha senso salvare, bisogna marcarli come transient.

Potremmo procedere così:

OggettoStrano o = new OggettoStrano();

ObjectOutputStream oos =

new ObjectOutputStream(

new BufferedOutputStream(

new FileOutputStream("out.dat")));

// String e' Serializable

oos.writeObject("Questo file contiene un oggetto strano.");

oos.writeObject(o); // scrivo l'oggetto

oos.close(); // chiudo lo stream

Come vedete nello stesso stream si possono salvare più oggetti in sequenza - in questo caso una stringa e l'oggetto in questione. Per recuperare questi oggetti al solito si usa la classe simmetrica, ObjectInputStream, e il metodo readObject(), il quale restituisce un Object, su cui si dovrà eseguire il cast appropriato.

Il meccanismo di serializzazione è usato, oltre che per ottenere la persistenza di oggetti, anche per passarli tra diverse JVM, magari dislocate su macchine diverse su una rete, ad esempio usando Java RMI.

Capitolo 8 - Programmazione concorrente

L'esecuzione di un programma java si divide in threads, o "processi leggeri". Questi frammenti di programma possono essere eseguiti indipendentemente (o quasi) dagli altri, ovvero contemporaneamente.

Il comportamento di un thread è implementato nella classe Thread, e l'esecuzione effettiva avviene nel suo metodo run().

Come esempio la seguente classe estende Thread e ridefinisce il metodo run(), in modo che calcoli se un numero è primo o no - un numero è primo se è maggiore di 1 e se è divisibile solo per 1 e per sé stesso:

public class Primi

extends Thread

{

private int n;

public Primi(int n)

{

super("Computing " + n); // nome del thread

this.n = n;

start(); // fa partire questo thread - chiama run()

}

public void run() { System.out.println(n + ( primo() ? "" : " non" ) + " e' primo."); } /** * Calcola se n e' primo. */ public boolean primo() { if( n < 2 ) return false; // 1 non e' primo for(int i = 2; i < n; i++) if( n % i == 0 ) return false; return true; } public static void main(String... args) { // crea alcuni thread new Primi(100000007); // questo ci mette un po' new Primi(100000081); // anche questo new Primi(1234567891); // questo di piu' new Primi(123456); new Primi(4999); new Primi(123); // qui tutti i thread hanno cominciato l'esecuzione System.out.println("Partiti!"); } }

Nel costruttore si chiama super(...) per assegnare un nome al thread, quindi lo si fa partire con start().

Il main stampa - non necessariamente nello stesso ordine:

123456 non e' primo.

4999 e' primo.

Partiti!

123 non e' primo.

100000081 e' primo.

100000007 e' primo.

1234567891 e' primo.

Il programma precedente non termina finché tutti i thread non sono terminati. Volendo un comportamento differente - il programma termina quando termina il main - basterà marcare i thread come demoni, chiamando setDaemon(true) prima di start().

Il metodo yield() permette di "fare una pausa" e informare così lo scheduler che può passare l'esecuzione a un altro thread, ma va usato con cautela; ad esempio, se nella classe precedente si modificasse il ciclo for così:

for(int i = 2; i < n; i++, yield())

if( n % i == 0 )

return false;

si avrebbe un notevole overhead, visto che yield() verrebbe chiamato a ogni ciclo.

È anche possibile impostare una priorità per un thread tramite il metodo setPriority(...); il suo valore, nel caso della JVM della Sun, è compreso tra 1 e 10.

Runnable

Nell'impossibilità di estendere Thread, magari perché si deve estendere un'altra classe, si può implementare l'interfaccia Runnable:

public class Primi

implements Runnable

{

...

public void run()

{

...

e creare un thread a partire dall'oggetto Runnable:

...

public Primi(int n)

{

Thread t = new Thread(this, "Computing " + n);

this.n = n;

t.start();

}

...

In realtà questo è il modo più pulito e quindi preferibile di creare un thread, se non si devono ridefinire altri metodi della classe Thread oltre a run().

Timer

La classe java.util.Timer consente di temporizzare un evento, rappresentato come TimerTask, una classe astratta che implementa Runnable.

Dal punto di vista del programmatore cambia poco, si implementa il metodo run() e si schedula l'evento con varie modalità; ecco un esempio:

import java.util.*;

public class Scrittore

extends TimerTask

{

private Timer tim;

private static int contatore = 0;

private int volte;

private String msg;

public Scrittore(String msg, int sec, int volte)

{

this.msg = msg;

this.volte = volte;

tim = new Timer();

tim.schedule(this, // esegui questo TimerTask...

0, // ... da adesso...

sec * 1000); // ... ogni 'sec' secondi

}

public void run()

{

contatore++;

if(contatore >= volte)

tim.cancel(); // questa esecuzione di run() e' l'ultima

System.out.println(msg + " numero: " + contatore);

}

public static void main(String[] args)

{

// scrivi "Ciao" ogni secondo per 5 volte

new Scrittore("Ciao", 1, 5);

}

}

I metodi schedule(...) di Timer permettono di posticipare un evento, oppure di eseguirlo ripetutamente, a distanza di almeno 'period' millisecondi tra un'esecuzione e l'altra; i metodi scheduleAtFixedRate(...), invece, eseguono il task esattamente ogni 'period' millisecondi, risultando più precisi sul lungo periodo.

Sincronizzare i thread

Molto spesso vari thread devono operare sugli stessi dati, e questo può portare a problemi di inconsistenza. Supponiamo di avere due thread, t1 e t2, che agiscono su un oggetto condiviso; può succedere che lo scheduler trasferisca l'esecuzione da t1 a t2 mentre t1 deve ancora terminare di aggiornare l'oggetto.

Ci sono molti modi per superare questo problema. Se l'oggetto in questione è di un tipo primitivo diverso da long e double l'assegnamento e la lettura sono atomici, nel senso che non possono essere interrotti. Per long e double, essendo "lunghi" 64 bit, l'atomicità non è garantita, ma questo si può risolvere con il modificatore volatile. Bisogna comunque fare attenzione, operazioni semplici come l'incremento, i++, non sono atomiche - in questo caso sono necessari una lettura, un incremento e un assegnamento.

Un'altra soluzione è quella di permettere a un solo thread alla volta di accedere all'oggetto condiviso, isolando il codice corrispondente dentro una regione critica.

Ogni oggetto java ha un monitor, una variabile che permette di realizzare la mutua esclusione (mutex). Tramite il costrutto seguente:

synchronized(this) {

// opera sull'oggetto condiviso

}

si dichiara che il codice compreso nel blocco può essere eseguito da un solo thread alla volta.

Si può sincronizzare anche l'intero corpo di un metodo:

private synchronized void do()

{

...

Quando un thread arriva all'inizio della regione critica si possono verificare due condizioni:

- se il monitor dell'oggetto (in questo caso this) è disponibile significa che nessun altro thread sta eseguendo la regione, quindi il suddetto thread può acquisire il monitor ed entrare nel blocco synchronized;

- se il monitor non è disponibile significa che un altro thread sta eseguendo la regione, quindi il thread corrente deve aspettare finché il monitor non viene rilasciato.

Nel caso l'oggetto in questione sia una collezione è possibile produrne una versione sincronizzata tramite alcuni metodi della classe Collections; ad esempio per avere una lista sincronizzata il codice dovrebbe apparire più o meno così:

List l = Collections.synchronizedList(new ArrayList());

...

synchronized(l) {

Iterator it = l.iterator();

while(it.hasNext()) {

...

}

}

da notare che l'iterazione, tramite Iterator o for-each, deve essere comunque sincronizzata sul monitor della collezione stessa - vedi anche le nuove collezioni concorrenti.

E java 1.5?

Come in altre aree la release 5 di Java ha introdotto sostanziali novità anche per quanto riguarda la programmazione concorrente, tramite tre nuovi package, java.util.concurrent e i suoi "amici" atomic e locks. In essi sono state inserite molte nuove classi e interfacce. Nello spirito di questa guida ne presenterò solo alcune, sufficienti però a dare un'idea della loro utilità.

Esecutori

Prima di tutto è presente la nuova interfaccia Callable<V>, con il solo metodo call(), che a differenza di Runnable.run() ritorna un valore di tipo V (usa i generics) e può lanciare eccezioni. Una volta implementata per un tipo specifico (o generico, lasciando indicato V):

class Calcolatore

implements Callable<Integer>

{

...

Integer call()

{

...

return risultato;

}

...

l'oggetto Callable può essere eseguito da un Executor:

ExecutorService esecutore = // sottoclasse di Executor

Executors.newCachedThreadPool();

Future<Integer> f = // vedi anche FutureTask

esecutore.submit(new Calcolatore());

... // posso fare altre cose mentre l'oggetto Callable viene eseguito

System.out.println("Risultato: " +

f.get()); // attendo il risultato - puo' lanciare eccezioni

esecutore.shutdown(); // finisci i task e non accettarne altri

Possono risultare utili in questo contesto anche i metodi invokeAll(...) e invokeAny(...) di ExecutorService.

Collezioni concorrenti

L'uso di synchronized è sicuro, ma può portare a un notevole degrado di prestazioni nel caso in cui molti thread si contendano un oggetto condiviso.

Se si intende condividere una collezione tra più thread, nella quale le operazioni in lettura siano molto più numerose delle modifiche, risulta conveniente usare le collezioni concorrenti, che sono thread-safe, ma non sincronizzate. Le classi (generiche) che implementano code, mappe, liste e insiemi concorrenti sono ConcurrentLinkedQueue<E>, ConcurrentHashMap<K,V>, CopyOnWriteArrayList<E> e CopyOnWriteArraySet<E>.

Ci sono poi le code bloccanti - interfaccia BlockingQueue<E> e cinque implementazioni - che consentono di risolvere senza fatica problemi di tipo produttore/consumatore, tramite i metodi put() e take().

Atomic

Nel package java.util.concurrent.atomic sono presenti classi come AtomicInteger, che fornisce metodi per varie operazioni atomiche, come getAndIncrement(), compareAndSet(...), set(...), eccetera.

Altre classi, come AtomicLongArray, rappresentano array sui quali sono possibili analoghe operazioni atomiche.

AtomicMarkableReference<V> e AtomicStampedReference<V> consentono di associare a un oggetto un boolean o un int, rispettivamente.

E gli altri

Infine java.util.concurrent.locks e i due package precedenti comprendono altre classi e interfacce che permettono una maggiore flessibilità nel controllo della concorrenza.

Capitolo 9 - Test delle unità

Un modo ovvio per testare i metodi di una classe è quello di inserire nel codice dei comandi che stampano il valore di determinate variabili, e di controllare se questi valori sono consistenti.

Esiste un metodo automatico per verificare che certe condizioni siano soddisfatte senza "inquinare" il codice con comandi di stampa: usare JUnit.

Una classe da testare

Supponiamo di dover testare una classe con dei metodi che calcolano il quoziente e il resto della divisione intera - naturalmente una classe del genere ha solo un interesse "teorico", allo scopo in java esistono già gli operatori / e %:

package algebra;

public class Div

{

/** divisione intera per sottrazioni successive */

public static int div(int a, int b)

{

if( a == 0 || a < b )

return 0;

return div( a-b, b ) + 1;

}

/** resto della divisione intera */

public static int mod(int a, int b)

{

if( a == 0 )

return 0;

if( a < b )

return a;

return mod( a-b, b );

}

}

La classe di test

JUnit 4.0 fa uso delle annotazioni, un meccanismo introdotto in Java 5 che permette appunto di annotare il codice per fornire indicazioni ad altri programmi - ad esempio JUnit.

Una classe di test per la classe Div si potrebbe scrivere così:

import algebra.*; // importo la classe Div

import org.junit.*;

import static org.junit.Assert.*; // static import

5

public class DivTest

{

@Test public void divisioni()

{

10 assertTrue( Div.div(654, 321) == 2 );

assertTrue( Div.div(3, 2) == 1 );

assertTrue( Div.div(123456, 15) == 8230 );

assertTrue( Div.div(0, 741) == 0 );

}

15

...

}

L'importazione della classe algebra.Div potrebbe non essere necessaria.

In linea 4 si trova un'altra novità di java 5, la possibilità di importare i membri statici di una classe; in questo caso si importa il metodo assertTrue della classe Assert, che fa fallire il test nel caso la condizione non sia vera. Gli static import dovrebbero comunque essere usati solo sporadicamente, se effettivamente rendono il codice più leggibile.

In linea 8 è stato annotato con @Test il metodo divisioni, che controlla i risultati di alcune invocazioni del metodo statico div.

Eseguire i test

A questo punto basta eseguire org.junit.runner.JUnitCore, con parametri i nomi delle classi contenenti i metodi annotati come @Test:

java org.junit.runner.JUnitCore DivTest

che in questo caso scriverà:

JUnit version 4.0

.

Time: 0

OK (1 test)

Nel classpath deve essere presente il file junit-4.0.jar (in questo caso anche la directory che contiene algebra, per trovare la classe Div).

Per poter eseguire i test scritti con la versione 4 di JUnit con un runner di versioni precedenti, come quelle incluse in Ant, Netbeans, eccetera, è necessario includere in ogni classe di test il seguente metodo "di adattamento":

public static junit.framework.Test suite()

{

return new junit.framework.JUnit4TestAdapter(DivTest.class);

}

avendo cura di sostituire DivTest.class con il nome della classe di test.

Eccezioni!

Proviamo ad aggiungere un metodo alla classe DivTest:

...

@Test public void divisioniPer0()

{

Div.div(0, 0);

}

...

questo non dovrebbe essere permesso, si tratta di una divisione per 0, eppure il test passa.

Il test che segue invece non passa:

...

@Test public void divisioniPer0()

{

Div.div(5, 0);

}

...

ma solo perché la ricorsione non arriva mai al caso base e quindi lo spazio sullo stack finisce.

In entrambi i casi l'invocazione del metodo dovrebbe lanciare un'eccezione di "divisione per zero". Il meccanismo delle annotazioni permette di specificare dei parametri; in questo caso è possibile indicare a JUnit quale eccezione deve essere lanciata nel corpo del metodo di test:

...

@Test (expected = ArithmeticException.class)

public void divisioniPer0()

{

Div.div(0, 0);

}

...

Adesso il test divisioniPer0 non passa, visto che non viene lanciata nessuna eccezione. Se nel corpo del metodo si invocasse div(8, 0) il test fallirebbe comunque, visto che si verifica uno StackOverflowError, in luogo di una ArithmeticException.

Per fare in modo che il test passi bisogna modificare il codice di div(...), in modo che lanci l'eccezione se il divisore è zero:

...

/** divisione intera per sottrazioni successive */

public static int div(int a, int b)

{

if( b == 0 )

throw new ArithmeticException("/ by zero"); if( a == 0 || a < b ) return 0; ... } ...

Le stesse considerazioni possono essere fatte per il metodo mod(...).

Timeout

È possibile indicare a JUnit che un test deve considerarsi fallito se trascorre un certo numero di millisecondi - ad esempio per un timeout di 30 secondi si può scrivere:

@Test (timeout = 30000) ...

Fixtures

JUnit permette di isolare il codice di setup, che deve essere eseguito prima di ogni test della classe - ad esempio per istanziare un oggetto - e il codice di teardown, eseguito dopo ogni test - ad esempio per liberare risorse, chiudere file, eccetera. Questo codice va messo in metodi annotati con @Before e @After, con nomi a piacere:

...

@Before public void setUp() { ... }

@After public void tearDown() { ... }

...

Quindi, in una classe con due metodi di test, test1 e test2, la sequenza di esecuzione sarà:

setUp

test1

tearDown

setUp

test2

tearDown

oppure con test1 e test2 scambiati.

Dove mettere i test?

Le classi di test possono essere messe nella stessa directory delle classi da testare, in modo che abbiano accesso ai membri protected. Una variazione può essere quella di mettere la classe di test nello stesso package della classe da testare, ma in una diversa directory.

Riprendendo l'esempio della divisione, dichiarando DivTest come facente parte del package algebra, si possono strutturare le directory così:

src/algebra/Div.java

test/algebra/DivTest.java

In questo modo i test sono ben separati dai sorgenti, pur appartenendo allo stesso package! Naturalmente quando si eseguono i test le directory src/ e test/ - oltre a junit-4.0.jar - devono essere incluse nel classpath:

java -cp <path programma>/src:<path programma>/test:<path junit>/junit-4.0.jar

org.junit.runner.JUnitCore algebra.DivTest

Quando scrivere i test?

Molti ritengono che sia una buona pratica scrivere i test prima del codice, in modo che la scrittura del codice sia diretta verso la riuscita dei test. Questo implica che i test siano buoni, ovvero che verifichino anche situazioni particolari, come la divisione per 0 nel nostro esempio. Per questo l'impegno nel preparare i test dovrebbe essere pari a quello speso nello scrivere il codice da testare.

Capitolo 10 - Organizzare lo sviluppo con Ant

In conclusione di questa guida presenterò Ant, uno strumento che permette di automatizzare vari task, come la compilazione di sorgenti java, l'esecuzione di programmi, la generazione della documentazione javadoc, varie operazioni su file e directory e molto altro. Ant è di fatto lo standard per gestire progetti software in java - viene adottato anche da applicazioni quali Eclipse e Netbeans - ed è praticamente indispensabile per progetti medio-grandi.

Un semplice esempio

Ant esegue i task indicati in un buildfile, un file analogo ai makefile di make scritto in xml.

Segue un esempio di buildfile, di default chiamato build.xml:

<project name="algebra" default="compila" basedir=".">

<!-- compilazione classi in algebra/... -->

<target name="compila">

5 <echo message="Sto compilando le classi in algebra/..."/>

<javac srcdir="algebra"/>

</target>

<!-- cancellazione file .class in algebra/... -->

10 <target name="clean">

<echo message="Sto cancellando i file .class in algebra/..."/>

<delete>

<fileset dir="algebra" includes="**/*.class"/>

</delete>

15 </target>

</project>

Deve essere presente un elemento project, che racchiude uno o più target - in questo caso due, uno per compilare le classi presenti nella directory algebra/, linee 4-7, e uno per "fare pulizia", che cancella i file compilati dalla stessa directory, linee 10-15. Le linee 3 e 9 sono commenti.

Eseguire Ant

I target vengono eseguiti passando il loro nome ad ant - ad esempio, per compilare si digita:

ant compila

che stampa più o meno:

compila:

[echo] Sto compilando le classi in algebra/...

[javac] Compiling 5 source files

Ma "compila" è indicato come default target in linea 1, quindi viene eseguito anche in mancanza di argomenti, digitando semplicemente ant.

I task di Ant

I target agiscono usando i task messi a disposizione da Ant.

Nel file di esempio ho usato i task echo, javac e delete, con significati ovvi.

Ci sono molti altri task - è anche possibile crearne di nuovi - tutti documentati nella sezione "Ant Tasks" del manuale di Ant, reperibile nel sito o nella distribuzione.

Il task junit

Un task molto utile è junit, che esegue delle classi di test. Aggiungiamo al file build.xml un altro target denominato "test", dipendente dal target "compila":

<target name="test" depends="compila">

<junit printsummary="on" haltonfailure="yes">

<classpath refid="test.class.path"/>

5 <batchtest fork="yes">

<fileset dir="test" includes="**/*.class"/>

</batchtest>

</junit>

10 </target>

digitando ant test vengono eseguiti prima "compila" e poi "test".

In linea 4 si vede come si usa un riferimento - prima, ad esempio a partire dalla linea 2 del file di esempio, definisco una proprietà, che decido di chiamare "junit.path", e la inizializzo con il path dell'archivio di JUnit:

<!-- junit location -->

<property name="junit.path" location="/opt/junit4.0/junit-4.0.jar"/>

poi uso questa proprietà in un elemento di un path che chiamo - tramite l'attributo id - "test.class.path":

<path id="test.class.path">

<pathelement path="${junit.path}"/>

<pathelement location="test"/>

<pathelement location="."/>

</path>

a questo punto posso fare riferimento al path suddetto all'interno dell'elemento classpath con refid:

<classpath refid="test.class.path"/>

È opportuno anche aggiungere un task javac al target "compila", per compilare le classi di test, e un task delete al target "clean":

<echo message="Sto compilando le classi di test..."/>

<javac srcdir="test">

<classpath refid="test.class.path"/>

</javac>

...

<echo message="Sto cancellando i file .class in test/..."/>

<delete>

<fileset dir="test" includes="**/*.class"/>

</delete>

Anche in questo caso posso usare il path definito in precedenza.

Nota: una proprietà si può ridefinire da linea di comando passandola ad Ant tramite l'opzione -D, ad esempio:

ant test -Djunit.path=/usr/lib/junit4.0/junit-4.0.jar

Il task jar

Infine esiste un task per produrre un pacchetto jar - in questo caso lo dichiariamo dipendente da "test", in modo da evitare che un'applicazione che non passa i test sia distribuita:

<target name="jar" depends="test">

<echo message="Sto impacchettando l'applicazione..."/>

<jar destfile="algebra.jar" basedir=".">

5 <include name="algebra/*.class"/>

<manifest>

<attribute name="Main-Class" value="algebra.Prova"/>

</manifest>

10 </jar>

</target>

Da notare alle linee 7-9 la possibilità di specificare un file manifesto in linea, senza prima crearlo.