Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
tecniche nuove
Sommario
Introduzione vii
Capitolo 1. Elementi base
Introduzione 1
Obiettivi 2
Direttive 2
1.1 Selezionare nomi significativi per le classi 2
1.2 Selezionare nomi significativi per attributi/variabili e parametri 6
1.3 Selezionare nomi significativi per i metodi 10
1.4 Implementare classi orientate agli oggetti 11
1.5 Porre attenzione alla scrittura dei metodi 14
1.6 Utilizzare correttamente l'ereditarietà 19
Capitolo 3. Approfondimenti
Introduzione 59
Obiettivi 59
Direttive 60
3.1 I Generics 60
3.2 Static import 71
3.3 Auto-Boxing / Unboxing 73
091 ßU|ßÖO| |3U 3J|JS3AU| 1'9
091 aA|UaJ|a
09i ¿9jezz|||jn 6uj66o| jp |OOi a|en{)
8Si (Df) 6u;66oi eABf aipedv
9si ßu|ßßon!jneABf
ojudujeuojzunj 3 Ejnuiuis
8H ffrßOT
m euojs |p,od up
9fr L |A|U3|qO
sn auo|znpojju|
6u;66o| h '9 0|0)jde)
ffrL E3|is|ßßess3Ui |p jujdisjs jp oßdjduiyie ¡a¡ib|3j juia|qojd pissep \ ajejapjsuo)
OH |U0|Z3»3 3||3p BjniBU B| 3JBJ3p|SU0) 9S
OH ¡UOJZ3333 3||3p BJ|A jp 0|3)J |l 3JU3U1BJU3UB 3JBjn|B/\ S S
8£L |UOjZ3333 3||3p BJ^JOU jp B}||BpOUJ B||B dUOjZUdUB 3JBJ frS
SE L ¡IKHZ3M3 3||3p 3SBq ¡d|l | 3JBZZ|||in UON £S
2£l A||eu[| 0»0|q |¡ ajuaiueyajjOD ajBzzjUifl TS
(¿II ¡uojzajja 3| 3 J B n | | | i n L S
m aA|U3J!0
821. OSnjIB BA|JB|3J BISJ3A0JIU03 B"|
921. pd)|)dip 3 3UI|lUnj ¡UOjZd})]
Sil B¡l|3JBJ39
Sil EACf Uj |U0|Z3W8 d||dp P|ipjeja6 en
Kl e|Joaj|p,od un
Kl |A|ua¡qo
£ZL auo|znpoJiu|
m o r o n a d||dp d u o j ) S d 6 ;p e i ß a i e j i s ' S o | o i ; d e 3
81L 3U0|ZB}U3lU3|dlU|,| 3}U3UJB1U3UB 3JBJU3UJUI0) S'fr
£11 jSSEp 3| 3JU3UJBH3JJ03 SJBJUaUJUJO)
601 3ßB)|DBd !P 0||3A{| |B BUOjZBJUaUimOp B| 3JjJ3SUj |p BJ|||q|SSOd B| 3JBjn|BA
SOI DOQBABf IJU3UIUI03 | 3J3AU3S l fr
201 aUOJZBJUBUinJOp B||3U 3Jj)S3AU| ffr
Í01 aA.ma.na
?0l jAjUdjqo
loi auojznpojiui
ÜU8UJUJ03 I 't7 0|01jdP3
AI
6.2 Porre attenzione al contenuto del log 162
6.3 Utilizzare correttamente i livelli di log 163
6.4 Valutare l'impatto sulle prestazioni 164
6.5 Implementare un corretto logging delle eccezioni 166
Appendice A. JavaDoc
Introduzione 247
L'utility JavaDoc 247
Appendice D. Multi-threading
Introduzione 283
Obiettivi 284
Nozioni di base 284
Programmazione MT Java originaria 295
Problemi di programmazione MT in Java 321
Innovazioni del JDK 5 per il MT 324
• sviluppatori che si oppongono con ogni mezzo a richieste di variazioni del codice;
• tempificazioni esagerate anche a fronte di richieste di variazioni minime;
• sistemi che permangono nella fase di test oltremisura; o peggio, che, una volta messi in
esercizio, frequentemente terminano inaspettatamente, oppure che impiegano un tem-
po prolungato per svolgere singoli servizi;
• sistemi che modificati da una parte, presentano comportamenti anomali in altre parti del
sistema apparentemente non relazionate, e così via.
Produrre codice di qualità, pertanto, è il prerequisito per assicurare l'evoluzione del sistema
e 0 relativo adeguamento al perenne cambiamento dei requisiti, senza dover rinunciare a carat-
teristiche chiavi come la robustezza, la consistenza e le buone prestazioni.
Obiettivi
L'obiettivo fondamentale di questo libro è di fornire una guida operativa, concisa e di rapida
consultazione per l'implementazione di sistemi Java (e non solo) di elevata qualità. Pertanto, il
fine ultimo è presentare succintamente linee guida e best practice ma anche le trappole in cui i
programmatori Java potrebbero cadere, illustrando soluzioni e alternative, tutte in un quadro
molto pragmatico, dove la teoria sia ridotta al minimo indispensabile.
Le direttive presentate si fondano su un impianto logico il cui obiettivo consiste nel far ac-
quisire al software realizzato 8 principali caratteristiche che riportiamo di seguito.
Correttezza
La correttezza è il grado di aderenza del software ai requisiti che lo ispirano, e quindi la capa-
cità del sistema di implementare esattamente quanto inizialmente previsto.
Robustezza
La robustezza è intesa coma capacità del sistema sia di rilevare situazioni anomale di funziona-
mento, sia di eseguire le previste procedure di gestione.
Efficienza
Efficienza è la capacità del sistema di produrre gli effetti desiderati con un utilizzo limitato di
risorse, sia temporali, sia fisiche.
Semplicità
Codice semplice è quello che permette di comprendere immediatamente gli algoritmi alla base
del software e di individuare semplicemente la corrispondenza dei vari elementi con lo spazio
del problema automatizzato.
Leggibilità
La leggibilità va intesa come la capacità di far comprendere allo stesso sviluppatore, e a perso-
ne estranee all'implementazione, il fondamento logico dell'implementazione. Ciò include i
pattern e gli algoritmi utilizzati con eventuali varianti, le scelte operate (incluse le relative giu-
stificazioni) e così via. Questa caratteristica è propedeutica ad altre proprietà fondamentali del
software, quali ad esempio la manutenibilità.
Manutenibilità
La manutenibilità è il grado di difficoltà (o semplicità, se si è ottimisti) di correzione e trasfor-
mazione del sistema al fine di adeguarlo alla perenne variazione dei relativi requisiti.
Trasportabilità
La trasportabilità è intesa come la capacità del sistema di funzionare correttamente anche in
ambiti diversi da quelli originariamente considerati.
Generalizzabilità
La generalizzabilità rappresenta il grado di generalità del software. Più è elevata e, chiaramen-
te, maggiore è la classe di problemi in grado di risolvere e quindi maggiore è il risultato dell'in-
vestimento (ROI, Return On Investment) dell'azienda produttrice.
Molta enfasi è poi stata conferita ad aspetti di importanza fondamentale per la produzione di
sistemi software di qualità come la gestione delle eccezioni, il logging, i test di unità e di integra-
zione e il processo di build. Pertanto, non solo è importante produrre un sistema di elevata
qualità, ma è anche fondamentale utilizzare una serie di best practice, come, per esempio, orga-
nizzare correttamente il progetto e utilizzare un sistema per il build continuo del sistema, al fine
di raggiungere questo risultato nella maniera più efficiente possibile e minimizzando i rischi.
Questo volume non intende illustrare né i principi fondamentali delTObject Oriented, né
insegnare il linguaggio di programmazione Java, per quanto diverse digressioni e riferimenti
facciano capolino qua e là in molti capitoli. Si tratta di tematiche imprescindibili che, in questo
contesto, assumono il ruolo di prerequisiti.
Genesi
L'idea di scrivere questo libro è nata come risposta a una necessità pratica. In molte occasioni
l'autore si è trovato a dover istruire personale junior, a dover fornire delle specifiche circa la
creazione dell'ambiente di sviluppo, a dover eseguire la revisione del codice prodotto da altri
membri del team, e così via. In tutti questi casi, di fronte alle richieste dei collaboratori di fornire
delucidazioni relative alle regole da seguire per produrre software di maggiore qualità, o sempli-
cemente di fronte a richieste di suggerimenti relativi alla documentazione da consultare, era
naturale suggerire una serie di libri, spesso voluminosi (molti dei quali inclusi nella bibliografia).
Questi libri, anche se rappresentano il bagaglio culturale fondamentale di ogni programmatore
Java, non costituivano però una risposta soddisfacente per coloro che, già a conoscenza del
linguaggio Java e delle fondamentali leggi dell'OO, necessitavano di reperire una sorta di
vademecum che, riducendo al minimo l'illustrazione della teoria, proponesse una serie di rapide
linee guide, supportate da esempi pratici, atte a migliorare la qualità del software prodotto.
Pertanto, l'idea base di questo libro è proprio quella di dar vita ad una sorta di vademecum di
carattere operativo per i programmatori Java, volto a fornire best practice, direttive e quant'altro,
al fine di supportare il miglioramento della qualità del software, senza dover necessariamente
ripetere tutta la teoria di base. L'impronta operativa del testo si riscontra sia dagli innumerevoli
esempi riportati, sia dal dominio di appartenenza: la maggior parte degli esempi, infatti, è stata
prelevata direttamente dai sorgenti delJDK 1.5. Questa decisione ha permesso sia di minimizza-
re presentazione e spiegazione dei vari esempi (le principali API Java, infatti, dovrebbero appar-
tenere al dominio di conoscenza della quasi totalità dei lettori), sia di fornire esempi reali.
Il libro si basa sulla versione JDK 1.5.
A chi è rivolto
Questo libro è rivolto alla comunità dei programmatori Java. In particolare, si tratta di un
supporto per tutti coloro che sviluppano quotidianamente codice scritto in Java, indipenden-
temente dalla loro esperienza. Questo libro, inoltre, è una guida per tutti coloro che hanno il
compito di revisionare il codice prodotto nella propria azienda, sia al fine di assicurare il rispet-
to dello standard qualitativo aziendale, sia per fornire un fondamentale feedback agli autori del
codice stesso. Infine, questo libro dovrebbe risultare molto utile a tutti coloro che sono chia-
mati a scrivere gli standard qualitativi aziendali.
Struttura
La struttura di questo libro è organizzata nei capitoli seguenti.
Presentazione
Questa sezione, come suggerisce il nome, è dedicata alla presentazione del libro e pertanto il
lettore ne ha già letto circa la metà. In particolare, gli argomenti trattati in questa sezione sono:
gli obiettivi, il potenziale pubblico dei lettori, la struttura, e, come per qualsiasi libro che si
rispetti, l'immancabile sezione dedicata ai ringraziamenti.
Capitolo 3. Approfondimenti
Questo capitolo è dedicato ad argomenti di livello tecnico molto avanzato come il multi-tbreading
e a specifici package/costrutti introdotti con il JDK 1.5, quali per esempio i generics e il nuovo
package della concorrenza. Gli argomenti proposti, gioco forza, sono completamente incentra-
ti su Java. In questo capitolo, Java sale in cattedra per assumere il ruolo centrale che gli spetta.
Molti degli argomenti trattati presentano un elevato livello di difficoltà. E il caso del multi-
threading per il quale è stata realizzata un'apposita appendice che probabilmente vale la pena
leggere prima di avventurarsi in questo capitolo.
Capitolo 4. I commenti
In questo capitolo sono presentate una serie di direttive atte a migliorare l'efficacia dei com-
menti del codice. Sebbene la quasi totalità della comunità informatica sia concorde nel ricono-
scere l'importanza di un codice ben documentato e del fatto che i commenti siano un requisito
fondamentale per la qualità del software, ci si imbatte spesso in applicazioni mal commentate
0 addirittura non commentate in alcun modo. Il problema fondamentale è che raramente una
determinata porzione di codice, per tutto il suo ciclo di vita, sarà mantenuta da chi l'ha scritta
per primo. Molto più frequente è il caso che diverse persone si avvicendino alla sua manuten-
zione. Pertanto, codici che presentano problemi per quanto riguarda chiarezza, comprensibilità
e facilità di manutenzione corrono il serio rischio di essere buttati via e riscritti. Cosa che,
ovviamente, non dovrebbe generare il compiacimento di nessun autore di codice.
Capitolo 6. Il logging
Una strategia di logging ben congeniata è un altro elemento di importanza fondamentale per
garantire una elevata qualità ai sistemi. Spesso anche questa attività è trascurata e/o realizzata
di fretta nei giorni precedenti al rilascio del sistema e/o eseguita in maniera casuale senza segui-
re specifiche linee guide. Gli inconvenienti generati da questi approcci emergono in tutta la
loro drammaticità quando, una volta installato il sistema in ambiente UAT o, peggio ancora, in
produzione, si ha la necessità di correggere i primi malfunzionamenti. Solo a questo punto,
venendo meno la possibilità di poter utilizzare sofisticati strumenti di debug, ci si rende conto
di quanto sia difficile analizzare anomalie senza un logging chiaro e consistente...
Obiettivo di questo capitolo è fornire un insieme di best practice e linee guide affinché la
strategia di logging sia presente nel sistema fin dalle primissime fasi e affinché questa si evolva
di pari passo con il sistema stesso.
Appendice A. JavaDoc
Questa appendice è dedicata alla presentazione dell'applicazione di utilità Java per la produ-
zione automatica della documentazione: JavaDoc. In particolare, molta attenzione è assegnata
ai diversi tag.
Appendice C. Hashing
Questa terza appendice è dedicata a un concetto molto interessante: Vhashing. Si tratta di un
teoria impiegata in diversi ambiti dell'informatica: dalla crittografia alle strutture dati.
In questa appendice, però, l'attenzione è focalizzata sul secondo aspetto. Ciò perché i pro-
grammatori Java, sia direttamente (estensione del metodo hashCode della classe java.lang.Object),
sia indirettamente (utilizzo delle collezioni java.util.Hashtable e java.util.HashMap), utilizzano fre-
quentemente il concetto di hashing con una frustrazione abbastanza ricorrente: interpellando
diversi programmatori, infatti, è facile riscontrare come questo concetto sia spesso avvolto da
una "sacra inibizione".
Appendice D. Multi-threading
Questa corposa appendice è dedicata alla programmazione multi-threading (MT). In particola-
re, si affrontano sia i concetti base della programmazione concorrente in MT, sia le tematiche
più avanzate legate al linguaggio Java. Poiché il MT è una tecnica di programmazione, il lin-
guaggio selezionato finisce gioco forza per offrire una serie di opportunità peculiari e porre
immancabili vincoli la cui intima comprensione è propedeutica alla produzione di sistemi MT
efficaci e che funzionano.
Le principali tematiche affrontate sono le tipiche problematiche della programmazione MT
in Java, i costrutti fondamentali dedicati alla programmazione concorrente e anche 0 nuovo
package Java specificamente dedicato alla concorrenza: java.util.concurrent. Riteniamo che que-
sta appendice rappresenti una risorsa molto utile, poiché riassume in un unico testo aspetti
molteplici e aggiornati.
Ringraziamenti
Il primo ringraziamento d'obbligo nonché la personale riconoscenza dell'autore va agli altri due
membri dell'ormai consolidata "banda dei tre", che, come al solito, lo hanno assistito e consiglia-
to nel faticoso onere di scrivere un altro libro... Sarà l'ultimo... Fino a quando non si inizierà il
prossimo! In particolare si ringraziano gli amici Roberto Virgili, team leader/project manager,
nonché architetto e sviluppatore di sistemi distribuiti, e Antonio Rotondi, project manager di
notevole caratura ed esperienza tecnica, che negli ultimi anni si è speso per la messa in opera di
progetti globali per banche di investimento di grandi dimensioni. Tecnici informatici di profonda
levatura ed esperienza, con il vizio di realizzare sistemi informatici che funzionano veramente.
Altri ringraziamenti spettano a Giovanni Puliti di Imola Informatica (gpuliti@mokabyte.it, Java
enthusiast, project manager esperto di Java e tecnologie annesse, autore, nonché creatore e
direttore della rivista web www.mokabyte.it) che si è dimostrato, da subito, entusiasta all'idea di
questo libro. Un particolare ringraziamento va poi a Francesco Saliola che, come al solito, ha
curato la redazione e l'impaginazione del libro: ormai pensa in Java e scrive log JIRA per richie-
dere correzioni/miglioramenti.
Sentiti ringraziamenti spettano di diritto alla casa editrice Tecniche Nuove per il coraggio di
continuare nella pubblicazione di libri tecnici scritti nella meravigliosa lingua di Dante, e per la
disponibilità e la comprensione sempre dimostrate.
Dulcis in fundo, la gratitudine dell'autore va ai familiari, a Vera per la tanta pazienza dimo-
strata e a tutti coloro che sono stati presenti nei momenti di bisogno.
Convenzioni grafiche
Questo carattere senza grazie è utilizzato per i termini appartenenti al linguaggio di programma-
zione, come ad esempio class, HashTable, e così via. Pertanto una loro non corretta digitazione
genera un errore di compilazione
Il carattere più piccolo, di corpo ridotto, in paragrafi rientranti come questo è utilizzato per parti
di testo introduttive o note a margine la cui lettura e comprensione non sono strettamente necessarie
per l'apprendimento di quanto riportato successivamente.
Non è infrequente il caso in cui per illustrare al meglio una best practice sia più facile partire
da una worst practice. Spesso, enfatizzando gli errori, si riesce a fornire un sistema più immedia-
to e intuitivo per comprendere approfonditamente le motivazioni intrinseche di una serie di
suggerimenti, idee e regole. Inoltre, spesso è necessario evidenziare alcune trappole e insidie
per evitare che queste finiscano per avere la meglio sugli sviluppatori.
L'icona presentata a fianco, con il chicco di caffè Java "bollito", serve proprio a
questo: enfatizzare chiaramente porzioni di codice utilizzate per mostrare errori,
problemi e trappole, ossia "worst practice". Come tali, ovviamente, non devono
assolutamente essere prese ad esempio positivo.
Ed è poesia
La realizzazione di codice di programmazione può apparire, a chi non vi si addentri, come un
mondo distante alieno, un deserto privo di umanità.
Non è così. Nella volta celeste dei byte luminosi c'è spazio anche per ben altro, anche per
momenti di riflessione, di arte e di poesia.
Di seguito sono riportate tre brevi composizioni inedite di Leonello Tatti (leonellotatti@libero.it),
affermato poeta, che il buon Dio mi ha donato come zio.
Lampi
Distanti le mura fanno da cornice ad uno sfondo sublime
e le luci che vi si riflettono sembrano apparire fugaci
per poi diventare aggressive e senza confini.
Attimi di pura fantasia, immagine infinita, essenza
stravagante, misura di tempo.
Oltre il pensiero la figura emerge e si insinua timida fra le pieghe
di sommessi tratti e successive definizioni
Fino ad interrompere le visioni.
Ombra sottile, generosa amante, pensiero invadente, struttura
appagante, sentiero nascosto, vita che traspare.
Evasione
Conosco bene le mura del mio paradiso...
mi destano... e fra di esse ogni giorno riscopro il mio sole la mia luna.
Solo ad esse posso parlare se voglio essere ascoltato.
Non c'è nulla al di là del mio paradiso... solo, la mia illusione
di poter toccare respirando, un'aria a me nuova
pensando, a come potrebbe essere la mia vita
se solo ci fosse al posto di questa apatia,
di questa solitudine, un uragano dentro di me
che mi scuotesse e mi portasse via.
Ma io non ho altro che questo paradiso.
Rifugio sicuro
Rifugio sicuro più non sei...
quell'abete candido ed inerme che dinnanzi
la mia strada trovai, ora vibra d'immortale possenza
e se pur tanto è il dominio suo nel tollerare
qualsivoglia gesto, a nulla può valere quella linfatica scintilla
di cui donata la mia essenza si priva.
Rifugio sicuro più non sei...
quel mio attingere dannato, volle far sì che impotente subissi
e quell'abete candido ed inerme, tu scordato hai.
Capitolo
Elementi base
Introduzione
Questo capitolo è dedicato all'illustrazione di un insieme iniziale di linee guida di carattere
generale relativo alla programmazione. L'obiettivo fondamentale è favorire la realizzazione di
programmi di migliore qualità. In particolare, in questo primo capitolo l'attenzione è principal-
mente focalizzata sulla produzione di codici più facilmente leggibili e quindi più facilmente
comprensibili, mantenibili e riusabili. L'enfasi, pertanto, è quasi interamente conferita a carat-
teristiche "stilistiche" della programmazione, mentre considerazioni relative al miglioramento
delle performance, a un più efficiente utilizzo della memoria e al miglioramento della
trasportabilità sono rimandate ai capitoli successivi. Tuttavia, compaiono anche tematiche più
complesse, come per esempio l'intelligente ricorso alla relazione di ereditarietà la riduzione
dell'accoppiamento tra classi.
Benché questa trattazione sia esplicitamente orientata a linguaggi di programmazione basati
sul paradigma Object Oriented (OO), e in particolare al linguaggio Java, le direttive proposte si
prestano a essere facilmente adattate ad altri linguaggi di programmazione anche non necessa-
riamente basati sul paradigma OO.
Considerata la caratteristica di generalità di questo primo capitolo, vi compaiono sia linee
guida relative a concetti basilari come le dimensioni ottimali di classi e dei metodi, ad una miglio-
re selezione dei nomi dei vari elementi del linguaggio di programmazione (attributi, metodi,
interfacce e classi), sia altre di carattere più squisitamente OO, come coesione, accoppiamento,
etc. Benché queste ultime regole presentino un livello concettuale molto diverso da quelle più
immediate presenti nel primo gruppo, si è comunque deciso di illustrarle in quanto rappresenta-
no concetti fondamentali della programmazione, frequentemente nominati e discussi.
Pertanto, benché l'obiettivo principale del libro sia quello di presentare aspetti molto pratici
riducendo al minimo la teoria, non è infrequente il caso in cui la trattazione si orienti verso
tematiche più astratte. Ciò è necessario sia per evitare un'eccessiva costrizione della trattazione
che finirebbe per porre un eccessivo limite, sia per fornire indicazioni a tutti colori che intenda-
no approfondire tematiche meno pratiche a maggior grado di astrazione.
La piena comprensione di quanto riportato in questo capitolo non richiede particolari
prerequisiti, sebbene una minima esperienza di programmazione Java sia sicuramente di aiuto.
Per quanto la maggior parte delle direttive presentate in questo capitolo possano, a tratti,
sembrare basilari e quasi scontate, nella pratica lavorativa esse vengono non di rado trascurate.
Obiettivi
L'obiettivo principale di questo capitolo consiste nel supportare la produzione di codice più
facilmente comprensibile. Si tratta di una caratteristica fondamentale del software ed è pre-
requisito irrinunciabile per un'altra caratteristica fondamentale: la manutenibilità. Quest'ulti-
ma ha assunto un ruolo fondamentale nei moderni processi di sviluppo del software essenzial-
mente per due motivi. In primo luogo perché è ormai universalmente accettato il fatto che i
requisiti del sistema siano un'entità dinamica e quindi in continua evoluzione. Ciò rende im-
possibile e/o non conveniente "congelare i requisiti". La logica conseguenza è che ogni software
di successo debba essere continuamente rivisto al fine di adeguarlo al perenne cambiamento
dei requisiti. In secondo luogo perché la quasi totalità dei moderni processi di sviluppo del
software include approcci di carattere iterativo e incrementale al fine di controllare più effica-
cemente i rischi progettuali. Ciò fa sì che la versione "finale" del sistema sia ottenuta attraverso
una serie di iterazioni, ognuna delle quali aggiunge un determinato incremento alla versione
precedente che, tipicamente, implica l'aggiornamento del codice prodotto precedentemente.
In ogni modo, produrre codici facilmente leggibili è importante per i seguenti motivi:
• permette di capire più velocemente, e spesso in maniera più completa, il problema che il
codice risolve: quindi semplifica l'attività di test e di individuazione di eventuali errori;
• semplifica l'utilizzo di approcci iterativi e incrementali che spesso richiedono, a persone
diverse, di modificare parti di programma realizzate in precedenza;
• facilita la comunicazione in termini degli algoritmi selezionati, delle scelte operate, e
così via: ciò è particolarmente importante sia per l'attività di revisione del codice, sia in
contesti di progetti di media-grande difficoltà;
• semplifica l'adeguamento del codice al perenne cambiamento dei requisiti.
Daremo pertanto di seguito delle indicazioni, delle direttive, che in questo caso saranno
numerate per poter fare riferimento ad esse nel corso di tutto il libro.
Direttive
1.1 Selezionare nomi significativi per le classi
La selezione dei nomi delle classi dovrebbe avvenire principalmente durante la fase di disegno
del sistema. Tuttavia, in alcuni contesti molto ben limitati e definiti, come per esempio circoscritte
investigazioni (pratica c o m u n e m e n t e nota con il nome di speak programmimi, può risultare
opportuno procedere con la codifica a partire da un documento di disegno molto essenziale. In
scenari di questo tipo, il programmatore si trova nella situazione di dover disegnare parte del
software direttamente codificando. Inoltre, anche in scenari più formali, l'attività di disegno non
dovrebbe mai giungere a un eccessivo livello di dettaglio. Ciò, probabilmente, risulterebbe in un
cattivo utilizzo del tempo a disposizione e c o n d u r r e b b e alla mortificazione del team di sviluppo.
Comunque sia, è abbastanza frequente la creazione di classi, inizialmente non previste, direttamente
nella fase di sviluppo e vi è quindi la necessità di includere tale serie di regole in questo volume
fortemente orientato alla programmazione.
Alcuni testi, non eccessivamente moderni, indicano in 15 lettere il valore ideale per la lunghezza
dei nomi di classi. Probabilmente, si tratta di un'indicazione eccessivamente restrittiva visti i
servizi esposti dai moderni I D E (Integrated Development Environment, Ambienti di Sviluppo
Integrato), ma che c o m u n q u e fornisce un'idea dell'ordine di grandezza.
Alcuni esempi di validi nomi di classe sono riportati nella tabella 1.1.
Dominio Esempi
A g e n z i a di viaggi Flight, Travel, Booking, Customer, Itinerary, Geographicllnit, Nation, City,
TimeZone, ShoppingCart, etc.
I s t i t u z i o n e di i n v e s t i m e n t o Currency, Price, StreamPrice, Quote Trade, Counterparty, Settlement,
HolidayCalendar, Transfer, Instrument, etc.
Biblioteca Book, Paper, Article, Author, Picture, Editor, etc.
Università Lecture, Topic, Teacher, Student, Thesis, Department, etc.
Da notare che mentre gli attributi come titolo. numeroPagine e prezzoConsigliato, sono veri e
propri attributi, caseEditrici, autori sono relazioni con altri oggetti. Queste distinzioni però, sebbene
siano fondamentali in termini di disegno, lo sono molto meno in termini implementativi.
Per quanto concerne il nome delle variabili, esso dovrebbe ricordarne l'utilizzo. Alcuni esem-
p i s o n o : b o o l e a n s k i p W h i t e S p a c e , int l e n g t h , c h a r c u r r e n t C h a r , b y t e [ ] b u f f e r , e t c .
Una convenzione efficace per la dichiarazione dei parametri, molto utile per i metodi costruttori
e modificatori, consiste nell'aggiungere al nome un prefisso formato dall'articolo indeterminati-
vo. Ciò evita eventuali conflitti con gli attributi di classe, che comunque in Java si risolvono
inserendo la parola chiave this per identificare gli attributi di classe. Ecco un esempio di un
costruttore della classe Byte. (L'implementazione Sun utilizza la convenzione this.value = value).
! "
Un'altra tecnica molto interessante consiste nell'aggiungere al nome della proprietà il suffis-
so new. Per esempio si consideri il metodo riportato di seguito atto a impostare il limite del
Buffer, nella classe java.nio.Buffer. Ecco il metodo limit della classe java.nio.Buffer.
limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
)
Il metodo rehash serve per incrementare la capacità di un oggetto Hashtable e quindi per meglio
organizzare gli elementi riducendo il numero di conflitti e aumentandone l'efficienza.
modCount++; modCount++;
threshold = (int) (capacity * loadFactor); threshold = (int) (newCapacity * loadFactor);
table = mapp; table = newMap;
tor (Entry<K,V> mapn = map[i]; m a p n ! = n u l l ; ) { for (Entry<K,V> old = oldMap[i] ; old != null ; ) (
capacity; newCapacity,
mapp[i1] = e; newMap[index] = e;
I I
1.2.5 Evitare di utilizzare la stessa variabile per scopi diversi
Non è infrequente analizzare implementazioni di metodi in cui una stessa variabile sia riutilizzata,
all'interno di uno stesso metodo, per scopi completamente diversi. Questa pratica dovrebbe
essere evitata in quanto tende a ridurre il grado di leggibilità del codice e, frequentemente, a
confondere il lettore. Inoltre, "pseudo-ottimizzazioni" di questo tipo raramente portano un
vantaggio reale e anzi finiscono per degradare la leggibilità del codice. Chiaramente, un discor-
so completamente diverso vale per tentativi di "riciclare" oggetti, soprattutto in contesti di
programmazione concorrente.
Inoltre, come visto in precedenza, in questi casi è sufficiente assegnare al parametro un pre-
fisso costituito dall'articolo indeterminativo: aValue.
La notazione ungherese (liunganan Notation in Inglese, il cui nome è un omaggio al suo ideatore
Charles Simonyi di origine appunto ungherese) è una convenzione nata in casa Microsoft per
l'attribuzione di nomi di parametri, variabili e attributi particolarmente popolare nella comunità
di programmatori C e C++. L'idea base consiste nell'assegnare un prefisso ai nomi al fine di
specificarne il tipo. L'obiettivo consiste nel permettere agli sviluppatori di capire il tipo di una
variabile semplicemente dal relativo nome. Si tratta di una convenzione spesso utile, in quanto
evita di dover effettuare continui salti di pagina dovuti al fatto che l'utilizzo di variabili,
frequentemente, avviene in punti distanti dalla relativa dichiarazione. Per esempio: iCOUilter
rappresenta un contatore intero, bFOUnd rappresenta una variabile di tipo booleana, OACCOUflt
rappresenta il riferimento a un oggetto di tipo account, e così via.
La notazione ungherese è oggetto di diversi dibattiti tra chi la considera molto utile e altri
che invece le addebitano di portare alla generazione di codice meno leggibile.
In questo contesto non viene presa alcuna posizione, ad eccezione del fatto che è fondamen-
tale preservare la coerenza. Pertanto, qualora si decidesse di usare la notazione ungherese, è
necessario utilizzarla in maniera costante e consistente. Nella tabella 1.3 viene proposto un
elenco standard di prefissi.
Infine, qualora si debba intervenire per modificare del codice scritto da altri, è consigliabile
mantenere la convenzione utilizzata dall'autore del codice onde evitare confusione.
Prefisso Tipo
b Boolean
c Char
by Byte
s Short
i Int
I Long
f Float
d Double
0 Object
e Exception
1.3.4 Non ripetere il nome della classe nei nomi dei metodi
Come visto in precedenza, per la selezione dei nomi dei metodi si dovrebbe limitare l'attenzio-
ne ai soli verbi, sebbene sia frequentemente necessario ricorrere a nomi composti. Spesso però
si compone il nome dei metodi utilizzando anche il nome della classe di appartenenza. Sebbe-
ne ciò non sia un problema serio, si tratta comunque di una ripetizione inutile.
Si consideri per esempio la classe java.util.Vector. Alcuni dei suoi metodi sono: addElement,
i n s e r t E l e m e n t A t , e l e m e n t A t , size, f i r s t E l e m e n t , l a s t E l e m e n t , i n d e x O f , e t c .
1.3.5 Utilizzare una convenzione chiara nella selezione dei nomi dei metodi
Nel disegno di classi succede spesso di dover realizzare metodi ricorrenti come per esempio
"inserimento", "eliminazione", "ricerca" di elementi e così via. In questo caso si consiglia di
selezionar coerentemente i nomi per questi metodi. Per esempio, analizzando le classi Java è
possibile evidenziare le convenzioni riportate nella tabella 1.4.
Tabella 1.4 - Tabella con la convenzione java per alcuni nomi di metodi ricorrenti.
sivo di classi, chiaramente, è altrettanto sbagliato e tende a creare una serie di problemi legati a
un elevato accoppiamento.
Ogni qualvolta la dimensione di una classe cominci a passare i limiti, dovrebbe essere natu-
rale interrogarsi se si stiano inglobando più concetti distinti in una sola classe; e, se non è così,
è comunque bene chiedersi se l'introduzione di opportune classi "helper" possano facilitarne
la comprensione.
Da notare che per quanto concerne il livello di accesso "amichevole", in Java questo è dichiarato
omettendo il livello di accesso di un elemento. Inoltre, un elemento il cui livello di accesso è
amichevole è inaccessibile a tutte le classi che non si trovino nello stesso package, anche qualora
queste siano classi figlie.
L'applicazione della legge dell'incapsulamento, per quanto concerne gli attributi, implica di
utilizzare prevalentemente un livello di accesso privato. Qualora una classe possa prevedere
Protetto
s V V X
protected
Pubblico
• •/ •
public
(m - I ( n r ) / a )
c=
( m -1)
dove
Da notare che minore è il valore di C e maggiore è il grado di coesione della classe. In sostanza,
maggiore è il numero di metodi che accedono a diversi attributi della classe, e superiore è il grado
di coesione. Questa formula considera anche casi in cui più classi siano state inglobate in una
sola, caratterizzati dall'esistenza di diversi gruppi (logici) di attributi, acceduti ciascuno da un
insieme distinto e ben definito di metodi. Pertanto la classe espone servizi che eseguono compiti
non relazionati.
I problemi tipici generati da classi a bassa coesione sono relativi alla difficoltà di compren-
sione del codice e quindi di manutenzione, di laboriosità nell'eseguire i vari test, impossibilità
nel riutilizzo, difficoltà di isolare elementi soggetti a variazioni, ecc. In casi estremi si giunge
alla perdita di controllo dell'intera applicazione caratterizzata da servizi sparpagliati in classi in
cui non dovrebbero appartenere e conseguente incremento del grado di accoppiamento.
Un esempio di classe a bassa coesione è relativa alla rappresentazione di un utente del siste-
ma (User) ove attributi del tipo name, surname, dateOfBirth, gender, etc. siano combinati con altri
del tipo currency, value, etc. Lo stesso vale nel caso in cui in una classe di tipo carrello della spesa
(Trolley), si trovassero dei metodi del tipo empty, addltem, verifyContent, etc. e altri del tipo placeOrder,
findllsers, etc.
Per valutare il grado di coesione di una determinata classe, non sempre è necessario applica-
re un approccio formale basato sulle formule. Infatti, problemi di scarsa coesione possono
essere facilmente rilevati da una serie di segnali di allarme. Il più evidente è connesso alle
dimensioni delle classi. Qualora una classe contenga troppi attributi, oppure troppe relazioni
con altre classi, oppure un numero eccessivo di metodi, molto probabilmente il livello di coe-
sione di questa non è molto elevato. Pertanto è probabile si abbia a che fare con una classe che
ne ingloba altre. Un altro segnale è rappresentato dalla presenza, nella medesima classe, di
attributi partizionabili in insiemi distinti e non relazionati, e dall'avere metodi che non accedo-
no mai ad attributi appartenenti a diversi insiemi. Un altro indicatore di scarsa coesione è dato
da situazioni in cui non si riesca ad identificare un nome preciso per una classe oppure questo
risulta troppo generico.
il (newCapacity < 0) (
newCapacity = lnteger.MAX_VALUE;
I else il (minimumCapacity > newCapacity) I
newCapacity = minimumCapacity;
I
il (InitialCapacity < 0)
throw new IHegalArgumentExceptionf'lllegal Capacity: "+initialCapacity);
ensureCapacity
if (newCapacity < 0) (
newCapacity = lnteger.MAX_VALUE
) else if (newLength > newCapacity) {
newCapacity = minimumCapacity;
Z/ I A\ if (o == this)
return true;
1. metodi che forniscono un servizio semplicemente elaborando i dati di input senza ricor-
rere all'utilizzo di altri dati e senza utilizzare lo stato dell'oggetto (per esempio in Java i
metodi della classe java.lang.Math, come Matti.absQ);
2. metodi che comunicano una porzione dello stato interno di un oggetto, oppure elabora-
no risultati dipendenti da esso, senza modificarlo (per esempio i famosi metodi getXQ);
3. metodi che aggiornano lo stato interno di un oggetto (per esempio i metodi setXQ).
Per quanto concerne la prima tipologia di metodi, essi presentano un accoppiamento nullo
quando risultano privi di effetti collaterali (i famosi side effects), ottenuti mutando lo stato di un
oggetto, e operano dunque esclusivamente sui parametri di input. Qualora questi metodi utiliz-
zino altri dati, magari privati all'oggetto, di cui però si abbia strettamente bisogno, si ha ancora
un accoppiamento minimo. Per quanto concerne i risultati generati, il metodo deve produrre
unicamente un dato atomico o eventualmente un altro oggetto di cui è fornito il riferimento in
memoria. Per mantenere un accoppiamento nullo, il metodo, durante la propria esecuzione,
non deve poi delegare ad altri parte del proprio processo (non deve invocare altri metodi). Da
quanto riportato, è evidente che non sempre un accoppiamento nullo è assolutamente indi-
spensabile e desiderabile. Anzi, spesso sono accettabilissimi alcuni compromessi, purché l'ac-
coppiamento resti minimo, magari al fine di soddisfare altre proprietà di qualità del software,
come per esempio rendere i metodi più leggibili, manutenibili, riusabili, etc. magari derogando
parte del proprio lavoro ad altri metodi.
Nel caso di metodi del secondo tipo, si ha un accoppiamento minimo quando il metodo, per
generare i risultati della propria elaborazione, utilizza i parametri di input e accede ai soli attri-
buti e metodi della classe (sia statici che non). Ancora una volta restituisce un valore atomico o
un riferimento a un apposito grafo di oggetti o, eventualmente, genera un'eccezione per comu-
nicare uno stato di errore. Metodi di questo tipo, pertanto, accedono allo stato dell'oggetto
senza però modificarlo e utilizzano esclusivamente proprietà (metodi e attributi) della classe o
dell'oggetto stesso.
Per i metodi dell'ultimo tipo, la materia non varia di molto. La differenza è che la propria
esecuzione altera lo stato dell'oggetto. Chiaramente un accoppiamento minimo non prevede la
variazione dello stato di altri oggetti.
1.6.2 Non utilizzare l'ereditarietà per oggetti che possono "cambiare tipo"
L'ereditarietà presenta una serie di problemi qualora un'istanza di una determinata classe ab-
bia necessità, in qualche modo, di "trasmutare tipo" durante il proprio ciclo di vita. In questi
casi, un'alternativa migliore consiste nel rappresentare questo comportamento per mezzo di
opportune versioni della relazione di associazione (composizione) e non con legami di
generalizzazione. Per chiarire quanto espresso, si consideri l'esempio classico relativo alla clas-
sificazione dei ruoli degli "attori" di una compagnia area. In prima analisi alcuni programmato-
ri sarebbero portati a definire una classe base, probabilmente astratta denominata Person, e
quindi a specializzarla con classi del tipo Pilot, Crew, Passenger, etc. Questo è un chiaro esempio
di errato utilizzo della relazione di ereditarietà: alcune entità possono cambiare il loro "tipo"
durante il relativo ciclo di vita. Per esempio, un pilota frequentemente è anche un passeggero.
Pertanto, una soluzione migliore consiste nel realizzare comunque la classe Person, questa volta
concreta e associarla, attraverso apposita associazione, con una classe astratta denominata Role,
le cui specializzazioni sono appunto le classi Pilot, Crew, Passenger, etc.
Obiettivi
L'obiettivo di questo capitolo è fornire una serie di direttive operative, best practice e quant'al-
tro al fine di supportare il miglioramento della qualità del software prodotto, Java e non. In
particolare, in questo contesto sono analizzate anche caratteristiche quali performance e
portabilità e così via. La lettura di questo capitolo dovrebbe permettere di considerare e di
identificare tutta una serie di imprecisioni sistematicamente commesse da diversi sviluppatori
ed eventualmente di apprendere proficue tecniche di programmazione Java. In questo capitolo
sono affrontate diverse tematiche abbastanza complesse, come la manipolazione dei campi
date, i campi numerici, l'utilizzo degli stream, la corretta conclusione dei programmi Java, etc.
Direttive
2.1 Investire nello stile
Uno stile di programmazione lineare, razionale e consistente è requisito fondamentale per la
produzione di codice più facilmente leggibile e comprensibile. Logica conseguenza di queste
due caratteristiche è un codice più facilmente testabile, mantenibile e perfino riusabile. Ciò è
particolarmente importante considerando che:
• una buona percentuale del ciclo di vita del software è utilizzato dal processo di manutenzione
(a seconda dello studio considerato, questo fattore può variare da un minimo di 4 0 % ad un
massimo di 9 0 % ) ;
• lo stesso codice, durante l'intero ciclo di vita, tende a essere mantenuto da diverse persone;
è raro che il codice sia scritto e mantenuto dalla stessa persona;
Pertanto è opportuno investire nello stile del codice partendo dall'utilizzo di una convenzione
largamente condivisa. Ciò permette di ridurre i tempi di apprendimento del codice e ne favorisce
una comprensione più approfondita, che quindi ne consente un maggiore riuso, una più semplice
manutenzione, e cosi via.
1. non cambierà pressoché mai. In questo caso è sufficiente utilizzare una "costante"
Java. Per esempio:
2. ha buone probabilità di venir aggiornato. In questo caso il ricorso a una variabile statica
non costituisce una buona pratica poiché variazioni del valore richiedono di ricompilare
il codice, distribuirlo, etc. In questo caso è più opportuno inserire tale valore in un
opportuno file di configurazione (property file, file XML, etc.).
3. seppur con scarsa probabilità, potrebbe comunque cambiare. In questo caso, una vali-
da strategia consiste nell'incapsulare i valori in opportuni metodi, affinché un eventuale
cambio di strategia risulti assolutamente trasparente. Per esempio: getTextFileExtension().
public AppIConstants {
// C o m m o n Strings
public static final String N E W _ U N E = System.getPropertyf'line.separator");
public static final String FILEJ3EPARAT0R = System.getPropertyffile.separator");
public static final String PATH_SEPARATOR = System.getProperty("path.separator");
1
2.2.4 Evitare Yhard-coding dei caratteri utilizzati per terminare una linea
Anche questa regola rappresenta un'ulteriore enfatizzazione di quanto enunciato in preceden-
za ma si tratta di un aspetto troppe volte trascurato.
Il problema consiste nel fatto che il terminatore di linea nei file di testo varia a seconda della
piattaforma di riferimento. In particolare, è possibile avere tre differenti convenzioni: "\n"
(Windows), "\r" (Unix) e "\r\n" (Apple). Per esempio, la seguente istruzione:
1)
user=vettitagl
attempts=2
Per evitare problemi di questo tipo è sufficiente utilizzare come sequenza di nuova riga, il
risultato della seguente istruzione
System.getPropertyfline.separator").
// some code
return talse;
I tinally {
return true;
)
il (properties != null) I
Come si può notare, dopo aver eseguito i controlli di rito, si esegue il cast (prop = (Properties)
properties;). Quello che si deve evitare, a meno che non sia strettamente necessario, è esattamen-
te quello che accade nel codice presentato: accettare un parametro generico e poi eseguirne il
down-casting. Questa pratica, come regola generale, è sconsigliabile poiché la firma del metodo
permette di dedurre, legittimamente, che il metodo preveda un tipo generico, mentre poi
l'implementazione ne richiede uno più specifico. Pertanto, l'implementazione del metodo ri-
chiede vincoli più stringenti di quelli sanciti dalla relativa firma, e quindi, ne viola il contratto.
Il down-casting però non è sempre evitabile. Per esempio è frequentissimo nell'im-
plementazione dei metodi equals. A un certo punto, infatti, è necessario passare dal tipo genera-
le Object a quello specifico per poter valutare l'uguaglianza dei vari elementi specifici. Da notare
che nel caso dell'equals però, questo è sia necessario per implementare una serie di metodi
generici (come per esempio indexOf, lastlndexOf, remove, etc. nelle liste), sia è consentito: non
avrebbe senso cercare di confrontare oggetti diversi e qualora si cercasse di fare ciò, il metodo
risponderebbe correttamente con un valore false.
Listlterator<E> e1 = listlterator();
Listlterator e2 = ((List) o).listlterator();
Questa regola, in prima analisi, potrebbe risultare in contraddizione con quella precedente.
Da un'analisi più attenta si capisce che non lo è, giacché la regola precedente asserisce di sele-
zionare il tipo più generico tra quelli possibili.
return valResult;
if (errors == null) {
errors = new ArrayList<String>();
errors, add(error);
return errors;
I
it ( ! validateUserData(userl, errors)) I
lor (int i = 0; i < errors.size(); i++) I
System.ouf.println(i+") error"+errors.get(i));
I
I else I
System. ouf.printlnfValidated!");
intlength = o1 .length;
result = (o2.length == length);
int i=0;
while ( (result) && (i clength) ) [
result = ( (o1 == nuli) ? (o2 == nuli) : ( o[i].equals(o2[i]) );
i++;
I
return result;
aGender = Character.toUpperCase(aGender);
If (aGender != 'M') && (aGender != 'F') (
thorw new NlegalArgumentExceptionfGender = "'+aGender+ );
)
gener = aGender;
)
Il (accessibleContext != nuli) (
// forces a repaint
accessibleContext.flrePropertyChange(
AccessibleContext.ACCESSIBLE_VALUE_PROPERTY,
new Integer(oldValue),
new lnteger(brm.getValue())
);
)
I
Un altro interessante esempio è fornito dalla classe astratta Buffer del package NIO, con 1'
implementazione del metodo limit della classe java.nio.Buffer.
" Sets this buffer's limit. If the position is larger lhan the new limit
* then It Is set to the new limit. If the mark is defined and larger than
' the new limit then It Is discarded. </p>
limit = newLimit;
if (position > limit)
position = limit;
if (mark > limit)
mark = -1;
return this;
setCollection()
Esempi:
setObservers(Observer[] observers)
setLineOrders(LineOrder[] lineOrders)
Questo metodo esegue due attività molto importanti: azzera la collezione e vi imposta i
valori specificati. Questi dovrebbero essere specificati utilizzando il tipo di dati più semplice
possibile: array o apposita interfaccia della collezione usata.
getCollectionf)
Esempi:
getObserversQ
getLineOrders()
Questo metodo restituisce la specifica collezione. In modo equivalente a quanto detto per il
corrispondente metodo set, anche in questo caso è opportuno riportare il tipo di dati più sem-
plice possibile. Ottimi canditati sono Iterator, un apposito array, l'interfaccia Collection, e così via.
addCollectionElement()
Esempio:
getObserverElement(Observer aObserver)
getLineOrderElement(LineOrde aLineOrder)
oppure:
getObserverElement(String observerld)
getLineOrderElement(String lineOrderld)
Questo metodo ha l'obiettivo di restituire uno specifico elemento presente nella collezione.
Si sarebbero potute utilizzare forme più contratte, tipo getLineOrder, ma queste si prestano a
generare confusione con il metodo get della collezione.
removeColleclionElementO
Esempio:
removeObserverElement(Observer aObserver)
removeLineOrderElementfLineOrde aLineOrder)
oppure:
removeObserverElement(String observerld)
removeLineOrderElement(String lineOrderld)
Questo metodo ha l'obiettivo di rimuovere uno specifico elemento dalla collezione. Anche
in questo caso si sarebbero potute utilizzare forme più contratte, tipo removeLineOrder, ma si è
preferita la forma più lunga onde evitare confusione.
setCollection()
Esempi:
clearObservers()
clearLineOrders()
Il metodo toString è molto utile per una serie di motivi, come eseguire il debug delle applica-
zioni, specie di applicazioni multi-threading, scrivere su opportuni file di log lo stato dei vari
oggetti, etc.
Tipicamente, l'esecuzione di questo metodo non è vincolata da forti requisiti di performan-
ce, però in alcuni contesti (per esempio applicazioni multi-threading che sfruttano il metodo
per effettuare il log dell'applicazione) potrebbe diventarlo. In questi casi è opportuno ricorrere
alla classe java.util.StringBuffer o lang.StringBuilder.
L'implementazione di questo metodo è fondamentale per tutte quelle classi disegnate quasi
esclusivamente per incapsulare dati, come per esempio Value Object e Data Transfer Object. Questo
perché il metodo è fortemente utilizzato nelle collezioni standard. Per esempio in
Collection.contains, Map.containsKey, Vector.indexOf, e t c . etc.
Secondo le direttive Java, se una classe ridefinisce il metodo equals, allora deve ridefinire
anche il metodo hashCode. Vediamo un esempio di implementazione del metodo equals nella
classe Array.
if ( a == nuli || a2 == nuli)
return false;
return true;
Quanto detto per il metodo equals in merito al corretto utilizzo delle collezioni Java è valido
per il metodo hashCode. Per esempio gli oggetti inseriti in un hashtable devono necessariamente
ridefinire il metodo hashCode perché questo è utilizzato dai vari metodi della collezione, come
per esempio get. Pertanto, qualora questo metodo non fosse definito per una determinata clas-
se, questa potrebbe creare problemi se utilizzata come chiave in collezioni come Hashtable e
HashMap.
Per capire appieno l'importanza del metodo hashCode è necessario ricordare che il valore
hash di un oggetto, utilizzato come chiave in una collezione Hashtable o HashMap, serve per
determinarne la posizione dei relativi elementi all'interno di tali collezioni. Poiché però esiste
anche il problema delle collisioni (elementi diversi possono generare uno stesso valore hash),
ne segue che il valore hash dell'elemento chiave è utilizzato per accedere alla posizione delle
liste di collisione (liste in cui sono memorizzati elementi diversi le cui chiavi danno luogo allo
stesso valore hash). In effetti, una collezione hash non è altro che un array di liste di collisione.
Una volta determinata la lista di collisione di un elemento, l'elemento stesso è individuato
utilizzando il metodo equals. Ma vediamo l'implementazione del metodo get della classe
java.util.HashMap.
Object k = maskNull(key);
ini hash = hash(k);
while (true) I
il (e == null)
return null;
il (e.hash == hash & & eq(k, e.key))
return e.value;
e = e.next;
I
h += ~(h « 9);
h ( h » > 14);
h += (h « 4);
h A = (h » > 10);
return h;
Come si può notare il valore hash della chiave è utilizzato per accedere alla lista dei conflitti (i =
indexFor(hash, table.length)) che viene scorsa finché o la lista termina e quindi l'elemento non è
presente, oppure finché l'elemento considerato e quello passato come parametro hanno lo stesso
valore di hash della chiave e questa è esattamente quella cercata ((e.hash == hash && eq(k, e.key))).
Come si può notare, la classe java.util.HashMap implementa un metodo hash (hash(Object x)) da
utilizzarsi per irrobustire il valore di hash restituito dagli elementi forniti. Si tratta di una tecnica
atta a sopperire a problemi generati da eventuali metodi hashCode non particolarmente brillanti.
• consistenza; l'invocazione del metodo, per un dato oggetto, deve ritornare lo stesso valore
intero a meno che l'oggetto stesso non sia modificato (in particolare non siano modificati gli
attributi utilizzati per la generazione del valore). Chiaramente, se così non fosse, sorgerebbero
dei problemi per l'individuazione degli elementi memorizzati in strutture hash;
• uguaglianza; se x.equals(y) = true <=> X.hashCodeO = y.hashCodef). Questo implica, tra l'altro,
che gli attributi utilizzati nel m e t o d o equals devono essere utilizzati anche
nell'implementazione del metodo hashCode.
Da notare che non è richiesto che x.equals(y) = false <=> x.hashCode() != y.hashCode(). Questa
proprietà sarebbe molto utile da ottenere; purtroppo il calcolo delle probabilità insegna che
funzioni hash in grado di non generare conflitti, il cui dominio è il tipo int, sono semplicemente
non fattibili!
v.modCount = 0;
r a t u m v;
) catch (CloneNotSupportedException e) I
// this shouldn't happen, since we are Cloneable
throw new lnternalError();
I
I
InputStream in = nuli;
OutputStream out = null;
OutputStream err = null;
try {
in = new FilelnputStream(source);
out = new FileOutputStream(dest);
err = new FileOutputStream(errStr);
// do some work
I finally {
If (in 1= null) {
try (
in.close();
} catch (lOException ex) I
// log this exception
I
1
If (out != null) I
try!
in.close();
) catch (lOException ex) {
// log this exception
I
I
II (err != null) I
try {
in.close();
I catch (lOException ex) {
// log this exception
}
Ed ecco un esempio di chiusura di stream con con la nuova versione Java 5.
try I
in = n e w FilelnputStream(source);
out = new FileOutputStream(dest);
err = new FileOutputStream(errStr);
// do s o m e w o r k
) finally I
closeAndLogException(in);
closeAndLogException(out);
closeAnd LogException(err) ;
Data l'imprevedibilità intrinseca del metodo, questo non è idoneo per implementare proce-
dure di "pulizia" delle risorse allocate da un oggetto: queste dovrebbero essere rilasciate espli-
citamente dopo l'utilizzo o, eventualmente, riconsegnate ad un apposito pool. L'utilizzo del
metodo finalize, in questo senso, potrebbe essere utilizzato solo come precauzione qualora si
implementi un framework estendibile. La procedura di rilascio delle risorse deve essere affida-
ta a un apposito metodo: dose, dispose, etc. che il client dell'oggetto deve chiamare esplicita-
mente per assicurare il corretto rilascio delle risorse.
System.ouf.printlnfResult : "trealNum);
La domanda da porsi è se ciò rappresenti un vero problema. La ovvia risposta è: dipende dal
tipo di applicazione. Per esempio, qualora il programma debba calcolare la distanza tra due
città, verosimilmente la probabilità di commettere un errore dell'ordine di qualche centimetro
o anche decimetro, raramente risulterebbe problematica. Si consideri invece il caso di applica-
zioni finanziarie in cui determinati valori siano utilizzati in calcoli abbastanza complessi da
ripetersi per milioni di transazioni giornaliere. In questi contesti, l'errore introdotto dalla per-
dita di precisione tenderebbe ad amplificarsi fino a poter generare problemi.
Nei casi in cui sia molto importante gestire cifre a elevata precisione, non resta altro da fare
che utilizzare classi come java.math.BigDecimal. Questi oggetti, come suggerisce il nome, utilizza-
no direttamente rappresentazioni decimali (array di cifre decimali), e pertanto offrono una
serie di vantaggi, come la possibilità di rappresentare numeri di lunghezza variabile (virtual-
mente infinita), l'esatta rappresentazione (tutta la precisione richiesta), etc. Gli inevitabili svan-
taggi sono "grammatica" più complessa e perdita di prestazioni (ciò principalmente perché
richiedono maggiore occupazione di memoria; le operazioni non possono avvenire tra registri
della CPU e questi oggetti sono immutabili). Vediamo un frammento di codice equivalente a
quello precedente, in cui il tipo float è sostituito da BigDecimal.
System.ouf.printlnfResult : "+reall\lum);
Per quanto concerne la complessità della notazione, tutto sarebbe più facile se in Java venisse
introdotta la possibilità di eseguire 1 'overloading degli operatori, come in C++.
Tabella 2.2- Alcune particolari espressioni relative ai numeri speciali in virgola mobile. Secondo le
specifiche standard, NaN non è uguale a nessun altro numero in virgola mobile, incluso sé stesso.
la comparazione tra due variabili impostate a NaN produce un valore false, mentre la stessa
comparazione (metodo equals) tra due oggetti Float produce un risultato true. Ancora, mentre 0
e -0 sono considerati uguali in termini di tipi base, lo stesso non vale per gli oggetti. In questo
caso è necessario eseguire le comparazioni utilizzando il metodo compareTo.
La presenza di questi elementi impone diverse cautele durante l'implementazione di metodi
che manipolano tipi reali. Per esempio:
• Se (realel < reale2) allora !(reale1 >= reale2) è vero solo quando entrambi i numeri non
hanno valore NaN
• reale == reale è vero (true) solo quando reale non ha un valore NaN
• realel + reale2 - reale2 = realel solo quando reale2 non è né un infinity né assume il valore NaN
System.ouf.printlnfResult : "+realNum1);
System.ouf.prinllnfResult : "+realNum2);
System. ouf.println(realNum1.equals(realNum2));
System. ouf.pnntln(realNum1.compareTo(reall\lum2));
2.9.7 Ricordarsi che i BigDecimal sono oggetti immutabili
Gli oggetti BigDecimal, come gli oggetti String, sono immutabili. Ciò significa che una volta im-
postato il relativo valore, questo non è più modificabile. Fin qui tutto bene se non fosse per
la presenza di alcuni metodi particolarmente ingannevoli, come per esempio add. In particola-
re, un'istruzione del tipo subTotal.add(taxAmount) lascerebbe presupporre che il valore dell'istan-
za taxAmount venga aggiunta a quella dell'oggetto subTotal aggiornandolo. Questo però non è il
caso, visto che la somma avviene correttamente, ma il risultato è restituito incapsulato da un
nuovo oggetto. Pertanto, il frammento di codice corretto è:
Per quanto tutto possa sembrare corretto, la precedente dichiarazione genera un overflow: il
valore impostato è infatti -194313216. Questo perché le varie moltiplicazioni sono eseguite su
tipi interi e solo al termine il risultato è impostato in una variabile di tipo long. Per evitare ogni
problema, è sufficiente indicare esplicitamente che la prima cifra (24) di tipi long come segue:
short count = 1;
int ine = 100000;
2.10 Selezionare attentamente le collezioni
Nell'implementazione di classi che manipolano liste di dati è particolarmente importante selezionare
correttamente la classe Java delegata a memorizzare e gestire tali collezioni.
Questa selezione, anche per motivi "storici", non è sempre agevole. Ciò essenzialmente per il fatto
che le versioni iniziali del linguaggio furono dotate di collezioni thread-safe, come Vector e Hashtable,
che per questioni di retrocompatibilità, sono state mantenute nelle versioni più recenti. Queste classi,
sebbene permettano di realizzare più agevolmente programmi multi-threading (la quasi totalità dei
metodi sono sincronizzati), fanno pagare, soprattutto in termini di efficienza, la gestione di dell'accesso
concorrente ( t h r e a d - s a f e t y ) anche nei casi in cui ciò non sia richiesto. Si pensi per esempio
all'implementazione degli E J B (Enterprise JavaBeans), in questo caso il multi-threading è gestito
dall'application server e quindi ogni esecuzione è eseguita da un opportuno thread. Quindi, ulteriori
non necessarie sincronizzazioni finiscono per ridurre le performance.
Con la versione Java 2 questo problema è stato risolto introducendo un nuovo insieme di classi
collezione, che diventano thread-safe solo su richiesta.
veloce) - lenta )
Vector,
List Sì ArrayList LinkedList x
Stack
LinkedHashMap TreeMap
N o chiavi Hashtable,
Map HashMap (ordinala - (ordinata
Sì o o c i t i Properties
veloce) - lenta)
o, meglio ancora, la versione non sincronizzata introdotta con il JDK 1.5: java.lang.StringBuilder. Il
problema della classe String è dovuto al fatto che è immutabile. Pertanto la concatenazione di più
stringhe è ottenuta attraverso la creazione di una serie di oggetti stringa intermedi.
Sebbene i compilatori moderni siano in grado di effettuare una serie di ottimizzazioni (so-
prattutto quando si tenta di costruire una stringa con un solo comando), evitando la generazio-
ne di una serie di oggetti intermedi, è sempre opportuno verificare l'utilizzo delle classi StringButfer
o StringBuilder qualora la costruzione richieda diversi concatenamenti eseguiti in tempi successi-
vi. Ecco il m etodo toString della classe java.util.Array.
buf.appendf]");
return buf.toString();
)
A prima analisi si sarebbe tenuti a pensare che questo produca in output un qualcosa del
genere: Date ->31-Dec-1999 00:00:00, mentre così non è. In particolare l'anno stampato è "magi-
camente" il 2000. Questo perché:
• i mesi vengono rappresentati a partire da 0, che significa Gennaio; pertanto Dicembre è
rappresentato dal numero 11 anziché 12;
• per qualche strana decisione, un valore fuori scala (come per esempio 12 del listato),
invece di generare un'eccezione, sposta il calendario in avanti!
Date dt = nuli;
tryl
dt = (new SimpleDateFormat(fmt)).parse(testDate);
] catch (ParseException e) {
"yy" "08"
y Year (anno) Numero "yyyy"
"2008"
"M" "7"
"MM" "07"
M Month (mese) Testo/Numero
"MMM" "Jul"
"MMMM" "July"
Day in month "d" "3"
d Numero
(giorno del mese) "dd" "03"
"h" "5"
h Hour (ora) 1 - 1 2 . A M / P M Numero
"hh" "05"
"H" "17"
H Hour (ora) 0 - 2 3 Numero
"HH" "17"
"k" "5"
k Hour (ora) 0 - 1 1 . A M / P M Numero
"kk" "05"
"K" "17"
K Hour (ora) 1-24 Numero
"KK" "17"
"m" "7"
m Minute (minuti) Numero
"mm" "07"
"s" "3"
s Second (secondi) Numero
"ss" "03"
Millisecond
s Numero "SSS" "003"
(millisecondi) 0-999
Day in week "EEE" "Mon"
E Testo
(Giorno della settimana) "EEEE" "Monday"
Day in year "D" "42"
D Number
(Giorno dell'anno) "DDD" "042"
Day of week in month
F (settimana del mese a cui Number "1"
il giorno appartiene) ( 1 - 5 )
Week in year
w Number "w" "7"
(Settimana dell'anno) 1 - 5 3
Week in month 1 - 5
W Number "W" "3"
(Settimana del mese)
"a" "AM"
a AM/PM Testo
"aa" "AM"
"z" "EST"
z Time zone (fuso orario) Testo "zzz" "EST"
"zzzz" "Eastern Standard time"
Escape for text Delimitatore "'hour' h " "hour 9 "
" Single quote Testo "ss"SSS" "42'576"
System. ou/.println(dt.toString());
Come si può notare, il codice tenta di impostare come data il 31 Febbraio del 2008. Come
risposta ci si attenderebbe una sonora eccezione... Invece, ahimè, questo non è il caso e infatti,
il risultato è il seguente output: Sun Mar 02 00:00:00 GMT 2008. Per avere il comportamento desi-
derato è necessario inibire il comportamento "silente" di default. Ciò si ottiene inserendo le
seguenti istruzioni al listato precedente: SimpleDateFormat sdì = new SimpleDateFormat(fmt);
sdf.setLenient(false); ossia si deve esplicitare la variabile di tipo SimpleDataFromat, e quindi impor-
re a false il comportamento silente.
Il secondo problema è relativo al fatto che la classe SimpleDateFormat non è thread-safe. Quin-
di una mancata considerazione di questa decisione di disegno può generare seri problemi. Per
esempio, non è infrequente visionare frammenti di codice, soprattutto in ambienti real-time, in
cui si tenti di evitare la ripetuta inizializzazione di oggetti di questa classe al fine di aumentare le
performance, magari ricorrendo a una dichiarazione static. Sebbene l'idea sia corretta soprat-
tutto considerando che tale inizializzazione non è sempre velocissima, è necessario implemen-
tare soluzioni più sofisticate per evitare problemi di raise condition. Un buon esempio consiste
nel memorizzare l'oggetto di tipo SimpleDateFormat in un'apposita istanza ThreadLocal.
cal.set(2008,1, 23);
Inoltre, qualora si esegua Xhiding di un attributo diminuendone il livello di accesso (come nel
listato che segue poco sotto), si finisce per violare l'importantissimo principio di sostituzione di
Liskov (se S è una classe che eredita dalla classe T, ne è un sottotipo, allora istanze di tipo T
possono essere sostituite con oggetti di tipo 5, senza alterare il corretto funzionamento del codice.
Quindi le classi ereditanti devono, almeno, mantenere le proprietà della classe da cui eredi-
tano). Si consideri il seguente listato relativo alle tre classi TestA, TestB e TestC, con un esempio di
hiding di un attributo.
Eseguendo il metodo main del precedente listato si ottiene la stampa ripetuta della stringa
"Parent". Mentre qualora la classe TestC cercasse di accedere all'attributo classLevel della classe
TestB (basterebbe variare la dichiarazione di testClassB come segue: TestB testClassB = new TestB() )
si otterrebbe un errore di compilazione.
Come si nota hiding è ben diverso dal meccanismo òétYoverridde, ottenibile introducendo
il metodo getClassLevel, prassi decisamente consigliata. In tal caso le classi ereditanti sarebbero
state vincolate dal dover dichiarare lo stesso tipo di ritorno e un compatibile livello di accesso.
In particolare, il modificatore di accesso di un metodo overridden deve garantire almeno lo
stesso livello di accesso del corrispondente metodo nella classe genitore. Inoltre, una volta che
un metodo subisce Yoverridde, questo non può essere eseguito nella classe ereditante, a meno
di esplicita invocazione per mezzo della parola chiave parent, ed inoltre diviene inaccessibile a
classi discendenti oltre la classe figlia.
Si ricordi quindi di evitare sempre l'hiding degli attributi, e in caso sia necessaria una situa-
zione come quella descritta dal listato, utilizzare la tecnica dell'overridde attraverso l'introdu-
zione di appositi metodi.
I
I
System.ouf.println(comment);
System.out.println(BASIC_VALUE);
Come si può notare è stato possibile nascondere e quindi ridefinire il valore della "costante"
BASIC_VALUE. Per evitare problemi di questo tipo è conveniente affidarsi ad opportuni metodi
e v e n t u a l m e n t e p r o t e t t i dal m o d i f i c a t o r e final (public static final String getBasicValue()). In q u e s t o
caso è veramente impossibile cambiare il comportamento dello stesso.
Capitolo 3
Approfondimenti
Introduzione
In questo capitolo si trattano in dettaglio sia alcune specifiche features introdotte con la versio-
ne Java 5, come per esempio i Generics, le nuove classi della concorrenza, l'autobox, e lo static
import, sia argomenti tradizionalmente molto complessi come la programmazione multi-
threading (MT), per approfondire la quale si rimanda anche all'Appendice D.
Java 5 è stata senza ombra di dubbio una versione fortemente innovativa. In particolare,
sono stati introdotti una serie di nuovi meccanismi, alcuni dei quali a lungo richiesti dalla co-
munità dei programmatori: per esempio i Generics, che hanno finito per cambiare drasticamente
la programmazione in Java.
Vedremo in dettaglio una di direttive e di trucchi molto utili per il lavoro quotidiano del
programmatore. Per quanto concerne la programmazione MT sono illustrati una serie di con-
cetti che anche programmatori esperti tendono a dimenticare, come per esempio il fatto che le
specifiche Java non prescrivano alcun modello MT di riferimento.
Obiettivi
Dalla lettura di questo capitolo, i programmatori dovrebbero approfondire la conoscenza delle
nuove feature introdotte con la versione Java5, inclusi alcuni aspetti spesso trascurati dalle
trattazioni ufficiali, e diverse strategie molto utili nella pratica quotidiana.
Per quanto riguarda la programmazione multi-threading, è sempre opportuno averne una
buona conoscenza, non solo perché prima o poi tutti si trovano a dover scrivere almeno piccole
utility MT, ma anche perché essere a conoscenza di quanto accade dietro le quinte aiuta a
implementare codici di migliore qualità.
Direttive
3.1 I Generics
I Generics rappresentano indubbiamente una delle caratteristiche più importanti introdotte con
il JDK5. A lungo richiesti dalla comunità degli sviluppatori, sono la versione Java del concetto dei
templates presenti in C++ con profondissime differenze. Queste sono principalmente dovute al
fatto che i Generics sono "risolti" completamente dal compilatore che si occupa di generare un
bytecode assolutamente compatibile con le precedenti versioni di Java. Questo meccanismo è
chiamato cancellazione (erasure). In questo contesto, questa tecnica è utilizzata per "ridurre" i
tipi parametrizzati nei corrispondenti raw type (tipi grezzi). Questa scelta risolve alcuni impor-
tanti problemi, come la compatibilità con le versioni precedenti (la policy di back-compatibility
ha da sempre ha contraddistinto la Sun), ma ne introduce di nuovi dovuti essenzialmente al fatto
che a run-time vengono perse diverse informazioni relative al tipo dei Generics. Al fine di apprez-
zare il meccanismo della cancellazione, si consideri il seguente frammento di codice:
• eliminazione di molti down-cast richiesti dalle iniziali collezioni Java. Il codice è maggior-
mente type-safe, spostando il controllo del tipo dall'esecuzione (runtime) alla compilazione;
• codice più elegante e leggibile. Spariscono molti casting, è sempre possibile capire che
tipo di dati gestisce una particolare collezione, si genera codice più succinto, e così via
Inoltre, le collezioni base Java 2 (presenti prima dell'arrivo dei Generics) sono tollerate solo
per compatibilità con le passate versioni di Java, e, stando a quanto sancito dal documento
delle specifiche ufficiali, potrebbero essere completamente rimosse in future versioni! Infine,
l'utilizzo dei Generics non genera alcun impatto sulle performance. Questo perché iJ compila-
tore fa in modo che i Generics a run-time siano rappresentati dai rispettivi tipi grezzi. Quindi
non ci sono variazioni di sorta sulle performance, cosa che invece avviene in C++, dove i template,
tipicamente, permettono di ottenere prestazioni migliori. Per comprendere alcuni vantaggi si
considerino i due frammenti di codice presentati di seguito. A sinistra, una versione pre-Generics;
a destra la versione che fa uso dei Generics.
System.out.prlntln("->"+llst);
public static void main(String args[]) {
ArrayList<Number> al = new ArrayList<l\lumber>();
al.add(10);
Test.test(al);
I
Ciò dimostra che un array di tipi più specifici possa sempre essere sostituito a un array di tipi
più generali. In effetti, gli array a run-time continuano a mantenere informazioni relative al
proprio tipo. Ciò permette alla JVM di poter eseguire i controlli relativi ai tipi di dati gestiti.
Mentre il tentativo di invocare il seguente metodo:
! i some initialisation
findMinimum(intNumbers)
Quindi sebbene sia possibile inserire un'arancia in un cestino della frutta, non è possibile
utilizzare un cestino di arance come un cesto della frutta.
Da notare che per via del fatto che gli array implementano la proprietà della covarianza, non
è possibile creare array parametrizzati il cui tipo sia un tipo concreto. Per esempio la seguente
dichiarazione non è valida:
• tipi enumerati. Questi, semplificando, rappresentano una lista di valori statici come tali
avrebbe poco senso cercare di parametrizzarne il tipo;
• eccezioni. Le eccezioni rappresentano il meccanismo utilizzato a run-time dalla JVM
per segnalare e gestire situazioni di errore. Poiché la stessa JVM non ha conoscenza del
meccanismo dei Generics (vengono cancellati attraverso il meccanismo dell'erasure),
questa non sarebbe capace di distinguere il tipo Generics in un costrutto catch (si consi-
deri l'errato listato poco sotto). Pertanto dichiarazioni come le seguenti non sono con-
sentite: public class MyException<T> extends Exception. Tuttavia le eccezioni possono essere
il tipo di una collezione;
• classi anonime annidate. Sebbene queste classi possano utilizzare al loro interno i Generics,
le stesse non posso essere tipizzate: avrebbe decisamente poco senso per via del sempli-
ce fatto che non dispongono di un nome.
void NlegalExceptionTypeO I
lry(
executeAMethod();
/ / . . . do something
/ / . . . do something
I
)
Il fatto che il tipo generico MyClass<T> possa essere ¡stanziato con un numero infinito di
parametri concreti, potrebbe portare all'errata conclusione che ciascuna delle istanze caratte-
rizzate dal medesimo parametro concreto (per esempio tutti gli oggetti di tipo MyClass<String>)
disponga del proprio attributo statico. Anche se ciò ha una sua logica che funzionerebbe in altri
linguaggi di programmazione, questo non è il caso dei Generics Java. Per capire il perché, è
sufficiente ricordare il principio della cancellazione adottato dal compilatore e il fatto che i tipi
generici sono ridotti alle versioni base (raw type). Come controprova basti considerare l'output
generato dall'esecuzione del codice riportato nel listato visto poco sopra:
Anche per questo motivo, istruzioni del tipo myClass1.numCalls++ dovrebbero essere sostitui-
te con la versione classica MyClass.numCalls++.
Per gli stessi motivi, dovrebbe essere chiaro il motivo per cui non è consentito dichiarare un
attributo statico del tipo del parametro della classe, come riportato nel listato seguente.
ICONA
// do something
I
I
Da tener presente che, sebbene la dichiarazione statica non sia consentita, quella final invece
non crea problemi.
Il carattere jolly è molto utile in tutti quei contesti in cui si abbia una conoscenza limitata del tipo
di argomento. Si consideri il seguente listato: metodo fill (java.util.Collections) utilizzato per impostare
tutti gli elementi della lista specificata con l'oggetto fornito. Come si può notare, 0 tipo della colle-
zione deve essere un supertipo dell'oggetto che si desidera impostare. Una dichiarazione più gene-
rale basata sul carattere jolly non limitato (public Static <T> void fill(List<?> list, T obj)) avrebbe portato
a un codice non type-safe, carente di importanti vincoli, e quindi, in ultima analisi, fragile.
/' '
" Replaces all of the elements of the specified list w i t h the specified
' element. <p>
Come dimostrato dal listato poco sopra, il carattere jolly "limitato" contiene un maggiore
quantitativo informativo, forzando opportuni controlli sul tipo. Pertanto, ogniqualvolta si in-
tenda utilizzare un carattere jolly senza limiti (unbounded), ci si dovrebbe interrogare se si tratti
della scelta più corretta o se sia possibile introdurre appositi limiti.
3.1.9 Evitare metodi che ritornino parametri di tipo jolly.
L'implementazione di metodi che restituiscono parametri di tipo jolly (?) non è in genere una
buona idea. Questo essenzialmente perché l'accesso al valore restituito è ristretto e le restrizio-
ni dipendono dal contesto di utilizzo del tipo jolly. Pertanto, nella maggior parte dei casi, nel
codice della classe cliente è necessario eseguire il tanto odiato down-cast.
Si consideri l'esempio del listato che segue. Come si può notare, una volta che il tipo di
ritorno è del tipo List<?>, si sono perse le informazioni sul tipo gestito dalla lista e quindi per
poter accedere ai metodi del tipo iniziale è necessario effettuare un down-cast. La versione
corretta del codice richiede di sostituire il carattere jolly con un tipo, ottenendo la seguente
firma: public static List<T> alterList(List<T> aList ).
return aList;
I
names.addfLuca");
names. addf'Vera");
names. addf'Francesco");
names. addf'Natalya");
Chiaramente esistono dei casi sporadici in cui quest'opzione è l'unica possibile; si consideri
per esempio il codice della classe: java.lang.Class. In questo caso diversi metodi prevedono il
punto interrogativo nel tipo di ritorno. Per esempio il metodo forName deve essere in grado di
ritornare l'oggetto Classe del tipo richiesto:
Bisognerebbe sempre cercare di evitare il carattere jolly nel parametro di ritorno di un metodo.
Il listato che segue illustra la tecnica della cattura del jolly. Dall'analisi del codice verrebbe
da chiedersi legittimante perché non utilizzare direttamente la firma del metodo alter. Chiara-
mente ciò è sempre consigliato, quando possibile. Tuttavia il codice mostrato è un esempio e
quindi è stato estrapolato da situazioni più complesse ove la sostituzione dei metodi non è
così immediata.
Chiaramente, non esiste la definizione migliore, entrambe hanno un loro dominio di utiliz-
zo. Tuttavia è necessario capirne le differenze per poter riuscire a selezionare correttamente
l'implementazione più idonea per i propri requisiti.
if (t.size() != size())
return talse;
try {
lterator<Entry<K, V » i = entrySet().iterator();
while (i.hasNextQ) I
Entry<K, V> e = i.next();
K key = e.getKeyf);
V value = e.getValue();
if (value == null) {
if (!(t.get(key) == null && t.containsKey(key)))
return false;
I else I
if (lvalue.equals(t.get(key)))
return false;
1
return true;
I
Da notare che, sebbene il codice sia stato preso direttamente dalla Java library, con poco
sforzo sarebbe stato possibile renderlo più leggibile. Inoltre, a prima vista il seguente down-
cast M a p < K , V> t = (Map<K, V>) o potrebbe sembrare un po' azzardato. Tuttavia non lo è, perché
a tempo di esecuzione non c'è differenza tra Map e Map<K, V>.
I catch (CloneNotSupportedException e) I
// this shouldn't happen, since we are Cloneable
throw new lnternalError();
I
return v;
Integer ¡1 = 1 ;
long 11 = 10L;
int i2 = 5;
myTest.printValue(il);
myTest.printValue(M);
myTest.printValue(i2);
lnteger:1
long:10
long:5
Ora, mentre per le prime due invocazioni non ci sono sorprese (il comportamento è esattamen-
te quello atteso), la stessa cosa non si può dire per la terza invocazione. In effetti, verrebbe spon-
taneo attendersi il boxing automatico della variabile i2. Ciò non avviene per questioni di compa-
tibilità con la precedente versione Java, in cui la variabile intera verrebbe "promossa" a long.
long startTime = 0;
// ArrayList declaration
startTime = System. nanoTimeQ;
List<lnteger> listValues = new ArrayList<lnteger>();
printPeriod(" HrrayL\s\ declaration: \t", startTime);
// Array declaration
startTime = System. nanoTimeQ]
int arrValues[] = n e w i n t [ 5 0 0 0 0 0 ] ;
printPeriod(" hmy declaration: \t", startTime);
// ArrayList initialisation
startTime = System. nanoTimeQ-,
for(int i=0; ¡<500000; i++)|
listValues. add(i);
I
/7r/niPer/orf("Initialisation ArrayList:", startTime);
// ArrayList initialisation
startTime = System. nanoTimeQ:
for(int 1=0; i<500000;i++){
arrValues[l] = i;
I
printPeriod("Initialisation ArrayList:", startTime);
Integer i3 = 20;
Integer i4 = 20;
System.ouf.println("2. Equals : " + (¡3 == ¡4));
Integer ¡5 = 2000;
Integer i6 = 2000;
System.oi/f.prlntln("3. Equals : " + (¡5 == ¡6));
)
1. Equals : false
2. Equals : true
3. Equals : false
Sebbene ciò possa sembrare strano e destare qualche preoccupazione, questo comporta-
mento è dovuto al fatto che la JVM quando alloca la memoria per i tipi di wrapping, e in alcuni
casi speciali, tende a riusare gli stessi oggetti.
if ( vallnt.length = = 0 ) 1
throw new HlegalArgumentExceptionfNo paraemter provided");
I
return min;
)
Inoltre, l'utilizzo dei varargs può creare problemi con l'overloading. Si consideri per il esem-
pio il caso di metodi overloaded con parametri definiti attraverso varargs. In questo caso un'in-
vocazione con nessun argomento risulterebbe ambigua. Ambiguità che però è immediatamente
intercettata dal compilatore.
In entrambi i casi è necessario definire l'implementazione del metodo run(). Tuttavia, qualora
si intenda definire esclusivamente il metodo run(), la prima alternativa è da preferire. Questa
tecnica è utile anche considerate le limitazioni di Java relative all'ereditarietà singola, per cui se
una classe eredita da Thread, non può ereditare da altre classi. Da tener presente che, sebbene sia
sempre possibile simulare l'ereditarietà con un'apposita composizione, alcune volte questa stra-
tegia non è utilizzabile proprio poiché è obbligatorio ereditare da un'altra classe, come nel caso
di Applet. La principale differenza tra le due strategie è che nel primo caso si ha un solo oggetto
eseguito da più thread, e nel secondo ogni thread incapsula anche l'oggetto da eseguire.
/**
I
I
/ "
Sebbehe il codice presentato rappresenti una buona strategia per terminare l'esecuzione di
un thread, ahimè, non sempre è applicabile. In particolare, qualora il thread sia impegnato
all'interno del metodo run nell'eseguire operazioni bloccanti, la richiesta di conclusione per
mezzo del flag non forzerebbe il thread a terminare. In questi contesti, un'altra strategia molto
efficace consiste nell'utilizzare l'eccezione di interruzione (InterruptedException). Da tener pre-
sente che questa non blocca immediatamente il thread destinatario, ma si limita a consegnare il
messaggio di richiesta di interruzione, lasciando quindi il thread il modo di terminare corretta-
mente.
/**
try I
boolean endOfWork = false;
I
I catch (InterruptedException ie) {
// clean-up and thread-exit
I
/**
La maggior parte di OS utilizza scheduler la cui politica di assegnazione della CPU è basata
sulla selezione del thread in stato di attesa a più alta priorità. Il problema è che diversi OS pre-
vedono differenti livelli di priorità. Qualora questi siano superiori o uguali ai dieci livelli previsti
(per esempio, Solaris assegna all'attributo di priorità 31 bit, quindi 2 " = 2 Gigabytes) si tratta di
risolvere un semplice esercizio di mapping, mentre quando il numero è inferiore (Windows NT
dispone di appena sette livelli di priorità), la situazione diviene più problematica (bisogna asso-
ciare più priorità concettuali a una stessa priorità fisica). A complicare le cose poi intervengono
alcuni servizi particolari di specifici OS (per esempio Windows NT) che operano sulla priorità
dei thread, aumentandola o riducendola, in funzione dell'esecuzione di prestabilite operazioni.
Questa tecnica è nota con il nome di priority boosting e tipicamente può essere disabilitata attra-
verso opportune chiamate native a codice, quindi non incluse nel linguaggio Java, tipicamente,
effettuate attraverso il linguaggio C. Pertanto, è sempre consigliabile evitare, per quanto possibi-
le, la gestione esplicita della priorità dei thread, e se è proprio necessaria, limitarla il più possibile.
Il codice seguente mostra un semplice esempio di deadlock causato da due thread che tenta-
no di acquisire due risorse in senso opposto.
name = aName;
resourcel = aResourcel;
resource2 = aResource2;
synchronized (resourcel ) {
try I
treadl ,start();
tread2.start();
Thread: T1 running...
Thread: T1 locked resource:Risorsa_1
Thread: T1 sleeping...
Thread: T2 running...
Thread: T2 locked resource:Risorsa_2
Thread: T2 sleeping...
Thread: T1 awaking...
Thread: T2 awaking...
Da tener presente che, qualora si abbia il sospetto di una situazione di deadlock o comunque
ogni qualvolta si voglia controllare lo stato dei thread e monitor di oggetti, è possibile richiede-
re il thread ed effettuare il monitor dump premendo CTRL + BREAK in Windows e CRTL + \ in
Solaris.
return instance;
Dall'analisi del codice è possibile notare che questo sia errato in quanto non tiene assoluta-
mente in considerazione la possibilità che diversi thread richiedano l'oggetto per la prima volta
contemporaneamente dando luogo ad un tipico esempio di race condition.
Per risolvere questo problema certi sviluppatori tentano di rendere il codice sicuro senza
appesantire ogni singola chiamata del metodo getlnstance con il sovraccarico della sincronizza-
zione. In effetti, questa sarebbe necessaria solo per la prima invocazione. Uno dei tentativi più
frequenti è dato dal double-check come mostrato nel listato poco sotto. Come si può notare si
esegue il primo test e quindi solo se l'oggetto sia effettivamente nullo, si entra nella zona protet-
ta. Sebbene le intenzioni siano lodevoli, l'implementazione non è sicura. Per comprenderne la
ragione è sufficiente considerare la seguente sequenza di azioni:
return instance,
I
L'unica implementazione veramente sicura è sfortunatamente quella che finisce per penaliz-
zare tutte le acquisizioni dell'istanza come riportato nel codice seguente.
return instance,
Da tener presente che in condizioni molto ben definite, quando per esempio si ha l'assoluta
certezza che un thread solo all'inizio esegua la parte critica (processo di inizializzazione), è
possibile evitare completamente la sincronizzazione. Però, si tratta di casi molto limitati che
comunque richiedono un'attentissima analisi.
Come si può notare dai passi 3,4 e 5, l'incremento unitario non è thread-safe. A seconda dei
propri requisiti, è possibile ottenere questa caratteristica sia schermando l'operazione da appo-
siti lock oppure utilizzare i tipi atomici, decisamente più performanti. Molto importante è an-
che considerare che l'utilizzo della parola chiave volatile non migliora le cose.
aggregator.addAndGet(element);
counter. incrementAndGet();
return result;
if (counter.get() > 0) I
result = aggregator.get() / counter.get();
I
return result;
I
synchronized ( ObjectLock ) I
/ / u p d a t e the o b j e c t
dove il primo costrutto protegge da accessi concorrenti il codice incluso nello stesso eseguendo
un lock sullo specifico oggetto, mentre nel secondo si protegge il codice definito dal metodo
bloccando l'intero oggetto a cui appartiene il metodo stesso.
In generale, il costrutto synchronized, permette di raggruppare le istruzioni incluse in un'uni-
ca esecuzione atomica. La logica conseguenza è che l'accesso da parte dei vari thread è serializzato:
solo un thread alla volta può entrare e quindi eseguire l'area protetta.
Per essere precisi, il costrutto synchronized influenza anche la visibilità. Questa è una caratteristi-
ca più complessa che interagisce con le politiche di gestione della memoria, le ottimizzazioni
eseguite dai compilatori, le cache gestite dai vari thread, etc. Ma in generale, la presenza del co-
strutto fa sì che gli aggiornamenti eseguiti da uno specifico thread prima di uscire dall'area sincro-
nizzata diventino visibili ad altri thread che si accingono ad entrare in tale porzione di codice.
Nonostante l'immediatezza, questo meccanismo soffre di una serie di limitazioni, le più im-
portanti delle quali sono l'impossibilità di interrompere un thread che si trova in stato di attesa
di acquisire un lock, impossibilità di eseguire un polling sul lock, impossibilità di tentare di
acquisire un lock senza dover attendere più di un determinato lasso temporale. Inoltre, anche dal
punto di vista delle prestazioni, la sincronizzazione non sempre risulta particolarmente efficien-
te. Per risolvere tutte queste limitazioni, Java 5 è stato dotato degli oggetti lock
(java.util.concurrent.locks.Lock), come per esempio ReentrantLock. Pertanto un blocco sincronizzato
viene trasformato come riportato di seguito in questo esempio di utilizzo di un lock rientrante.
lock.lock();
tryl
/ / update the object
I finally {
lock.unlockQ;
I
Diversi studi hanno inoltre dimostrato che i nuovi oggetti lock, oltre ad offrire tutta una serie
di nuove funzionalità avanzate, presentano prestazioni migliori e maggiori livelli di scalabilità.
Tuttavia, i lock hanno anche qualche svantaggio da tener presente: è necessario eseguire
esplicitamente il lock ed il conseguente unlock\ quando si utilizza la sincronizzazione la JVM ne
è al corrente, e quindi le relative informazioni, utilissime per individuare dead-lock, sono mo-
strate nei vari thread dump.
Executor è il mattoncino sul quale sono basate diverse classi del java.util.concurrent, che permet-
tono di risolvere le limitazioni sopraccitate.
Per esempio, l'interfaccia ExecutorService che estende Executor definisce i seguenti servizi:
public interface ExecutorService extends Executor I
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
public static File createTempFile(String prefix, String suffix, File directory) throws lOException
La perdita di prestazioni dovuta all'utilizzo della parola chiave Synchronized è facilmente comprensibile
considerando la politica con cui le J V M gestiscono la memoria in presenza di thread (le relative
specifiche fanno parte del J M M , Java Memory Model). In particolare, ogni thread è fornito con
un'apposita cache la cui politica di aggiornamento dei valori è fortemente influenzata dalla presenza
di aree sincronizzate. In assenza di queste, un thread è lasciato libero di evolvere accedendo alla
copia "locale" dei valori delle variabili memorizzate nella propria cache. Pertanto (sempre secondo
quanto stabilito dal J M M ) i thread sono autorizzati ad avere valori diversi relativi alla stessa variabile.
La situazione cambia notevolmente in presenza di aree sincronizzate. In questo caso le direttive
richiedono che un thread invalidi la propria cache, e quindi la aggiorni con i valori presenti nella
memoria "principale", non appena acquisito un lock (ingresso in un'area sincronizzata), e che riporti
tutte le modifiche effettuate nella memoria principale, appena prima di rilasciare il lock (uscita dall'area
sincronizzata). Pertanto è facile comprendere come ripetute richieste di sincronizzazione, da memoria
principale verso la cache del thread e viceversa, portino a una riduzione delle performance.
Come regola generale, è necessario sincronizzare, secondo la tecnica desiderata, i metodi che
modificano gli attributi delle classi le cui istanze sono condivise. Qualora, poi, sia necessario
che i thread acquisiscano sempre il valore più recente possibile, è necessario sincronizzare an-
che i metodi accessori. Si consideri una classe che contiene i dati, aggiornati in tempo reale, dei
prezzi di strumenti finanziari che, tipicamente, sono aggiornati diverse volte al secondo. In
questo caso è necessario che ogni thread acquisisca il valore aggiornato e, quindi, anche i meto-
di accessori devono essere opportunamente sincronizzati.
Non è infrequente il caso in cui lock a livello di istanza presentino un'eccessiva portata e che
quindi siano la causa di colli di bottiglia. In questi casi è necessario ricorrere a lock più granulari.
Si consideri l'esempio, di un oggetto che manipoli diverse strutture dati. In questo caso la
presenza di metodi sincronizzati potrebbe risultare non efficiente: l'acquisizione del lock da
parte di un thread per modificare una struttura dati blocca automaticamente l'accesso agli altri
thread, magari interessati a manipolare un'altra struttura dati, indipendente dalla precedente.
In questi casi, un'interessante alternativa consiste nel ricorre all'utilizzo di oggetti fittizi di cui
utilizzare il relativo monitor per l'accesso a specifiche sezioni critiche. Per esempio:
public void w r i t e ( ) (
// non criticai i m p l e m e n t a t i o n
synchronized ( lockWrite ) {
// write data in the shared objects
)
// do something else
1
)
I lock statici, infine, in maniera del tutto analoga a quanto riportato poco sopra, generano il
blocco di thread che desiderino eseguire metodi statici sincronizzati appartenenti alla stessa
classe, mentre non bloccano thread che desiderino eseguire metodi non sincronizzati o sincro-
nizzati al livello di istanza.
Come visto in precedenza, un'ottima tecnica consiste nell'utilizzare i nuovi oggetti Lock,
introdotti con Java 5.
Chiaramente le due alternative presentano ben definiti pregi e difetti e pertanto si configura-
no come soluzioni ideali per ben definiti scenari. La prima soluzione tende a ridurre la concor-
renza, evitando però problemi di performance dovuti alla copia di elementi e pertanto va utiliz-
zata qualora la concorrenza non sia elevata e sia relativa a strutture dati di dimensioni conside-
revoli. La seconda, ovviamente, offre una strategia opposta e pertanto è adatta qualora ci sia un
forte parallelismo su strutture dati di dimensioni limitate.
Da notare che le nuove collezioni della concorrenza non presentano questo problema. Infat-
ti, forniscono oggetti iterator che non lanciano eccezioni di modifica concorrente
(ConcurrentModificationException), questo perché questi oggetti piuttosto che implementare un
meccanismo di fallimento rapido, implementano quello di debole consistenza (weak consistency).
Ciò significa che 1' iterator accetta modifiche concorrenti mentre avviene la navigazione degli
elementi presenti quando costruito, però non garantisce (accade solo in determinate circostan-
ze) che questi siano incorporate nella propria lista iniziale e quindi presenti durante la scansione.
3.7.7 Non mischiare le politiche di lock
Ogni qualvolta si debba implementare del codice per ambienti MT è importantissimo disegna-
re attentamente la politica di locking prima di dar luogo a qualsiasi implementazione. In parti-
colare, un problema non infrequente è dovuto ad avventati mix di politiche di locking codifica-
ti in estremo per cercare di porre rimedio a soluzioni non studiate attentamente.
Un esempio classico si ha con l'utilizzo delle collezioni Java 2, come mostrato nel listato poco
sotto. Come da manuale, tali collezioni non sono nativamente thread-safe, ma lo possono diven-
tare attraverso l'utilizzo di apposite classi, come Collections. Ciò al fine di non appesantirne
l'utilizzo in contesti che non sono concorrenti. Tuttavia, ciò non risolve tutti i problemi. Nel
codice seguente, riportiamo una classe di utilità che consente di memorizzare una serie di errori
evitandone ripetizioni. Come si può notare, nonostante l'oggetto ArrayList sia reso sincronizza-
to, ciò non è sufficiente per la corretta implementazione del metodo addlfNotPresent. Un partico-
lare thread in esecuzione potrebbe essere interrotto tra il test e il conseguente inserimento,
compromettendo il requisito di non ripetizione degli errori. Una volta scoperta questa deficien-
za, alcuni programmatori potrebbero essere tentati di cercare di risolvere il problema sincro-
n i z z a n d o il m e t o d o a d d l f N o t P r e s e n t (public synchronized boolean a d d l f N o t P r e s e n t ( T n e w E r r o r ) ) . C i ò
sortirebbe ben poco effetto giacché si cercherebbe di sincronizzare l'oggetto sbagliato (si noti,
tra l'altro, la presenza del metodo di get). La soluzione al problema consiste nell'introdurre un
costrutto di sincronizzazione dell'oggetto errorList all'interno del metodo addlfNotPresent:
synchronized (errorList) ) . . . ( .
if (IwasPresent) I
errorList.add(newError);
I
return wasPresent;
I
Obiettivi
L'obiettivo di questo capitolo è presentare una serie di tecniche, linee guida e best practice
finalizzate al miglioramento del livello di documentazione del codice; pertanto la proprietà del
software su cui si focalizza l'attenzione è, ancora una volta, la leggibilità. Come largamente
discusso nel Capitolo 1, si tratta del prerequisito irrinunciabile per la manutenibilità del codice.
Gran parte degli argomenti trattati in questo capitolo prevedono come requisito una buona
conoscenza e padronanza dell'utility Java per la produzione automatica della documentazione:
JavaDoc. Pertanto nell'Appendice A è riportata una utile e concisa presentazione dell'utility
JavaDoc inclusi i relativi tag.
Direttive
4.1 Investire nella documentazione
Sebbene questa regola sia universalmente accettata e quindi possa sembrare inutile da include-
re in questo contesto, la realtà quotidiana ci insegna come non sia affatto raro imbattersi in
codici mal documentati o non documentati affatto. Codice difficilmente leggibile e comprensi-
bile diviene complesso da mantenere e ciò prelude alla necessità di riscriverlo (anche se magari
non sarebbe necessario).
• fornisce un primo strumento di verifica semi-formale del codice che si sta scrivendo. La
necessità di redigere in linguaggio pseudo-naturale la spiegazione dell'algoritmo ogget-
to di implementazione stimola, implicitamente, a valutarne sia la validità sia la correttez-
za della codifica; inoltre, qualora risulti difficile commentare porzioni di codice, potreb-
be essere il segnale di allarme di un algoritmo errato, contorto o non ben scritto;
• permette di illustrare efficacemente le decisioni prese nello stesso momento in cui ven-
gono prese;
• evita stressanti e frettolose attività di documentazioni nel breve periodo che precede il
rilascio del codice.
La scrittura a posteriori dei commenti è frequentemente un'attività tediosa, e pertanto, spes-
so assegnata a programmatori junior molte volte estranei alla progettazione iniziale. Ad aggra-
vare la situazione interviene il semplice fatto che, tipicamente, documentare un codice privo di
adeguati commenti è un compito complesso. La logica conseguenza è che, per rispettare i tem-
pi di consegna, si finisce per documentare il codice troppo rapidamente e superficialmente.
Ciò, oltre a generare una riduzione di gran parti dei vantaggi derivanti da una buona documen-
tazione, e quindi a ridurre la generale qualità del codice, può portare in casi limite a situazioni
fuorviami dovute alla presenza di commenti ingannevoli.
* Prints this throwable and its backtrace to the specified print stream.
*/
public void printStackTracefPrintStream s) I
synchronized (s) I
s.println(this);
n ..................................
// * Prints the cause, if present
4.1.5 Spiegare "che cosa" il codice esegue, "il perché" e non "il come"
L'illustrazione del "come" il codice risolva un compito, frequentemente, è una documentazio-
ne poco effettiva; infatti, le persone interessate a comprendere e a mantenere il codice sono, a
loro volta, sviluppatori. Anche qualora non conoscano specifiche istruzioni o librerie, esistono
molte fonti a cui attingere per la necessaria documentazione. Quello che invece è più difficile
da comprendere è "che cosa" il programma intenda eseguire e "il perché". Pertanto, questi
sono gli elementi sui quali è più opportuno investire il proprio tempo a disposizione.
Per esempio, si consideri il caso di un metodo la cui implementazione acceda sempre al
primo elemento di un array riportante i valori di offerta e acquisto di un determinato strumento
finanziario. Sebbene che cosa faccia un codice di questo tipo sia inequivocabile, potrebbe esse-
re meno chiaro il perché, che, sempre nel caso ipotetico, potrebbe dipendere dal fatto che la
prima posizione dell'array (indice = 0) sia riservata al prezzo più aggiornato.
" The < c o d e > S y s t e m < / c o d e > class c o n t a i n s several useful class fields
" and m e l h o d s . Il c a n n o l be instantlated.
"<P>
* A m o n g the tacilllies p r o v i d e d by the < c o d e > S y s t e m < / c o d e > class
' are s t a n d a r d Input, s t a n d a r d o u t p u t , a n d e r r a r o u t p u t s t r e a m s :
* access to externally d e t i n e d p r o p e r t l e s a n d e n v i r o n m e n t
" variables: a m e a n s ot l o a d i n g files and librarles: a n d a utility
" m e t h o d tor q u i c k l y c o p y l n g a p o r t l o n ot an array.
/**
/" *
' <P>
* First, if a security manager exists, its
* <code>SecurityManager.checkPermission</code> m e t h o d
* is called w i t h a <code>PropertyPermission(key. " w r i t e " ) < / c o d e >
* permission. This may result in a SecurityException being t h r o w n .
" If no exception is t h r o w n , the specified property is removed.
* <P>
*/
public static String clearProperty(String key) I
II caso di commenti doc dei sorgenti J D K che utilizzano il tag < H 4 > è un azzardo perché potrebbe
creare problemi con evoluzioni future JavaDoc e/o con estensioni create dall'utente.
4.2.5 Fare attenzione all'inserimento di link
Nel produrre documentazione è spesso necessario inserire dei link (hyperlink) ad altri elemen-
ti. In questi casi è opportuno evitare il ricorso al tag HTML <A> e utilizzare al suo posto il tag
{©Link}.
Il beneficio prodotto dall'utilizzo del tag link consiste nell'inserire un hyperlink all'interno
della documentazione. Pertanto permette di saltare da una parte di documentazione all'altra.
Poiché il pubblico dei fruitori di documentazione doc è costituito da personale esperto, è op-
portuno utilizzare con parsimonia questi link che richiedono tempo per essere inseriti e spesso
rendono la documentazione meno chiara.
Un esempio di utilizzo del tag link è presente nel commento doc utilizzato per illustrare il
metodo setProperties presente nella classe System.
* <P>
' First, it there is a security manager, its
* < c o d e > c h e c k P r o p e r t i e s A c c e s s < / c o d e > m e t h o d is called w i t h no
* a r g u m e n t s . This m a y result in a security exception.
' <p>
1. un metodo di una classe esegue l'overriding del corrispondente metodo della superclasse
(per esempio ogniqualvolta si scrive l'implementazione del metodo toStringQ di una classe);
2. un metodo di un'interfaccia esegue l'overriding del corrispondente metodo della
superinterfaccia;
3. quando un metodo di una classe implementa un metodo di un'interfaccia.
</head>
cbody bgcolor="white">
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
< l i x a href="">##### REFER TO NON-SPEC DOCUMENTATION HERE #####</a>
</ul>
</body>
</html>
II seguente listato riporta un esempio di utilizzo della documentazione doc al livello di package.
@(#)package.html 1.7 0 4 / 0 6 / 1 7
</head>
<body bgcolor="white">
<P>
Provides the classes and interfaces of
the J a v a < S U P x F O N T SIZE="-2">TM</F0NT></SUP> 2
platform's core logging facilities.
The central goal of the logging APIs is to support maintaining and servicing
software at customer sites.
<P>
There are four main target uses of the logs:
</P>
<0L>
<LI> <l>Problem diagnosis by end users and system administrators</l>.
This consists of simple logging of c o m m o n p r o b l e m s that can be fixed
or tracked locally, such as running out of resources, security failures,
and simple configuration errors.
<h2>Null Polnters</h2>
<P>
In general, unless otherwise noted in the javadoc, methods and
constructors will throw NullPointerException if passed a null argument.
The one broad exception to this rule is that the logging convenience
methods in the Logger class (the config, entering, exiting, fine, finer, finest,
log. logp. logrb, severe, throwing, and warning methods)
will accept null values
for all arguments except for the initial Level argument (if any).
<P>
<H2>Related Documentation</H2>
<P>
For an overview of control flow,
please refer to the
<a href="../../../../guide/logging/overview.html">
Java Logging Overview</a>.
</P>
</body>
</html>
/'
" This software is the confidential and proprletary Information of <nome azienda>
" ("Confidential Inlormation"). You shall not disclose such Confidential Information
" and shall use it only in accordance with the lerms of the license agreement
* you entered into with <nome azienda>.
7
* <b>Persistent:</b>Yes/No<br>
* @since JDK<*.*.x>
' @author <nome degli autori>
* ©version <x.xx.xxx> - <data dell'ultima modifica>
' @param <parametro> - <descrìzione del parametro
/ "
* <P>
* <b>Sample Usage</b> (Note that the following classes are all
" made-up.) <p>
' <pre>
' interface ArchiveSearcher I String s e a r c h ( S l r i n g target); I
' class App I
" ExecutorService executor = ...
' ArchiveSearcher searcher = ...
" void s h o wSea re h ( fi n a I String target) t h r o w s InterruptedException I
" F u t u r e & l t : S l r i n g & g t : future = e x e c u t o r . s u b m i l ( n e w C a l l a b l e & l t : S t r i n g & g t : ( ) {
public String call() I return searcher.search(targel): I
I):
" displayOtherThings(): / / do other things while searching
try!
displayText(future.getO): // use future
! catch (ExecutionException ex) I cleanup(): return; 1
' </pre>
" ©see FutureTask
* ©see Executor
"©silice 15
' aauthor Doug Lea
' ©param <V> The result type returned by this Future's <tt>get</tt> m e t h o d
7
public interface Future<V> (
1 0 / M a r / 2 0 0 8 - L.V.T. - Introdotto metodo per il riordino automatico degli elementi della lista.
E qui è riportato un esempio di commento doc relativo al metodo log della classe
java.util.jogging.Logger.
/**
* Log a LogRecord.
* <P>
* All the other logging methods In this class call through
' this m e t h o d to actually p e r f o r i t i any l o g g i n g Subclasses can
* override this single m e l h o d to capture ali log activity.
Come al solito, nella descrizione del metodo è importante descrivere cosa il metodo fa e non
come lo da. Le descrizioni dovrebbero essere indipendenti dall'implementazione. Inoltre, qua-
lora non sia immediato il perché il codice esegua determinati compiti è consigliabile aggiungere
anche una breve descrizione di ciò. Queste informazioni aiutano a inserire il metodo nel relati-
vo contesto e quindi ne semplificano la leggibilità e riutilizzabilità.
/ "
Ogni stile presenta specifiche caratteristiche che ne rendono opportuno l'utilizzo in determi-
nati ambiti a discapito di altri. Per esempio i commenti in JavaDoc sono quelli prelevati dal-
l'omonima utility e inseriti nella documentazione generata automaticamente da tale tool. Il
relativo utilizzo è pertanto consigliato come introduzione a dichiarazioni di classe, interfacce,
metodi e attributi, mentre più raro è il relativo utilizzo all'interno del codice.
I commenti stile C sono spesso utilizzati in tutti quei casi in cui il testo di commento richieda
diverse linee. Inoltre risulta particolarmente utile durante la fase di debbugging qualora si ren-
da necessario isolare porzioni di codice per individuare la parte di codice errata.
I commenti di singola linea sono tipicamente utilizzati per commentare variabili locali a
metodi e opportune porzioni di codice. Un'interessante convenzione consiste nell'allineare questi
commenti al margine destro. Tuttavia, per quanto tale convenzione sia in grado di produrre un
effetto gradevole in alcuni casi specifici, in diverse situazioni, l'ottenimento dell'effetto voluto
richiede un notevole investimento di tempo non sempre giustificato.
! "
' Sets this buffer's position. If the mark is defined and larger than the
* new position then it is discarded. </p>
* © r e t u r n This buffer
* ©throws IHegalArgumentExceptlon If the p r e c o n d i t i o n s on
< t t > n e w P o s i t l o n < / t t > do not hold
•/
position = newPosition;
return this;
/ "
' Returns the current value of the most precise available system
* timer, in nanoseconds.
' <p> For example, to measure how long some code takes to execute:
" <pre>
* long startTime = System.nanoTime():
* / / . . . the code being measured ...
'long estimatedTime = System.nanoTimeQ - startTime;
* </pre>
Recenti studi hanno dimostrato come lo sviluppo di sistemi robusti sia un compito comples-
so. A seconda degli studi considerati, la percentuale dei progetti software falliti varia in inter-
vallo compreso tra il 5 0 % e il 70%. A complicare la situazione interviene la tendenza moderna
di realizzare sistemi sempre più grandi, più complessi, costituiti da un insieme di sottosistemi
comunicanti dispiegati in diverse aree geografiche che forniscono servizi real-time. Logica con-
seguenza è che il requisito di affidabilità assume un ruolo di primaria importanza.
I moderni linguaggi di programmazione offrono sofisticati meccanismi di supporto per le
eccezioni (rilevazione e comunicazione), ma spesso il codice scritto male non è in grado di
gestire efficacemente, sistematicamente e consistentemente eventuali condizioni anomale.
Obiettivi
L'obiettivo di questo capitolo è fornire una serie di direttive operative, best practice e quant'al-
tro, necessarie per la realizzazione di sistemi robusti e affidabili. La trattazione è focalizzata sul
linguaggio di programmazione Java, sebbene molti concetti presentino una validità che pre-
scinde dallo specifico linguaggio di programmazione, e che include concetti architetturali di
più ampia portata. La lettura di questo capitolo dovrebbe fornire ai lettori materiale necessario
per la definizione di efficaci politiche di gestione delle eccezioni in grado sia di semplificare il
lavoro degli sviluppatori, sia di produrre sistemi robusti e quindi di maggiore qualità. Dalla
lettura di questo capitolo è possibile evidenziare come una corretta gestione delle eccezioni
richieda una serie di accorgimenti, che non sempre i programmatori considerano, come ad
esempio una consistente struttura dei record di log argomento del prossimo Capitolo 6.
Un po' di teoria
I concetti di "eccezione" e relativa gestione non sono certo nuovi nella comunità informatica. La
formulazione iniziale, infatti, è attribuibile a J.B. Goodenough il quale, nel "lontanissimo" 1975
[EFFCJA], propose formalmente di inserire un simile costrutto nei linguaggi di programmazione.
Ciò nonostante, fu necessario attendere ben cinque anni per vederne una concreta manifestazione:
nel 1980 la Microsoft, infatti, incluse il costrutto 0 N E R R O R G O T O nell'allora popolare GWBasic
(celebre dialetto del linguaggio BASIC inizialmente studiato per conto della Compaq, il cui nome
è un omaggio alle iniziali dal suo ideatore: Greg Whitten). L'exception è Xcxceptional event\ un
evento che si verifica durante l'esecuzione del programma e che scompagina il normale flusso di
esecuzione delle istruzioni. All'iniziale formulazione del concetto di eccezione seguirono accesi
dibattiti inerenti il suo utilizzo. In particolare, la comunità informatica si divise in due grandi gruppi:
da una parte si schierarono le persone che consideravano le eccezioni come uno strumento demandato
esclusivamente a notificare l'insorgere di condizioni di errore, e dall'altra coloro che invece ne
proponevano un utilizzo più ampio paragonabile a quello di qualsiasi altro costrutto come i cicli for,
while e do [EXCPRL], Lo stesso J.B. Goodenough nella formulazione iniziale ne propose un utilizzo
alquanto esteso, destinato ad includere le seguenti principali situazioni: gestione delle condizioni di
errore; elaborazione di un costrutto supplementare atto a comunicare, in modo artificioso,
informazioni circa il corretto completamento di un'operazione; controllo delle operazioni in corso
di esecuzione. La visione attualmente accettata e quindi considerata in questo libro, è chiaramente
la prima, che come espresso nel 1987 da Melliar-Smith e Randall asserisce che le "eccezioni sono
una proprietà dei linguaggi di programmazione atta a migliorarne la caratteristica di affidabilità
alla presenza di condizioni di errori o di eventi inaspettati. Le eccezioni non sono destinate a
fornire costrutti di controllo generale. Un utilizzo liberale non dovrebbe essere considerato
sufficiente per fornire ai programmi una completa tolleranza degli errori".
Z\
i
java .lang.Throwable
" 1 j a v a . lang. R u n t l m e E x c e p t l o n
try I
fileReader = new BufferedReader(new FileReader(file));
while (leof) I
nextLine = fileReader.readLine();
if (leof) (
strBuffer.append(nextLine);
I finally I
tryl
if (fileReader != nuli) I
fileReader.close();
I
I catch (lOException ioe) I
logException(ioe); // this is an internai method
I
I
return strBuffer.toString();
I
Si consideri, per esempio, di dover leggere delle informazioni presenti in un file di testo. A
questo punto, la prima cosa da farsi consiste nel crearne una rappresentazione logica del file (File
file = new File(filePath); ) e quindi passarne 0 riferimento a uno stream di lettura (BufferedReader
fileReader = new BufferedReader(new FileReader(file)) ). Giacché il file potrebbe non esistere, il
costruttore della classe java.io.FileReader può lanciare un'eccezione java.io.FileNotFoundException che
deriva da java.io.lOException che a sua volta eredita java.lang.Exception. Trattandosi di un'eccezione
checkcdh obbligatorio o gestirla attraverso apposito costrutto catch oppure rilanciarla al metodo
chiamante (dichiarazione nella definizione del metodo). Quest'ultima soluzione è adottata nel
listato visto poco sopra. Qualora ciò non avvenga, il compilatore genera un apposito messaggio
di errore. La tabella 5.1 è una comoda sintesi delle proprietà dei due diversi tipi di eccezione.
r i l a n c i a r l a (IhrOWS)
Exception
Classe antenata RunlimeExceplion o Error
Direttive
5.1 Utilizzare le eccezioni
Il meccanismo delle eccezioni permette di risolvere una serie di anomalie causate dalle prece-
denti strategie di gestione degli errori, e in particolare garantisce
• Migliore organizzazione del codice. Ciò grazie alla separazione elegante e pulita tra il codi-
ce necessario per implementare il particolare servizio e quello richiesto per gestire even-
tuali errori. Inoltre, non è necessario ritornare esplicitamente codici di errore che riduco-
no la chiarezza delle API con continui passaggi di codici di controllo e forzano la ripetizio-
ne di blocchi di istruzioni necessari per controllare tali valori in tutti i metodi chiamanti.
• Maggiore livello di robustezza. Codici di errori sono difficili da mantenere, specialmente
a seguito a refactoring del codice e possono facilmente generare situazioni inconsistenti
come quelle dovute a stessi codici utilizzati per rappresentare anomalie diverse, o a codici
non più generati da una classe che comunque persistono nel sistema. Pertanto, a differen-
za delle eccezioni, i codici di errore possono facilmente sfuggire al controllo.
• Scrivere codici più leggibili. In particolare, poiché i parametri restituiti dai metodi non
devono essere compromessi per via della necessità di restituire codici di errore, è possibile
dar luogo a firme dei metodi non artificiosi. Inoltre, il meccanismo delle eccezioni rappre-
senta la pratica quotidiana che consiste nello specificare una serie di richieste e quindi
fornire ulteriori informazioni relative alla situazione in cui non sia possibile soddisfare
l'elenco principale. Un esempio classico è quello della spesa "acquista 1 kg di riso e una
bottiglia di Barolo; se non trovi il Barolo, prendi pure una bottiglia di Nebbiolo" e così via.
• Dare luogo a un migliore disegno. Questo grazie al fatto che, spesso, il punto in cui un
errore si verifica non è il posto migliore dove gestirlo e, utilizzando i codici di errore, è
molto frequente che accada che il percorso necessario per la propagazione a ritroso
dell'errore generi la perdita di informazioni relative al contesto dell'errore verificatosi.
Le eccezioni, invece, includono tutte le informazioni necessarie dal punto in cui si veri-
ficano sino al punto in cui vengono gestite.
• Migliorare le prestazioni. Ciò essenzialmente per via del fatto che non è necessario veri-
ficare continuamente i valori restituiti dai metodi.
5.1.1 Le eccezioni posso verificarsi: è un fatto inevitabile
Nel mondo ideale si sarebbe tentati di pensare che, in presenza di un codice ben scritto, le
eccezioni non dovrebbero mai verificarsi, quindi l'infrastruttura dovrebbe sempre funzionare
correttamente, nessun processo business dovrebbe mai interrompersi, tutti i messaggi dovrebbe-
ro essere corretti e consegnati nell'ordine previsto, il Database Management System dovrebbe
essere sempre perfettamente funzionante e così via. Purtroppo, nella realtà la situazione è decisa-
mente diversa e, per quanto accuratamente si tenti di scrivere il codice, le eccezioni comunque si
verificano. Chiaramente, se poi il codice è scritto in modo trasandato, allora le eccezioni tendono
a presentarsi frequentemente e l'applicazione tende a presentare un elevato livello di instabilità.
Pertanto, sebbene sia necessario produrre il massimo sforzo per aumentare la qualità del
codice e per realizzare opportuni meccanismi atti a diminuire la probabilità del verificarsi delle
eccezioni, queste comunque si verificano e quindi solo un attento e ben progettato sistema di
gestione è grado di fare la differenza tra sistemi affidabili e di qualità e sistemi problematici.
Si consideri il codice assolutamente sbagliato riportato nel listato poco sotto. Si tratta del
metodo già riportato nel listato visto in precedenza, ove le eccezioni sono state sostituite da
codici di errore. Come si può notare, la firma del metodo risulta decisamente meno intuitiva e
non c'è un modo formale per inserire la lista dei possibili errori che possono verificarsi. Per
quanto riguarda il codice restituito è stata utilizzata la seguente convenzione:
Ecco il codice precedente, modificato affinché il metodo ritorni codici di errore e non eccezioni.
public String readFilefString tilePath, String result) I
int errorCode= 0;
result = null;
File file = new File(filePath);
try I
fileReader = new BufferedReaderfnew FileReader(file));
while (leof) I
nextLine = fileReader.readLine();
if (leof) I
strBuffer.append(nextLine);
result = strBuffer.toStringf);
I finally {
try I
if (fileReader != null) I
fileReader.close();
)
) catch (lOException ioe) I
logException(ioe); // this is an internal method
I
I
return errorCode;
Ed ecco i controlli del codice di ritorno da ripetersi nella lista di metodi inclusi nella sequen-
za di invocazione.
Sting fileContent = "";
if (errorCode == 0) I
/ / s e q u e n z a di istruzioni necessarie per gestire
// il caso di successo
I else if (errorCode == -1) I
// sequenza di istruzioni atte a gestire
// la situazione di file not f o u n d
I else if (errorCode == -2) I
// sequenza di Istruzioni atte a gestire
// la situazione di p r o b l e m i di IO
try I
fileReader = new BufferedReader(new FileReader(file));
nextLine = fileReader.readLine();
il (!eof) I
slrBuffer.append(nexlLine);
return strBufter.toString();
) finally I
try I
if (fileReader != null) I
fileReader.close();
I
} catch (lOException ioe) I
logException(ioe); // this is an internal method
Si noti che la clausola finally è eseguita sia quando non intervengono problemi (ossia la J V M
esegue il return presente nel costrutto try), sia quando ci sono dei problemi, ossia quando ven-
gono eseguite le clausole catch.
while (ieof) I
nextLine = tileReader.readLine();
if (!eof) I
strBuffer.append(nextLine);
I
)
try!
if (fileReader != null) I
fileReader.close();
)
I catch (lOException ioe) I
logException(ioe); // this is an internal method
I
try I
if (fileReader != null) I
fileReader.close();
I
I catch (lOException ioe) I
logException(ioe); / / t h i s is an internal method
throw fnfe;
try)
it (fileReader 1= null) {
fileReader.close();
I
) catch (lOException ioe) (
logException(ioe); // this is an Internal method
I
throw ioe:
return strBuffer.toString();
I
Il mancato utilizzo del blocco finally ha richiesto di ripetere il blocco delle istruzioni di chiusu-
ra (fileReader.close()) in diverse parti del codice (all'interno del blocco try e in tutti i vari catch).
Ciò, oltre a causare un'inutile ripetizione di codice (in parte risolvibile con l'implementazione di
un apposito metodo), potrebbe generare problemi qualora un aggiornamento del codice richie-
da di gestire nuove eccezioni e ci si dimentichi di ripetere il blocco di chiusura dello stream.
try I
I catch (Exception e) I
// qualche operazione di gestione
I
Come si può notare il costrutto catch intercetta, in maniera impropria, istanze della classe
java.lang.Exception. Ora, si supponga di estendere il precedente codice invocando un metodo che
effettui il parsing del contenuto del file XML, trasformando la stringa in un apposito grafo di
oggetti (ConfigVO), come riportato nel seguente frammento:
try {
I catch (Exception e) {
// qualche operazione di gestione
I
Si supponga, come è lecito fare, che il metodo sia in grado di generare un'eccezione. Come si
può notare, la presenza del costrutto catch (Exception e) finisce per intercettare la nuova eccezio-
ne senza dare comunicazione al programmatore. Il caso in questione potrebbe sembrare co-
munque senza troppe conseguenze: in fondo, sono coinvolte solo due istruzioni... Si immagini
però il caso tipico di una serie abbastanza lunga di invocazioni, in cui diversi metodi siano
composti da circa 10-15 istruzioni, oppure la situazione abbastanza frequente in cui sia neces-
sario cambiare la firma di un metodo, aggiungendo una nuova eccezione, e che questo sia
utilizzato in molte parti del sistema.
In questi casi si capisce come è facile generare lo scenario in cui la nuova eccezione venga
intercettata in un posto dove non dovrebbe esserlo e quindi viene gestita in modo errato. A
complicare le cose interviene il fatto che problemi di questo tipo, normalmente, sono difficil-
mente individuabili.
• esiste una eccezione base Java in grado di descrivere il problema che si intende comunicare?
• l'implementazione di nuove eccezioni migliora il codice? In particolare, le classi client
ricevono un chiaro beneficio dalla presenza della nuova eccezione?
• la stessa porzione di codice lancia altre eccezioni in qualche modo legate alla nuova?
• la nuova eccezione o quelle fornite da una terza parte, sono accessibili alle classi client?
Qualora una o più delle precedenti domande presenti una risposta negativa, è il caso di
verificare opportunamente la necessità di ricorrere all'implementazione di una nuova eccezione.
/ "
Figura 5.2 - Esempio di gerarchia delle eccezioni definito nel package java.security.
* Constructor method
* ® p a r a m excMessage exception message
" @ p a r a m error encapsulated o c c u r r e d
'/
public MyException(String excMessage, Throwable error) (
super(excMessage, error);
A
tryi
La direttiva di mantenere l'utente sempre informato su quanto accade nel sistema si applica a tutti
gli scenari e non solo a quelli di errore. Per esempio, quando si devono far eseguire al sistema
lunghi processi (come le tipiche procedure di revisione che si eseguono in banca a chiusura della
giornata, i famosi E O D , End Of Day), è sempre opportuno cercare di suddividere il processo
sull'intero insieme di dati in un numero di iterazioni dello stesso su opportune ripartizioni del
dominio. Ciò non solo per avere l'opportunità di fornire informazioni all'utente circa lo stato di
avanzamento del processo stesso, tra un'iterazione e quella successiva, ma anche per gestire
transazioni di minori dimensioni, per salvare risultati intermedi molto utili se il processo fallisce: in
tal caso non bisognerà ricominciare da capo ma dall'elemento successivo all'ultimo processato.
1. predisporsi a ricevere notifiche relative a eccezioni di tipo business (per esempio sotto-
scrivendo il canale delle eccezioni);
2. ricevere e analizzare le varie segnalazioni generate dai sottosistemi assistiti;
3. per ogni messaggio ricevuto, valutare, in base a un opportuno sistema di regole, la prio-
rità da assegnare alla corrispondente gestione;
4. creare un record relativo ad ogni eccezione ricevuta, inserirlo in un'apposita coda interna
e, contestualmente, inviare una segnalazione agli opportuni operatori sulla sua presenza;
5. verificare continuamente i record presenti nelle varie code al fine di aumentare la priori-
tà di quelli che sono presenti nella coda da un eccessivo intervallo temporale.
Da tener presente che per quanto l'intervento umano sia una soluzione molto flessibile,
tipicamente è anche molto costosa, e pertanto è buona pratica minimizzarne l'utilizzo.
• analisi statistiche;
• attività di controllo del programma, come riproduzione di scenari di errore, analisi di
specifiche transazioni, etc.;
• implementazione di meccanismi di backup e recovery;
• analisi dello stato dell'applicazione da parte di altri sistemi di amministrazione.
Obiettivi
L'obiettivo di questo capitolo è fornire una serie di direttive e best practice per l'implementazione
di efficaci strategie di log.
Sebbene molto frequentemente il concetto di logging sia inevitabilmente associato con la
libreria Log4J (l'esempio che spiega il concetto, l'istanza che illustra la classe) e per quanto
anche questo libro ne faccia largo utilizzo, il presente capitolo non è e non può essere una guida
di riferimento di Log4J (a tal fine esistono appositi libri, cfr. l'Appendice E). Tuttavia parlare di
strategia di logging senza presentare minimamente i principali tool di logging equivarrebbe a
parlare di programmazione senza citare alcun linguaggio di programmazione.
Ciò premesso, le varie regole presentate nel corso di questo capitolo hanno una loro validità
che esula dall'uso di un particolare software.
Un po' di storia
Ogni volta che si parla di logging, si pensa a Log4j (http://logging.apache.org/log4j/). Ciò è abba-
stanza normale considerando sia che si tratta di un software che ha contribuito notevolmente a
trasformare questa attività in una vera e propria pratica ingegneristica, sia il grandissimo suc-
cesso riscosso. Successo ulteriormente confermato da vari porting del codice, come Log4net,
Log4xx, etc.
L'importanza delle strategie di logging, tuttavia, è stata compresa dagli uomini e applicata
per secoli negli ambienti più disparati... Basti pensare ai diari di bordo dei grandi navigatori
del XVII e XVIII secolo, tuttora utilizzati per i fini più diversi, come studi storici generali o
relativi alle tecniche di costruzione navale e di navigazione, alle conoscenze di cartografia, o alle
variazioni climatiche del pianeta Terra e così via.
Il metodo più semplice, e sicuramente meno efficace, spesso utilizzato dagli sviluppatori alle
prime armi per eseguire il logging dell'applicazione consiste nell'eseguire stampe a video:
Per quanto questo metodo offra il vantaggio della semplicità, e possa risultare molto comodo
per attività a brevissimo termine, presenta un insieme di seri problemi tali da sconsigliarne
l'utilizzo. Alcuni dei più importanti sono:
1. non esiste un meccanismo automatico che consente di inibire i messaggi. Ciò risulta
assolutamente necessario al momento di mettere il sistema in produzione.
2. non è immediato implementare un sistema atto ad organizzare i log per categorie, come
trace, info, debug, etc. e quindi non è sempre possibile eseguire il tuning dei log, specie in
presenza di sistemi complessi. Si consideri, per esempio, la necessità di voler escludere i
log della libreria di integrazione con il database e, allo stesso tempo, mantenere i log
della restante parte del sistema.
3. i messaggi di log sono inviati nel dispositivo standard di output, tipicamente il video, e
pertanto risultano poco utili sia per la fase di sviluppo, in cui è possibile utilizzare sofisti-
cati strumenti di debug, sia per la diagnosi del sistema in produzione.
4. spesso il deployment di sistemi richiede che diverse applicazioni condividano uno stesso
server, per esempio non è assolutamente infrequente lo scenario di un server dotato di
diverse CPU ospiti diversi server, in questa configurazione (a seconda delle impostazioni)
i vari log potrebbero finire o per mescolarsi o per creare una moltitudine di finestre
difficilmente consultabili.
Dati gli evidenti enormi svantaggi generati dal logging eseguito attraverso all'output sulla
console, è evidente come una corretta strategia di logging sia fondamentale per la produzione
di un sistema di qualità.
L'esperienza insegna che sistemi di bassa qualità tendono a presentare tantissimi problemi
una volta messi in produzione, ma anche, che gli errori possono accadere (e lo fanno! parola di
Murphy) anche in sistemi accuratamente progettati ed implementati. Quindi un'opportuna
strategia di logging è in grado di far risparmiare tempo e denaro nell'individuazione di errori e
a mantenere elevata la confidenza da parte degli utenti del sistema.
Log4J
Log4J è probabilmente il più famoso software di logging. Le sue caratteristiche principali sono
la semplicità di utilizzo, l'elevata affidabilità, le buone prestazioni, la ricchezza di feature, e
l'estensibilità. Si tratta di uno dei software di maggior successo tra quelli appartenenti a quella
fucina di idee open source denominata Apache. L'ottima riuscita di Log4J è testimoniata da
diversi fattori, come il larghissimo utilizzo in applicazioni professionali e non solo, la presenza
di versioni realizzate per altri linguaggi (porting Log4Net, Log4xx, etc.) e dalla conquista dello
status di standard de facto, status che neanche l'introduzione dell'API "standard" java.util.logging
è riuscita a sminuire.
L'introduzione di Log4J ha portato molti benefici, tra i quali uno dei più importanti consiste
nell'aver enormemente semplificato e standardizzato il meccanismo del logging. Prima della
produzione di Log4J, il logging delle applicazioni era un problema serio. Tanto che molte ap-
plicazioni venivano rilasciate senza appropriate funzionalità di logging, oppure con soluzioni
basilari implementate in qualche modo dai singoli team di sviluppo, i quali spesso si affidavano
alla visualizzazione di messaggi sulla console (con tutti le problematiche riportate nei paragrafi
precedenti). Meno frequenti invece erano le situazioni in cui i team di sviluppo investivano
tempo e denaro per produrre vere e proprie librerie. Anche in queste situazioni, tali librerie
molto spesso erano realizzate rapidamente (bisognava investire il tempo sull'automazione del
business) finendo per implementare funzionalità piuttosto basilari con tutti i limiti del caso:
impossibilità di eseguire il tuning dei log, significativo impatto sulle performance, etc.
Tuttavia, uno dei tentativi degni di nota in cui si tentò di implementare una vera e propria
libreria di logging fu quello eseguito all'interno del progetto della comunità europea SEMPER
(,Secure Electronic Marketplace for Europe, mercato elettronico sicuro per l'Europa, 1996), di
cui Joe-Luis Abad-Peiro fu l'autore iniziale. Questa libreria costituì la base di partenza utilizza-
ta da Ceki Gùlcu, che dopo diversi ripensamenti, revisioni e cambiamenti portò alla realizza-
zione di jZRLog. Questa prima versione fu poi ulteriormente rielaborata grazie anche alla par-
tecipazione di persone come: Andreas Fleuti, Micheal Steiner e N. Asokan fino al consegui-
mento della pubblicazione ad ottobre del 1999 di Log4J su alpha Works.
Struttura e funzionamento
Gli elementi fondamentali di Log4j sono tre: Logger, Appender e Layout (figura 6.1). Le loro
istanze cooperano per ottenere la produzione di opportuni messaggi in formati prestabiliti
nelle destinazioni specificate. I Logger (dalla versione Log4J 1.2 hanno rimpiazzato gli iniziali
elementi Category) hanno come responsibilità principale la cattura dei messaggi. Sono organiz-
zati secondo una ben definita gerarchia che permette di filtrare i vari messaggi. Si tratta di
elementi dotati di un nome univoco (necessario per il relativo reperimento) e sono organizzati
secondo una gerarchia in grado di rispecchiare i package Java. Pertanto, il logger com.mokabyte
risulta genitore del logger com.mokabyte.financing. Gli Appender hanno la responsibilità di pubbli-
care le informazioni di log su opportuni target. Per esempio, l'appender che invia messaggi a
video (ConsoleAppender) ha la console come target. Questi possono utilizzare una serie di filtri: in
questo caso tutti i filtri devono abilitare il log affinché questo venga incluso in nel target dell'ap-
pender. Infine i Layout sono responsabili per la formattazione dei vari messaggi.
Nella figura 6.2 è mostrato il diagramma delle classi relativo alla struttura interna di Log4j.
Come si nota dal diagramma delle classi (figura 6.2), Log4J offre un elevato grado di flessibi-
lità ed estensibilità: gli elementi fondamentali, come per esempio Appender, sono rappresentati da
un'interfaccia (Appender), spesso corredata da una classe base astratta (AppenderSkeleton) che in-
clude il comportamento base condiviso da tutte le specializzazioni. Ciò permette di implementa-
re più agevolmente le diverse versioni del concetto in questione (ConsoleAppender, FileAppender).
Tali elementi rappresentano vari e propri punti di estensione, socket in cui inserire le varie
customizzazioni. La presenza di ben ponderati punti di estensione e l'elevato numero di plug-in
disponibili hanno contribuito notevolmente al grande successo di Log4J. Per poter utilizzare
Log4J all'interno di una classe è necessario:
Ha la resposabilità di Ha la responsabilità di
Ha la resposibilitàdi catturare
pubblicare'nessaggi di log su formattare i messaggi nello
messaggi di log
uno o più target stile desiderato
ii ^ C r
Logger Appender Layout
p V J
Applicazione
-vC
Universo e s t e m o
Filler Target
Tutti i fi Iter
devono
abilitare il log
...::Log4h:spi::
Appenderwttachable
::log4j::Logger
...::log4j;:hetpers
AppenderAttachablelmpl
...::log4j:: ...:iog4j::
AppenderSkeleton BasIcConf Ig urator
• closed boolean
#name Slnng
:log4J::
PropertyConfigurator
.. ::log4j::spi:: log4j::spi:
Configurator LoggerFactory
::log4j::
Layout ...::k>g4J::spi
* closed boolean Filter
0 name String
D E N Y ml
...::k>g4j::spi:: N E U T R A L inl
Option Handler A C C E P T int
• ^f fy I 7TT
«headl ;
// printing methods:
public void trace(0bject message);
public void debug(0bjecl message);
public void info(0bject message);
public void warn(0bject message);
public void error(Object message);
public void fatal(0bject message);
La classe Logger (grazie all'eredità da Category) dispone di una composizione con un'istanza
Level (anche se sarebbe stato più corretto associarvi la classe Priority, questa è stata introdotta in
un secondo momento), che, come lecito attendersi, permette di definire il livello di severità
della specifica istanza del Logger. I diversi livelli di severità o di log sono rappresentati da istanze
statiche della classe Level, la cui semantica è descritta nella tabella 6.1. Oltre ai livelli di logging
illustrati nella tabella 6.1, Log4J mette a disposizione altri due livelli particolari:
• ALL: trattandosi del livello di logging più basso in assoluto, è utilizzato per abilitare tutti
i restanti livelli di log;
• OFF: si tratta del livello di log più alto e quindi utilizzato per inibire completamente il
logging.
La seguente relazione mostra la relazione di maggioranza che lega i vari livelli di severità:
ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
I livelli di log predefiniti dovrebbero essere più che sufficienti per gestire le tipiche necessità
di logging di un'applicazione. Tuttavia Log4J permette di definire ulteriori livelli di logging
personalizzati. Questa pratica è sconsigliata a meno che non si abbiano delle esigenze di inte-
grazione particolari non prettamente legate al meccanismo di log.
In Log4j, tutti le istanze Logger hanno un livello di log. Questo può essere assegnato esplici-
tamente oppure ereditato da un logger antenato. La regola afferma che un log a cui non sia
stato assegnato esplicitamente un livello di log, eredita quello assegnato al più prossimo antena-
Categoria Descrizione
Si tratta del livello di logging più severo ed è utilizzalo per segnalare messaggi
funzionare.
a l l ' a p p l i c a z i o n e di p o t e r c o n t i n u a r e a f u n z i o n a r e .
S i t r a t t a d e l l i v e l l o d i l o g a s e v e r i t à p i ù b a s s a e d è u t i l i z z a t o p e r il d e b u g d i e v e n t i molto
TRACE
d e t t a g l i a t i . T i p i c a m e n t e l o si u t i l i z z a p e r e s e g u i r e il l o g d i f l u s s i d i e s e c u z i o n e .
Tabella 6.1 - Livelli di logging di Log4j definiti nella classe org.apache.log4j. Level.
to. Questo, nella maggioranza dei casi è la root, l'antenato di tutti i logger, che, secondo le
regole, deve necessariamente avere un livello assegnato. L'assegnamento del livello di log può
avvenire o programmaticamente (LOGGER.setLevel(Level.INFO), strategia sconsigliata), oppure,
molto più frequentemente, attraverso opportuni file di configurazione.
Pertanto, qualora si esegua una richiesta di log, (invocazione di un metodo di log), questa è
abilitata, e quindi produce un record (a meno di filtri), se e solo se, il relativo livello è superiore
o uguale a quello del logger di appartenenza. Per esempio, l'invocazione LOGGER.debugftrade
new value ="+trade.getValueAmount()) viene abilitata se, e solo se, il livello del logger è impostato a
DEBUG o a uno dei livelli inferiori. Più formalmente: una richiesta di log di livello n eseguita in
un logger di livello m è abilitata se è solo se n >- m
La figura 6.3 mostra un esempio di configurazione di Log4J, in cui due classi utilizzano il
relativo logger ereditando le impostazioni del nodo di root. Poiché i nodi a livelli intermedi (come
com), non sono presenti o non utilizzano il relativo logger, questi non vengono inseriti nella
generarchi di log4j. Così si incrementano le performance e si riduce l'occupazione di memoria.
La tabella 6.2 mostra la mappa della verità del livello di log delle invocazioni e quello dei
logger. Alcuni appender disponibili in Log4j sono:
root : C a t e g o r y D E B U G G E R : Priority
FIlaAppandar Appender
ApponderAttacheablelmpI '
cpm.mphabytaHnanc«Teat : cpmmokabvtaflnancaMaln
Category : Category
DEBUG Y Y Y Y Y
INFO N Y Y Y Y
WARN N N Y Y Y
ERROR N N N Y Y
FATAL N N N N Y
ALL Y Y Y Y Y
OFF N N N N N
Tabella 6.2 - Tabella della verità tra livello di log delle richieste e quello del Logger.
• SocketAppender: con questo appender i messaggi di log sono inviati ad un log server re-
moto, tipicamente un SocketNode.
• SocketHubAppender: con questo appender i messaggi sono inviati in forma di oggetti
LoggingEvent ad un insieme di log sever remoti, tipicamente SocketNodes.
• SysIogAppenders: questo appender invia i log ad un servizio di syslog remoto.
• TelnetAppender: in questo caso i messaggi vengono inviati a un socket di sola lettura.
• JMSAppender: come lecito attendersi, in questo caso il record di log viene inviato su un
topic di un sistema di messaggistica JMS.
• JDBCAppender: si tratta di una semplice implementazione di un appender atto a registrare
i record di log in un database.
• layout: si tratta di una relazione con la classe Layout, che è necessaria per formattare,
tipicamente in modo comprensibile a persone, le informazioni fornite.
• target: ogni appender ha un target associato: può essere la console, un file, un socket, e così
via. Il target è intrinseco nel log: è dato dall'implementazione dell'intero appender.
• level: il livello è rappresentato dall'associazione con la classe Priority (da non utilizzarsi
direttamente, di cui Level è l'estensione accessibile), denominata threshold. Questo svolge
la stessa funzionalità del livello del logger, ma ne è indipendente e quindi rappresenta un
ulteriore livello di abilitazione/inibizione.
• fi Iter: ogni appender può essere dotato di una lista concatenata di filtri. L'appender con-
serva il riferimento alla testa della lista (associazione headFilter) e alla coda (associazione
tailFiiter). Ogni elemento filtro (implementazione della classe astratta ...log4j.spi.Filter)
mantiene il riferimento al successivo (auto-relazione next) e include il metodo astratto
decide(LoggingEvent event) c h e r e s t i t u i s c e u n a d e l l e c o s t a n t i : ACCEPT, D E N Y , N E U T R A L .
Come nel caso degli appender, in Log4j anche la classe astratta Layout prevede una serie di
specializzazioni, in particolare:
Il listato seguente mostra una configurazione di Log4J utilizzato per un'applicazione Tomcat.
Questo file deve essere inserito nella cartella /webapps/ping-server/WEB-INF/<d/recfory applicazione>.
log4j.debug=TRUE
log4j.rootLogger=INFO, R
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=/home/l/h/ihrname.ch/tomcatlogs/tomcat.log
log4j.appender.R.MaxFileSlze=100KB
log4j.appender.R.MaxBackuplndex=5
log4j.appender.R.layout=org.apache.log4j. PatternLayout
log4j.appender.R.layout.ConversionPattern=%d|yyyy-MM-dd HH:mm:ss.SSSS) %p %t %c - % m % n
log4j.appender.mail=org.apache.log4j.net.SMTPAppender
log4j.appender.mail.To=@ERROR-MAILTO@
log4j.appender.mail.From=@ERROR-MAILFROM@
log4j.appender.mail.SMTPHost=@ERROR-MAILHOST@
log4j.appender.mail.Threshold=ERROR
log4j.appender.mail.ButferSize=1
log4j.appender.mail.Subject=CCT Application Error
log4j.appender.Mail.layout
=org.apache.log4j.PatternLayout log4j.appender.Mail.layout.ConversionPattern=%d % - 5 p % c % x - % m % n
E possibile specificare la configurazione per Log4J anche attraverso il formato XML. Proba-
bilmente si tratta dell'alternativa più elegante, sebbene, probabilmente per motivi storici, il
formato classico (property file) tenda ancora a essere molto utilizzato. Tuttavia, la tendenza è
sempre più quella di utilizzare il formato XML tanto che gli appender più recenti, come per
esempio AsyncAppender, prevedono solo questa modalità di configurazione.
<!— —>
< ! — setup l o g o ' s root logger — >
<!— —>
<root>
clevel value="all" / >
<appender-ref ref= "EMAIL A P P " / >
</root>
</log4j:configuration>
java.util Jogging
java.util.logging è una API introdotta in Java relativamente tardi con la versione 1.4 (JSR 47, http://
jcp.org/en/jsr/detail?id=47) rilasciata nel maggio del 2002. Si tratta di una API chepresenta molte
similitudini con Log4J. L'architettura concettuale di Java Logging, come mostrato in figura 6.4, è
del tutto equivalente a quella di Log4j. Considerate le similitudini con Log4J, non è necessario
illustrare in dettaglio questa libreria: sono presenti gli stessi concetti con nome diverso.
Anche in questo caso, ogni classe che intenda registrare appositi log deve:
1. importare la libreria:
i m p o r t java.util.logging.Logger
Si tratta del livello più alto, utilizzato per segnalare messaggi estremamente importanti,
SEVERE
c o m e e r r o r i f a t a l i c h e i m p e d i s c o n o al s i s t e m a d i f u n z i o n a r e .
WARMING C l a s s i c i m e s s a g g i di a v v e r t i m e n t o .
INFO U t i l i z z a t o p e r m e s s a g g i di informazione
FINEST Si t r a t t a d e l l i v e l l o p i ù b a s s o , u t i l i z z a l o p e r s e g n a l a z i o n i di d e t t a g l i o e l e v a t o
3. utilizzare il logging:
LOGGER.finestfHello logging!");
java::utili::k>gging
Handler
java::ulìli::logging
SocketHandler
k J ^ > /
varia a seconda della versione. Versioni precedenti alla 1.1 utilizzano il primo ritrovato,
mentre le versioni successive selezionano il file a più alta priorità. Questa è definita da un
apposita parola chiave (priority). Qualora poi diversi file dovessero presentare la stessa
priorità, allora nuovamente verrebbe selezionato 0 primo file incontrato a più alta priorità.
2. Qualora il passo precedente dovesse fallire, tenta di reperire la proprietà di sistema
denominata: org.apache.commons.logging.Log. Come nel caso precedente, la proprietà scritta
in lettere minuscole viene accettata ugualmente.
3. Ulteriore tentativo consiste nel cercare di reperire Log4J dal classpath; in tal caso J C L si
limita a utilizzare la corrispondente classe wrapper: Log4JLogger.
4. Ultimo tentativo: si prova a utilizzare il logging standard Java. Questo è ovviamente
possibile solo per J D K 1.4 e superiori. La classe di wrap utilizzata è Jdk14Logger.
5. Fallito anche il precedente tentativo, J C L utilizza un semplice logging wrapper chiama-
to appunto SimpleLog.
Come si può notare Log4j rappresenta lo strumento di log di default per JCL, questo sia per
motivi diciamo di appartenenza (la "squadra Apache" è la stessa), sia perché prediligere TAPI
Java, di fatto, non avrebbe dato alcuna possibilità a Log4J di essere selezionato: è sempre più
raro trovare applicazioni precedenti alJDK1.4. Una volta configurato JCL, (il che equivale a
configurare uno dei tool da utilizzare) è necessario:
Come si può notare il tutto è molto simile a Log4J e inoltre la presenza del meccanismo di
discovery rende l'utilizzo di JCL assolutamente non intrusivo.
Direttive
6.1 Investire nel logging
Questa regola potrebbe sembrare inutile vista la sua universale accettazione... almeno dal pun-
to di vista teorico. Tuttavia non è infrequente imbattersi in sistemi completamente privi di
meccanismi di log, con log effettuati attraverso semplice stampa a video, o dotati di log incon-
sistenti e caotici, tanto da generare il mal di testa del personale addetto a supporto e manuten-
zione del sistema. Sistemi di questo tipo rendono problematico e lento il processo di analisi di
eventuali problemi che possono verificarsi e tipicamente occorrono a sistemi in produzione.
Lunghi tempi di analisi e soluzione di problemi finiscono inevitabilmente per ingenerare negli
utenti l'idea che il sistema non sia sufficientemente robusto e di buona qualità.
Log4J J a v a logging
Maturità E s t a r o il p r i m o t o o l r e s o d i s p o n i b i l e e È s t a t o r i l a s c i a t o c o n la v e r s i o n e J D K 1 . 4 ,
quindi è q u e l l o c h e p r e s e n t e da più e q u i n d i p r e s e n t a un m i n o r e g r a d o di
tempo e quindi maggiormente m a t u r i t à s e b b e n e la relativa a r c h i t e t t u r a
consolidato. è stata c o m p l e t a m e n t e b a s a t a su q u e l l a di
Log4j.
Stabilità M o l t o elevata. E l e v a t a , m a i n f e r i o r e a q u e l l a di L o g 4 j .
Tempo medio P o i c h é si t r a t t a di u n t o o l o p e n s o u r c e L a c o r r e z i o n e d e i b u g e il r i l a s c i o d e v e
di correzione e m e n o f o r m a l e , i b u g t e n d o n o ad s e g u i r e i t e m p i e il p r o c e s s o f o r m a l e
dei bug e s s e r e risolti a b b a s t a n z a v e l o c e m e n t e . stabilito per le correzioni del J D K . U n
fix di q u e s t o p a c k a g e n o n p u ò e s s e r e
rilasciato da solo.
Dipendenza E n e c e s s a r i o i n c l u d e r e le l i b r e r i e di Nessuna
da J A R esterni Log4j.
Nella letteratura informatica è possibile incontrare altri approcci, come quello relativo alla
definizione di log tematici: performace (Logger LOG_PERFORMANCE = Logger.getLoggerfperformance")),
sicurezza (Logger LOG_SECURITY = Logger.getLogger("security")), etc. Sebbene questa strategia sia
basata su una valida intuizione, si consiglia di mantenere la strategia standard, demandando ad
altri tool l'analisi di aspetti specifici come le performance, la sicurezza, e così via.
Sebbene, in linea di principio si tratti di una valida prassi, questa non è esente da effetti
collaterali. In particolare, bisogna porre attenzione ad alcuni elementi, quali:
• semplifica il lavoro del personale addetto alla manutenzione del sistema: per esempio è
possibile effettuare delle ricerche mirate nel file di log;
• agevola l'addestramento del personale addetto alla manutenzione del sistema;
• permette di realizzare sistemi esterni atti a monitorare i file di log al fine di identificare
specifiche condizioni, come per esempio tentativi di eseguire delle operazioni sensibili
per la sicurezza del sistema.
La consistenza dei record di log è particolarmente utile per tutti i livelli utilizzati dal sistema
in produzione, pertanto per tutti quelli a partire da info. Tuttavia i vari tool di log non offrono
alcun supporto nell'implementazione di log consistenti. Pertanto, una buona idea consiste
nell'implementare una propria classe helper per la formattazione dei messaggi.
• permettere un corretto fine-tuning del logging delle applicazioni. Logging molto detta-
gliati sono necessari durante la fase di sviluppo del software ma non sono auspicabili per
sistemi in produzione, per via dell'impatto sulle performance, della difficoltà nel reperire
le informazioni desiderate in file di larghe dimensioni, etc. Pertanto, è pratica comune
quella di limitare il log delle applicazione in produzione al livello info.
• consentire a sistemi esterni di monitorare il file di log al fine di scoprire rapidamente
possibili condizioni di errore
Le linee guida circa l'utilizzo dei livelli di severità sono mostrate nelle tabelle 6.1 e 6.3.
1. importanti informazioni non riportate (caso in cui messaggi di info siano stati comuni-
cati con il livello debug);
2. log eccessivi (caso contrario in cui log di livello debug siano riportati come info).
i( ( LOGGER.isDebugEnabledf) ) {
logger.debug( "New dictionary: " + dictionary );
I
Dall'analisi del codice ci si potrebbe interrogare circa la necessità di includere questi blocchi
di if... In fondo, qualora un logger sia impostato ad un livello superiore di quello dell'invocazio-
ne (per esempio info), quest'ultima comunque non verrebbe riportata nel target del logger.
Sebbene ciò sia vero, c'è da notare che comunque, prima di eseguire l'invocazione, i parametri
devono essere risolti. Pertanto, qualora tra i parametri sia presente un'istruzione dispendiosa in
termini di cicli macchina, questa comunque verrebbe eseguita per poi accorgersi all'interno
della librerira che non era necessario eseguirla. Inoltre, elemento ancora più importante, ogni
logger per comprendere se una data invocazione debba essere abilitata o meno, deve navigare
nella propria gerarchia di log. Questo scorrimento di oggetti, tipicamente, richiede di risalire
fino al nodo di root. Quindi, anche qualora la gerarchia sia ridotta al minimo, comunque può
avere un impatto sulle performance. Da quanto riportato, dovrebbe risultare chiaro che ha
senso inserire apposite guardie solo per i livelli più bassi: debug e trace.
LOGGER.setUseParentHandlers(false);
6.5.6 Cercare di utilizzare una strategia di log al più alto livello possibile
Un elemento importante della strategia di log consiste nel decidere a quale livello effettuare il
log delle eccezioni. In particolare, sono disponibili le seguenti tre alternative:
• dal basso: ciò consiste nell'eseguire il log delle eccezioni nel punto in cui si manifestano;
• nel mezzo: ciò equivale a riportare le eccezioni in un livello intermedio, possibilmente
non appena maggiori informazioni siano disponibili;
• in alto: l'eccezione viene riportata nel livello più alto della catena di invocazioni.
Sebbene la prima opzione possa sembrare la migliore (strategia semplice ed efficace per cui
tutti i problemi vengono riportati nel log), essa crea una serie di importanti effetti collaterali:
Un primo tentativo di mitigare alcuni degli effetti collaterali della precedente strategia consi-
ste nell'eseguire il log in un livello intermedio. Questa strategia tuttavia continua a presentare
molti dei precedenti problemi, per quanto spesso in forma mitigata. Inoltre ne pone di nuovi.
Per esempio, tende a creare incertezze ed ambiguità relativi al giusto punto in cui effettuare il
log, richiede di comunicare informazioni aggiuntive, etc. Ovviamente, le informazioni contenu-
te nelle eccezioni possono sempre essere arricchite, alleviando parzialmente questo problema.
La strategia di riportare le eccezioni al livello più alto, da un punto di vista concettuale, è
sicuramente la migliore:
• data e ora dell'eccezione secondo il classico formato: yyyy-mm-dd hh:mm:ss mmm; onde
evitare problemi di cambiamento di ora e di fusi orari si consiglia di utilizzare sempre lo
Universal Time (per esempio: 2006-07-07 15:13:10.99);
• il livello di log;
• identificatore univoco: si tratta di un'informazione utile per semplificare il lavoro di
possibili agenti automatici demandati all'esecuzione di opportune procedure di gestio-
ne degli errori; pertanto l'assegnazione di un identificatore è necessaria esclusivamente
per gli ultimi tre livelli di log: warning, error e fatai.
• percorso completo o semi-completo della classe in cui si è verificato il problema;
• il messaggio relativo al problema verificato;
• eventuale lista con gli argomenti del problema.
Ricapitolando:
Per esempio:
Da quanto riportato risulta evidente che i primi tre attributi sono necessari per una gestione
automatizzata delle procedure di gestione degli errori, mentre i restanti sono utili per un'ispe-
zione manuale.
Qualora si decidesse di ricorrere a questa alternativa, è fortemente consigliato utilizzare clas-
si di supporto, sia per rinforzare la struttura, sia per disporre di un "registro" in cui memoriz-
zare i vari codici.
itolo 7
Test di unità
Introduzione
Il presente capitolo è dedicato all'illustrazione dei test di unità (unti test); in particolare, analo-
gamente ai capitoli precedenti, sono presentate una serie di tecniche, framework di supporto,
linee guida e best practice utili per la produzione efficace di validi test di unità
Al momento in cui viene scritto questo libro, l'ingegneria del software annovera una serie di
processi di sviluppo del software che spaziano da quelli più centrati sul codice, come XP (eXtreme
Programming) a quelli via via più formali, per esempio RUP (Rational Unified Process) e MDA
(Model Driven Architecture). Nonostante le fondamentali differenze filosofiche e pratiche alla
base dei vari processi, è comunque possibile individuare alcune aree in cui vi è unanime con-
senso: una di queste è relativa all'importanza dei test. Verifiche formali, chiaramente, devono
essere eseguite durante tutte le fasi del processo di sviluppo del software, non appena nuovi
manufatti (per esempio il modello dei requisiti utente, il disegno del sistema, etc.) diventano
disponibili. Tuttavia, il fine ultimo dei processi di sviluppo del software è realizzare sistemi
software, pertanto è fondamentale che il codice prodotto sia verificato accuratamente. Ciò è
possibile corredando il sistema con approfonditi test automatizzati.
Come si vedrà meglio nel prossimo capitolo, esistono diversi livelli di test. In questo capitolo
l'attenzione è centrata su un primo stadio di verifica fornito dai test di unità. Con questo termi-
ne ci si riferisce alle procedure utilizzate per verificare che una particolare porzione di codice
(tipicamente una classe, un componente) presentino il comportamento aspettato: in una paro-
la, si verifica che l'unità funzioni correttamente.
I test di unità, e più in generale l'intero insieme di test (unità, integrazione, sistema, etc.), sono
fondamentali non solo per esaminare il funzionamento delle varie parti del sistema al momento
della loro scrittura, ma anche per disporre di uno strumento formale che eviti un problema
fondamentale: i processi di aggiornamento del codice non devono produrre malfunzionamenti
in parti del sistema correttamente funzionanti prima dell'incorporazione degli aggiornamenti.
Disponendo di una batteria di test automatizzati, è quindi possibile eseguirla dopo ogni insieme
di modifiche (questo processo è tipicamente denominato test di regressione, regression test) al
fine di individuare, istantaneamente, eventuali errori introdotti con la nuova versione. I test di
regressione tendono a essere un formidabile supporto per contrastare la paura dei cambiamenti
che spesso si annida nella mente degli sviluppatori. Pertanto, come logico attendersi, la presenza
di questi test riduce i fattori di rischio intrinseci a ogni processo di modifica e aumenta il livello
di sicurezza del team di sviluppo. Da tenere presente che la mancanza di approfonditi test auto-
matici fa si che il codice prodotto diventi legacy già prima del suo rilascio.
Obiettivi
L'obiettivo di questo capitolo è fornire una serie di direttive relative alla produzione di efficaci
test di unità. In particolare, la maggior parte delle direttive presentate fanno riferimento al
framework JUnit che, al momento in cui viene redatto questo libro, è lo standard de facto per la
produzione di test di unità in Java.
Produrre i test di unità è un'attività indispensabile del ciclo di vita del software. La loro
esistenza offre molti vantaggi, come l'immediata individuazione di eventuali errori, l'aumento
della qualità del codice prodotto, la semplificazione del processo di refactoring, la semplifica-
zione del processo di integrazione e la produzione di documentazione supplementare. Tutta-
via, test di bassa qualità o il cui livello di copertura sia eccessivamente limitato possono produr-
re disastrosi risultati — per esempio un falso livello di sicurezza circa la qualità del sistema — e
portare alla consegna di un sistema dalla qualità piuttosto limitata, etc.
Quindi, per evitare questi effetti paradossali, è necessario porre attenzione a una serie di
fattori, come il dominio di applicazione dei test di unità, la copertura dei test, l'impatto dei
processi di sviluppo iterativi e incrementali sul processo di manutenzione dei test, etc. Questi
argomenti, chiaramente, sono oggetto di studio del presente capitolo. Mentre concetti come
per esempio test di integrazione, processo di build e relativi tool (quali Ant e Maven), integra-
zione continua, per quanto strettamente interconnessi con i test di unità, sono trattati nei ri-
spettivi capitoli.
Un po' di teoria
Con il termine test di unità ci si riferisce alle procedure utilizzate per verificare che un partico-
lare frammento di codice presenti il comportamento aspettato.
Al fine di comprendere chiaramente l'idea alla base di questo tipo di test, si consideri la pro-
duzione di schede elettroniche. Ciascuna scheda contiene una grande quantità di componenti:
resistenze, condensatori, chip, etc. Ogni singolo componente, prima di essere assemblato in
circuiti complessi, viene sottoposto a un processo di verifica. Per esempio, si verifica che i resistori
presentino la resistenza dichiarata con un'approssimazione del 3 % o 5 % (a seconda della qua-
lità della resistenza), che i condensatori presentino la capacità attesa, sempre entro certi limiti, e
così via. Pertanto, ciascun componente è verificato singolarmente, indipendentemente dagli altri
e dalla scheda in cui verrà inserito. Queste verifiche, eseguite nell'ambito dell'elettronica, sono
l'equivalente dei test di unità eseguiti nel dominio della produzione del software. In questo
dominio, l'obiettivo dei test di unità è analogo: verificare che ogni singola classe/componente
presentino il comportamento atteso, indipendentemente dal contesto di utilizzo. Chiaramente,
come si vedrà di seguito, si tratta di test necessari ma non sufficienti: il fatto che un componente
elettronico funzioni come previsto non significa assolutamente che automaticamente la scheda
montata funzioni altrettanto correttamente. La completa esecuzione senza errori dei test di unità
rappresenta l'evento di avvio dei test di integrazione che, come si vedrà nel capitolo successivo,
servono a verificare che componenti funzionanti singolarmente, una volta correttamente assemblati
tra loro diano luogo a elementi più grandi ancora funzionanti correttamente.
L'oggetto di analisi dei test di unità sono le singole classe ed i singoli componenti. Ora qualo-
ra si utilizzino dei container, come gli application server o Spring, la definizione di component
è abbastanza immediata. Quando invece si fa riferimento a pure applicazioni Java stand-alone,
l'individuazione di un componente è meno immediata. In questo caso con le dovute precauzio-
ni ed i dovuti distinguo in linea di massima è possibile considerare i package come componenti.
if (this == obj) {
return true;
it (Isuper.equals(obj)) I
return false;
I
if (getClass() != obj.getClass()) I
return false;
I
if (id == null) I
if (other.id != null) I
return false;
I
I else if (!id.equals(other.id)) I
return false;
I
if (login == null) I
if (other.login != null) I
return false;
I
I else if (Mogin.equals(other.login))
return false;
I
if (name == null) I
if (other.name != null) {
return lalse;
I
I else if (!name.equals(other.name)) I
return false;
if (middlename == null) I
if (other.mlddlename 1= null) I
return false;
I
I else if (!middlename.equals(other.middlename)) I
return false;
it (surname == null) I
il (other.surname != null) I
return false;
I
I else it (¡surname.equals(other.surname)) I
return false;
if (dob == null) I
if (other.dob != null) I
return false;
I
I else if (!tomaiDafe(dob).equals(tormafDate(other.dob)))
return false;
il (email == nuli) {
il (other.email 1= nuli) I
return false;
I
I else if (lemail.equals(other.email)) {
return lalse;
if (userStatus == null) I
if (other.userStatus != null) I
return lalse;
I
I else if (!userStatus.equals(other.userStatus)) I
return false;
I
if (addresses == null) I
if (other.addresses != null) I
return false;
I
1 else if (!addresses.equals(other.addresses)) I
return false;
if (orgUnit == null) I
if (other.orgUnit != null) (
return false;
this - - obi..
lalu:
!super.equals(obj)
^jlse
ir
getClass() '= obi gelClass()
id == null
login ==null
!login.equals{other.login)
olher.login != nuli
orgUnit — nuli
lorgllnit.equals(other orgUnit)
other.orgUnrt nuli
return true return false return false return false return false return false return false return false return false return true
Figura 7.1 - Grafo dei diversi percorsi presenti nel metodo equals.
} else il (!orgUnit.equals(other.orgUnit)) I
return false;
I
return true;
I
2 I n v o c a z i o n e del m e t o d o s u l l o s t e s s o o g g e t t o true
3 I n v o c a z i o n e del m e t o d o s u d u e o g g e t t i c o n un e l e m e n t o d e l l a c l a s s e a n t e n a t a d i v e r s o false
I n v o c a z i o n e d e l m e t o d o su d u e o g g e t t i di t i p o d i v e r s o e r e d i t a n t i d a l l o s t e s s o a n t e n a t o
-1 false
c o n gli e l e m e n t i in c o m u n e u g u a l i
I n v o c a z i o n e d e l m e t o d o su d u e o g g e t t i c o n t u t t i gli e l e m e n t i u g u a l i , e c c e t t o l ' a t t r i b u t o
8 false
l o g i n del p r i m o i m p o s t a t o a nuli
false
Tabella 7.1 - Elenco dei test dei percorsi del metodo di equals.
lungo termine, in termini produzione di sistemi di qualità superiore, che, in ultima analisi,
producono economie di tempo e budget.
Tutto il codice rilasciato, anche se non utilizzato, deve essere sottoposto a opportuni proces-
si di test. Paradossalmente, anche le funzioni meno utilizzate devono essere verificate appro-
fonditamente, proprio perché la normale pratica difficilmente potrebbe portare a individuarne
i problemi. Indipendentemente dalla strategia utilizzata per la scrittura dei test di unità, è
importante misurarne il livello di copertura. A tal fine sono disponibili una serie di tool, molti
dei quali sono illustrati all'indirizzo
http://java-source.net/open-source/code-coverage
La maggior parte di questi tool non si limita a indicare la percentuale di codice esercitata dai
test, ma fornisce una serie utilissima di dati. Tra questi ci sono anche le classi/metodi non verifi-
cati, la traccia dei percorsi analizzati, etc. Pertanto, nella produzione dei test, invece di aggiun-
gerne altri in modo randomico, è più conveniente introdurre test mirati che vadano a esercitare
proprio le parti di codice non controllate da appositi test di unità. Ciò permette di evitare un'inutile
ridondanza nella produzione di test, da evitare al fine di minimizzare il processo di manutenzio-
ne degli stessi test generato dalla variazione del corrispondente codice del sistema.
JUnit
Al momento in cui viene scritto questo libro non è possibile parlare di test di unità in Java senza
menzionare JUnit (http://www.junit.org/index.htm): nella comunità dei programmatori Java i due
concetti sono spesso utilizzati come sinonimi. JUnit, indubbiamente, appartiene di diritto alla
cerchia dei progetti open source più popolari nella comunità dei programmatori Java e non
solo. Tanta è la popolarità di questo strumento da avergli fruttato il titolo di strumento standard
de facto per la produzione di test di unità in Java. Sviluppato inizialmente da Kent Beck e Eric
Gamma nel contesto del movimento XP, Martin Fowler ha commentato JUnit in mondo
trionfalistico scrivendo che "mai, nella disciplina dell'ingegneria del software, così tanto è stato
dovuto a così poche linee di codice". Per quanto l'obiettivo di questo capitolo non sia presen-
tare in modo diffuso JUnit (esistono interi libri dedicati all'argomento, cfr. [JUNREC],
[JUNACT], e [JUNPCK]), si è ritenuto che una minima conoscenza di questo framework sia il
prerequisito irrinunciabile per la comprensione di quanto riportato di seguito.
L'introduzione del JDK5, inoltre, ha avuto un grosso impatto non solo sui package interni di
Java ma anche sulla maggior parte delle librerie fornite da terze parti. A questa regola non fa di
certo eccezione JUnit che, come si vedrà a breve, ha subito significative variazioni.
Tuttavia, poiché la versione JUnit 4, al momento in cui viene scritto questo libro, non è
ancora largamente utilizzata e poiché non sempre è possibile abbandonare il JDK4, si è deciso
di mostrare, brevemente, entrrambe le versioni.
0 package com.mb.mypackage;
1
2 thire! party imporls
3 import junit.tramework.TestCase;
4
5 public class MyTest extends TestCase I
6
7 public void testBasicTest() I
8
9 UserVO newUser = UserFactory.getUser(O);
10 assertlMotlMull(newUser);
11
12 I
Dall'analisi di tale listato è possibile evidenziare gli elementi fondamentali delle classi di test:
• la classe di test doveva necessariamente ereditare dalla classe TestCase (linea 5);
• il nome dei metodi di test doveva necessariamente iniziare con il suffisso test (linea 7), al
fine di poter essere riconosciuti come tali tramite i meccanismi della riflessione (package
java.lang.reflect);
• all'interno dei vari metodi di test era necessario utilizzare le varie versioni dei metodi
assert, come per esempio assertNotNull (linea 10).
Oltre questi elementi base era possibile definire del comportamento da invocare, rispettiva-
mente, all'atto dell'avvio del test (setllpQ) e alla conclusione (tearDownQ). Metodi utili sia per
allocare e rilasciare risorse (come per esempio connessioni al database), sia per inizializzare og-
getti successivamente verificati. Pertanto, questi metodi permettono di evitare notevoli
duplicazioni di codice. In effetti, se non ci fossero sarebbe necessario, per ogni test, riportare il
codice necessario per impostare le condizioni necessarie per l'avvio dei test stessi e per la chiusu-
ra pulita, soprattutto in caso di errore. Il listato riportato di seguito mostra un esempio di utilizzo
dei metodi di setUp and tearDown. In particolare, la procedura di setUp si occupa di inizializzare il
"componente" del business Service layer da verificare (UserBS). Questo, a sua volta, utilizza tre
"componenti" del livello inferiore: business object. Poiché l'obiettivo del test è verificare il com-
ponente e non l'integrazione di diversi layer, le classi del livello BO sono sostituite da opportuni
mock up (oggetti che implementano la stessa interfaccia del componente vero, ma che presenta-
no implementazioni semplici). Il metodo di chiusura si occupa di riportare metodi non invocati
: se si definisce il mock up di un metodo è legittimo attendersi che questo debba essere invocato.
orgllnitBO = EasyMock.createMock(IOrgUnitBO.class):
profileBO = EasyMock.createMock(IProfileBO.class);
userBO = EasyMock.createMock(IUserBO. class);
userBS.setUserBO(userBO);
userBS.setProfileBO(profileBO);
userBS.setOrgl)nitBO(orgUnitBO);
this.userBS = userBS;
I
Una buona pratica di programmazione consiste anche nello scrivere una classe (TestSuite) atta
a invocare le varie classi di test. Lo scheletro di tale classe assume una forma del genere:
// CONSTANTS SECTION
// ATTRIBUTES SECTION
// METHODS SECTION
/ "
suite.addTest(testToRun[ind]);
I
return suite;
JUnit versione 4
La versione 4 del framework presenta alcune significative differenze dovute principalmente
all'inclusione delle annotazioni introdotte con la versione Java 5.0.
Si tratta del m e t o d o destroy della classe Thread, izialmente ideato per s o p p r i m e r e i thread in
maniera brusca non consentendogli neanche di rilasciare eventuali monitor. Per questa ragione
l'implementazione di questo m e t o d o rappresenta un ottimo sistema per generare dead-lock.
Il listato seguente mostra l'adattamento dell'esempio riportato nel primo listato, modificato
in base alle direttive della versione JUnit 4.
0 package com.mb.mypackage;
1
2 //
. third party imports
3 import static org.junit.assert.assertNotNull;
4 import org.junit.Test;
5 public class MyTest I
6
7 ©Test public void BasicTest() I
8
9 UserVO newllser = UserFactory.getUser(O);
10 assertNotNull(newllser);
11
12 I
In particolare:
Qualora poi, si voglia far sì che la versione 4 del framework JUnit esegua correttamente alcune
classi di test scritte per una versione precedente, è necessario includere il seguente metodo:
public static junit.framework.Test suite() {
return new JUnit4TestAdapter(<nome della classe di testxclass);
L'uso delle annotazioni, come lecito attendersi, non è limitato ai soli metodi di test, ma è
utilizzato in altri contesti. Per esempio, è possibile utilizzare le annotazioni @Before e ©After,
contenute nei package org.junit.Test.Before e org.junit.Test.After, per definire comportamento da
eseguire, rispettivamente, prima e dopo l'esecuzione di ogni metodo di test. Queste annotazio-
ni vanno premesse a opportuni metodi di inizializzazione e rilascio dei test, ed è possibile defi-
nirne quanti se ne vuole. Da notare che qualora si decida di implementare una classe antenata
di test, tutte le classi figlie erediteranno i vari metodi @Before e ©After.
Nella versione JUnit 4 è possibile dichiarare delle annotazioni, @BeforeClass e @AfterClass, che
sono invocate una sola volta per tutti i test. Coerentemente con la relativa definizione è possibi-
le specificarne una sola coppia per classe.
Per terminare questa sezione, è importante considerare che l'annotazione @Test permette di
definire diversi parametri, come per esempio:
Un'altra annotazione degna di nota è @lgnore. Questa, come lecito attendersi, permette di
ignorare un test e di includere una stringa che riporti la motivazione dell'esclusione.
Direttive
7.1 Investire nei test di unità
Sebbene questa regola possa sembrare scontata, si continuano a trovare sistemi privi di test di
unità. La mancanza di tali test, inevitabilmente, conferisce minore robustezza al codice, rende
più rischiosi i processi di aggiornamento e quindi riduce la voglia/possibilità di eseguire pro-
cessi di refactoring, aumenta il grado di difficoltà dell'attività di integrazione, e spesso ne ridu-
ce la comprensibilità. Per alcuni tecnici, un sistema non dotato di test automatici è un sistema
non funzionante per definizione.
Indipendentemente dall'impegno profuso nell'attività di disegno del sistema e dalla metico-
losità utilizzata nello scrivere il codice, è sempre possibile/frequente commettere errori. Per-
tanto la presenza di test automatizzati e quindi ripetibili fa spesso la differenza tra codice robu-
sto e non.
Test complesso
@Test
public void testABCQ I
// inizializzazione dei singoli
// test. Diverso da setUP!
assertTrue(conditionl);
assert l\lotl\lull(o);
assertEquals(o1 ,o2);
I
Test sempltci
@Before
public void testlnit() {
I
@Test
public void testCondition! () I
assertTrue(conditionl);
I
@Test
public void testCondition2() I
assertNotNull(o);
@Test
public void testCondition3() I
assertEquals(o1,o2);
]
Alcuni di questi strati, poi, sono ulteriormente composti da sottostrati; lo strato di integrazio-
ne tipicamente è suddiviso per le varie sorgenti integrate: database, sistema di messaggistica, etc.
Inoltre, sono presenti ulteriori componenti atti a ospitare elementi base, come eccezioni comuni,
value object utilizzati per scambiare informazioni tra i vari strati dell'architettura etc. Pertanto è
opportuno far sì che tutti i test degli elementi di macro-componenti (spesso anche di importanti
package) siano invocabili da un'opportuna classe "suite di test". Questa, a sua volta dovrebbe
essere inclusa in una suite a livello più generale fino a giungere a quella di livello globale.
Da notare che le singole classi di test (elementi foglia) e le varie suite (elementi composti)
sono state implementate secondo le direttiva del Composite Pattern e quindi è possibile avere
un numero illimitato di classi "suite di test".
7.2.5 Assegnare alle classi di test lo stesso nome delle classi testate
Come visto in precedenza, nel contesto dei test di unità, ogni classe di test dovrebbe verificare
il comportamento di una classe del sistema. Pertanto, la convezione utilizzata per i nomi delle
E] ^ ^ main
0 _ j java
• 2 ) com
B mokabyte
B ^dataioader
B ^ base
B 3 V 0
Q DataTableVO.java
Q DataRowVO.java
Q ColumDescrVO.java
B test
02 i ava
• com
B mokabyte
B ' ^jdataloader
B base
B 3 V 0
Q DataTableVOTest.java
Q DataRowVOTest.java
Q ColumDescrVOTest.java
Il listato di seguito riportato presenta due semplici test, il primo negativo e il secondo positi-
vo. In particolare, il primo serve a verificare che a fronte di una richiesta relativa a un utente non
presente nel sistema (il login specificato non appartiene ad alcun utente), il sistema sia in grado
di rilevare l'anomalia e quindi lanci un'opportuna eccezione. Il secondo test invece verifica che
qualora tutti i dati siano corretti, il sistema sia in grado di autenticare l'utente specificato.
/ * * Default Constructor */
public AuthenticationServiceTest() I
authenticationService = new AuthenticationServiceQ;
authenticationService.setProfileBO(profileBO);
authenticationService.setUserBO(userBO);
I
/**
profileBO.findActiveProfileByLoginldfinvalid");
EasyMock.expecf/.asfCa//().andReturn(null);
userBO.findRefl)serByLogin("invalid");
EasyMock.expecf/.as/Ca//().andReturn(null);
replayf);
try {
authenticationService.authenticate(createSubject("invalid"));
fail(" MySystemSecurityException should be thrown");
I catch ( MySystemSecurityException e) I
// ignore this exception
assert 7rt/e(true);
I
" This method verifies that the service, in case everything is correct
" is able to correctly deliver the authentication service
' @throws Exception a problem occurred during the process
profileBO.findActiveProfileByLoginld(correctUser.getLogin());
EasyMock.expectLastCall().andReturn(correctProfile);
replay();
try I
authenlicationService.authenticale(
createSubject( correctUser.getLogin()));
) catch I (MySystemSecurityException e) I
fail("MySystemSecurityException should not be thrown");
I
• Disporre di una valutazione quantitativa del livello di copertura dei test. Per alcuni tec-
nici (estremisti) questi indici, indirettamente, costituiscono una misura del livello di qualità
del sistema. Di sicuro c'è la necessità di aver ben chiaro il livello di affidabilità dei test di
regressione e, in ultima analisi, il valore del superamento dei test di unità. Non è infre-
quente infatti che i team di sviluppo abbiamo la falsa sensazione di aver prodotto un
codice di alta qualità giacché il sistema passa indenne i vari test anche quando questi
coprono esclusivamente una piccola percentuale del codice.
• Individuare aree di codice non testate sufficientemente e che quindi beneficerebbero
dall'aggiunta di nuove classi di test. Questo evita inutile dispendio di tempo e denaro
derivante dall'aggiunta di test in modo randomico quando invece occorre aggiungere
test solo per affrontare le aree effettivamente non coperte opportunamente.
• Individuare test ridondanti che risultano deleteri nel momento in cui sia necessario ese-
guire delle operazioni di refactoring che necessariamente si riflettono anche sulle classi
di test. Quindi, maggiori sono le classi di test, maggiore è l'impegno richiesto dai proces-
si di refactoring.
• di test, per i quali esistono gli appositi metodi; il problema principale è che qualora si
verifichi un problema nel metodo costruttore, questo verrebbe riportato in termini di
un'eccezione invece che del fallimento di una assertion;
• di procedure di inizializziazione dei test; a tal fine sono stati predisposti opportuni me-
todi di setup.
/ * * users data */
public static final Object USERS_DATA[] [] = I
[ "1", UserSlatusDTO.USER_ACTIVE, "123435467", "VeraD" ,
"Vera", "Grigorievna","Dianova", "22-02-1982",
"VG@Dianova.com" ."VeraGD","2342342", 10-01-2006', "10-01-2008" I,
I "2", UserStatusDTO.US£ÌMC77l/£, "454564564", "MarcoM",
"Marco", nuli ."Materazzi", "19-08-1973",
"MarcoMaterazzi@WorldCupChampion2006.com", "MarcoB", "02033423",
"12-02-2006", "15-05-2008" I,
I;
i"
' Return a pre-set user
* @param index index ot the requested user
' ©return the requested user
if userDTO
( (index >= -1) && (index < USERS,DATA.Iength) ) {
new UserDT0(
new Long((String)USERS_DATA[index] [0]), // id
(UserStatusDTO) USERS_DATA[index] [1], // status
(String)USERS_DATA[index] [2], // gpn
(String)USERS_DATA[index] [3], // login
(String)USERS_DATA[index] [4], // name
(Strlng)USERS_DATA[index] [5], // middlename
(String)USERS_DATA[index] [6], // surname
getReqDate((String)USERS_DATA[index] [7]), // dob
(String)USERS_DATA[index] [8], // e-mail
(String)USERS_DATA[index] [9], // nick name
(String)USERS_DATA[index] [10], // off tel
getReqDate((String)USERS_DATA[index] [11]), // start
getReqDate((String)USERS_DATA[index] [12]) //end
);
userDTO.addAddress(
new UserAltAddressDTO(
null, UserAltAddTypeDTO.ADDRESS_HOME, "Home Address"));
userDTO.addAddress(
new UserAltAddressDTO(
n u l l , UserAltAddTypeDTO.ADDRESS_MOBILE, "Mobile phone"));
userDTO.addAddress(
new UserAltAddressDTO(
null, UserAltAddTypeDTO.ADDRESS_PAGE, "Pager"));
I
return userDTO;
)
@Test(expected=lndexOutOfBoundsException.class)
public void testlndexOutOfBoundsException() I
ArrayList emptyList = new ArrayList();
Object o = emptyList.get(O);
I
Il seguente metodo di test ha successo solo in caso in cui il sistema lanci l'eccezione attesa (lo
statement catch contiene un assertTrue(true)) mentre fallisce se il servizio non la lancia. Infatti,
dopo l'istruzione successiva all'invio dell'eccezione c'è una fail.
' This tesi veiifies that it an invalid login
1 is specified, then the s y s t e m t h r o w s a proper
" exception
protileBO.tindActiveProfileByLoginld("invalid");
EasyMock. expectLastCallQ. andReturn(null);
userBO.findUserByLoginfinvalid");
EasyMock.expectLastCall().andReturn(null);
replay();
try I
authenticationService.authenticate(createSubject("invalid"));
fail("SecurityException should be thrown");
assertTrue(lrue);
I
I
A
public slatic Test suite() {
suite.addTest(new FirstTestCase("TestToPer1ormFirst"));
suite.addTest(new
return suite; SecondTestCasefTestToPerformSecond"));
• altri test potrebbero fallire in quanto lo stato del sistema è diverso da quello atteso;
• tipicamente è necessario intervenire manualmente per porre nuovamente il sistema in
uno stato neutro.
• significativo risparmio di tempo e quindi di budget (per esempio è più facile e veloce
individuare, e quindi correggere, i malfunzionamenti del sistema);
• drastica riduzione dei costi dovuti ai difetti;
• miglioramento del livello qualitativo del sistema.;
• aumento del livello di soddisfazione degli utenti
Obiettivi
L'obiettivo di questo capitolo è fornire una serie di suggerimenti utili e linee guida relative alla
delicata fase dei test di integrazione. Come visto in precedenza, si tratta di un'attività che,
almeno logicamente, si colloca tra i test di unità e quelli di accettazione da parte degli utenti.
Pertanto, questo capitolo rappresenta la logica continuazione di quello precedente sia perché
almeno una buona parte dei test di integrazione sono a carico del team di sviluppo, sia perché
molti dei suggerimenti presentati nel capitolo percedente mantengono la loro validità anche in
questo contesto.
I test di integrazione, presentano inoltre molte analogie con gli UAT (che in effetti rappre-
sentano il test di integrazione finale, in un ambiente molto simile a quello di produzione con
firma finale di accettazione da parte degli utenti). Questo perchè si inizia con il verificare che
un certo numero di classi/componenti, una volta assemblate insieme, funzionino correttamente
e si procede ad assemblare fino a giungere alla verifica dell'intero sistema, verifica che è oggetto
fondamentale degli UAT. Pertanto, anche se i test di integrazione, almeno nelle sue versioni
finali, somigliano molto agli UAT, esistono importanti differenze, quali:
• principali utenti: nel primo caso questi sono responsabilità del team di sviluppo (anche
se questa responsabilità può essere delegata) mentre nel secondo si tratta di una rappre-
sentanza degli utenti finali;
• strategia: i test di integrazione devono copiare da vicino l'andamento del progetto;
• finalità: promozione all'ambiente di staging nel primo caso, accettazione da parte degli
utenti nel secondo.
L'argomento dei test di integrazione è molto vasto, ma qui viene trattato solo limitatamente
alle esigenze minime del team di sviluppo. In particolare, l'attenzione è focalizzata sui test di
integrazione interni al team di sviluppo (IST, Internai, figura 8.1). Alcuni suggerimenti presenti
in questo capitolo finiscono per essere più di carattere manageriale che implementativo. Tutta-
via si è deciso di inserirli lo stesso, sia per favorire una migliore comprensione dell'argomento,
sia per fornire ai lettori una serie di linee guida utili per i propri progetti.
1ST
Integration System Test i
F u n c t i o n a l 1ST N o n f u n c t i o n a l IST
UAT
j I 1ST-Development j 1ST - P e r f o r m a n c e 1ST-Stress Test
User A c c e p t a n c e e s t
Sanity Check
Alcuni tool
Presentiamo brevemente alcuni tool, come punto di partenza, demandando ai lettori ulteriori
approfondimenti. In primo luogo JUnit mantiene completamente la propria validità. Tuttavia,
in questo contesto la semplicità che lo contraddistingue finisce per essere un problema: si tratta
comunque di un tool nato per implementare i test di unità. Per sopperire a questa carenza di
feature, esiste tutto un fiorire di framework/estensioni il cui scopo è risolvere specifici proble-
mi. I tool più interessanti sono:
Un altro tool concepito come estensione di JUnit è FIT (http://fit.c2.com/). Tuttavia in questo
caso si tratta di un vero e proprio tool la cui peculiarità risiede sulla logica di funzionamento
basata su tabelle HTML. Ciò lo rende un valido strumento anche per l'implementazione di
UAT. In particolare, il tool richiede che ad ogni tabella corrisponda un'implementazione nota
come "correzione" (fixturé) scritta dagli sviluppatori. La tabella è organizzata nel seguente modo:
Un altro tool degno di nota è Cactus. Si tratta di un framework di test disegnato apposita-
mente per verificare componenti web come Filters, pagine J S P e Servlet supportando il test
all'interno dei container. Come tale si presta a verificare — indirettamente (qualcosa dovrà pur
generare i dati da mostrare in pagine HTML!) — sia componenti EJB, sia tag libraries.
Un tool particolarmente efficace nella realizzazione di test automatici per thin UI è Selenium
(http://selenium.openqa.org/). I test implementati con Selenium girano direttamente nei browser
simulando fedelmente un utente reale. Il principio di funzionamento si basa su una serie di
JavaScript e ¡Frames al fine di includere il motore di automatizzazione dei test nel browser.
Infine, come lecito attendersi, questa è la fase in cui la necessità di implementare classi mock
raggiunge i massimi livelli. Pertanto, tool quali EasyMock, MockEJB, Mock Objects, Mock
Creator, jMock, Mock Runner, e chi più ne ha più ne metta, trovano largo utilizzo.
Direttive
8.1 Investire negli use case e test case
Sebbene dovrebbe essere ormai chiaro il motivo per cui sia importante investire nei test di
integrazione, alcuni lettori potrebbero trovare l'associazione con i casi d'uso non immediata;
tuttavia, come riportato di seguito, i due manufatti sono strettamente correlati. In effetti
[TSTDSG] mentre i casi d'uso descrivono esattamente cosa dovrebbe fare il sistema, soprat-
tutto dal punto di vista dei servizi da fornire, i test case definiscono le procedure necessarie per
verificare che il sistema implementi correttamente quanto dichiarato nei casi d'uso. Inoltre,
qualora gli use case siano chiari, completi e accurati, il processo di scrittura dei test case risulta
assolutamente meccanico [UMLING], mentre, negli altri casi, la loro generazione permette di
individuare lacune, imprecisioni e aree ambigue. Quindi la redazione dei test case rappresenta
un'ottima occasione per effettuare una revisione formale dei casi d'uso. Infine, i casi d'uso sono
indubbiamente una delle migliori notazioni per analizzare e documentare i requisiti funzionali
dei sistemi, tanto da essersi agiudicato lo status di standard de facto. Una caratteristica che li
rende veramente unici consiste nel favorire l'analisi e la documentazione sistematica dei requi-
siti di correttezza e robustezza del sistema. I primi sono assicurati dalla presenza dello scenario
principale e di quelli alternativi, ossia quelli in cui tutto funziona correttamente e pertanto il
sistema è in grado di erogare correttamente il servizio. Il requisito di robustezza del sistema (in
termini di servizi business) è ottenuto per mezzo degli scenari di eccezione. Questi descrivono
sia le condizioni dei casi anomali, sia le misure che il sistema deve attuare per gestirli.
La logica consequenza è che, dato un insieme di casi d'uso, è possibile derivare direttamente
e meccanicamente i relativi test case. Questi, a loro volta, rappresentano ottimi requisiti per
implementare sistemi automatici per testare il sistema. Tali sistemi rappresentano ottimi ausili
per ogni nuova release del sistema che può essere sottomessa ad un test esaustivo in grado di
neutralizzare immediatamente un gran numero di eventuali malfunzionamenti. Da notare che i
requisiti funzionali restano la principale fonte di informazione per il test del sistema.
Come spesso accade in presenza di approcci diametralmente opposti, come i due testé espo-
sti, è possibile utilizzare approcci ibridi. In questo caso tale approccio è spesso denominato a
ombrello per via del fatto che ci si focalizza su specifiche aree (quelle "coperte dall'ombrello").
Si tenta di beneficiare dei vantaggi forniti da entrambi gli approcci, mitigandone gli svantaggi.
In particolare, si cerca di sviluppare appena possibile determinati servizi/flussi di dati partendo
dal basso. I vantaggi sono che si conferisce enfasi agli scenari più importanti/critici, la possibi-
lità di dar luogo a iniziali consegne, il minimizzare la necessità di implementare oggetti di mock
up, etc. D'altro canto però, c'è sempre un certo ritardo nell'iniziare a verificare i principali
servizi, si ha una ridotta enfasi nella realizzazione dell'architettura, etc.
8.4.2 Allocare e mantenere un sufficiente lasso temporale
Questa regola è una sorta di riassunto di quanto espresso in precedenza. I test di integrazione
rappresentano una fase importantissima del processo di sviluppo del software. Pertanto è di
fondamentale importanza assicurarsi di disporre di un tempo sufficiente per eseguire questi
test. Spesso accade che il tempo allocato sia decisamente sottostimato o che questo venga
depauperato dai ritardi di sviluppo finendo con l'annullare tutti i vantaggi della strategia di test.
Inoltre, mentre i processi tradizionali prevedevano che questa fase fosse eseguita solo alla fine
dell'implementazione, quelli più moderni, iterativi e incrementali, prevedono che i test siano
eseguiti svariate volte, almeno alla fine di ogni iterazione. L'implementazione di test di integra-
zione automatici ovviamente fornisce un validissimo supporto.
8.4.3 Assicurarsi che i test di integrazioni evolvano pari passo con il progetto
Questa regola è una logica conseguenza delle strutture sempre più iterative e incrementali dei
processi moderni. Pertanto è molto importante che la strategia di test (ed eventualmente il
relativo sistema) sia assolutamente affine a quella del progetto. Inoltre, la sua pianificazione
deve seguire passo passo quella del progetto al fine di eseguire tutti i necessari aggiustamenti al
piano per salvaguardare il tempo necessario.
Infine, è necessario che la strategia di test tenga conto delle diverse versioni del sistema e
delle relative pianificazioni. Per esempio, se la versione O.x del sistema è orientata al rilascio
parziale dei servizi x e y, allora il relativo test di integrazione, per la stessa data, deve prevedere
che tali versioni di servizi siano verificabili. Inoltre, è importante assicurarsi di rivedere i test in
funzione del rilascio: questo perchè in un rilascio successivo potrebbe rendersi necessario cor-
reggere il sistema, per esempio i servizi x e y vengono rilasciati nella loro interezza e quindi i
test devono prevedere le necessarie correzioni.
• il corretto funzionamento con input corretti (stimoli ammissibili per il determinato stato)
• la capacità del sistema sia di riconoscere input non corretti e sia di intraprendere le
procedure di gestione previste.
Per quanto attiene al primo punto, c'è da notare che un test esaustivo è molto oneroso e
spesso impossibile. Questo è il caso in cui la macchina a stati preveda dei cicli.
Tuttavia, è consigliabile rappresentare l'evoluzione dell'entità attraverso una matrice in grado
di rappresentare il prodotto cartesiano (insieme stati x insieme eventi) —> stato. Ciò va operato
tenendo conto sia del fatto che nell'insieme degli stati vanno considerati anche uno o più stati di
errore, sia che è assolutamente necessario definire l'array degli stati iniziali (eventualmente ridot-
to a un elemento). Ciò rende possibile implementare un'applicazione di test in grado di proces-
sare la matrice al fine di generare i necessari test di integrazione, eliminando cicli infiniti.
• permette di avere un build sempre corretto e quindi ad avere un sistema sempre pronto
ad essere installato;
• evita che il processo di build diventi un evento completamente aleatorio in cui l'unica
certezza sia il momento di avvio del processo stesso, mentre la corretta esecuzione ed il
tempo necessario risultino completamente imprevedibili;
• facilita l'evoluzione del progetto: l'aggiunta di nuovi componenti, la modifica o rimozione
di altri diviene un'attività di normale amministrazione;
• semplifica la comprensione dell'organizzazione dei progetti, soprattutto di quelli più
complessi. Ciò, ovviamente, riduce la curva di apprendimento del progetto e quindi
facilita l'allocazione dinamica di risorse umane/sotto progetti;
• migliora e semplifica la comunicazione tra i diversi team di sviluppo e tra le diverse
figure professionali (project manager, business analyst, architetti, sviluppatori, etc.)
coinvolte nello sviluppo di un tipico sistema software. Inoltre rimuove alla basa la necessità
di tutto un insieme di comunicazioni, guerre religiose, relative al modo migliore di
organizzare i progetti;
• permette di minimizzare il tempo speso nella gestione dei vari ambienti e nella
manutenzione di applicazioni complesse: ciò si ottiene riutilizzando script di compilazione,
build, test, file di configurazione, etc.
• supporta la produzione di applicazioni di migliore qualità ed in particolare permette di
avere un sistema sempre complete mante verificato, evitare l'errata allocazione di file o
la mancanza di produzione di importanti file, e così via;
• semplifica e supporta la standardizzazzione delle strategie di CCM (Change Control
Management, gestione del controllo dei cambiamenti)
Obiettivi
Obiettivo di questo capitolo è fornire una serie di linee guida, suggerimenti e orientamenti
molto pratici su come organizzazione razionalmente i progetti software. In particolare, l'atten-
zione è stata focalizzata sui seguenti tre elementi fondamentali:
• compatibilità con i principali processi di sviluppo del software, a partire da quelli più
leggeri, centrati sul codice, fino ad arrivare a quelli più formali;
• scalabilità, intesa come possibilità di utilizzare quanto esposto sia per progetti di modeste
dimensione, sia per progetti molto grandi, che prevedano team di produzione dislocata
in diverse aree geografiche;
• razionale organizzazione, basata su poche linee guide, semplifica il relativo utilizzo senza
aver bisogno di dover sempre leggere la relativa documentazione,
• vantaggi e vincoli offerti dai principali tool utilizzabili in questo spazio.
Sebbene coerentemente con l'impostazione di questo libro, la presentazione dei principali tool
utilizzabili per l'automazione del processo di build non sia argomento di questo capitolo, tale è
l'importanza ed il successo di alcuni di loro, come Ant e Maven, che una loro trattazione, sebbene
minima, sia inderogabile. Questi, in effetti, offrono tutta una serie di vantaggi e vincoli da tener
presente per poter organizzare razionalmente i propri progetti. Per esempio, sarebbe probabil-
mente poco intelligente, definire una struttura del file system dei progetti senza tener conto della
struttura "standard" proposta da Maven, così come sarebbe poco furbo organizzare un progetto
senza considerare la necessità di dover pubblicare i manufatti generati. Quindi, gioco forza, que-
sti tool esistono e giocano un ruolo di primaria importanza da imporne la relativa considerazione.
Prima di proseguire con la presentazione dei due tool, tuttavia, è necessario definire cosa si
intenda con il termine di processo di build. Nella sua eccezione più essenziale rappresenta
l'insieme di attività che permettono di convertire un insieme di codici sorgente nei corrispon-
denti codici eseguibili. Questa definizione, abbastanza semplice (utile proprio per questo mo-
tivo), non tiene però conto del fatto che linguaggi moderni come Java, combinano i file "com-
pilati" in insiemi più complessi (.jar, .war, .ear) che non necessariamente possano essere eseguiti
direttamente (come per esempio librerie), e che comunque la relativa esecuzione richieda, ri-
spettivamente, l'esecuzione della macchina virtuale, il deployment all'interno di container come
web server e application server...
Inoltre, le visioni di diverse figure professionali non sempre convergono circa le responsabi-
lità del processo di build. Infatti, dal punto di vista dei molti sviluppatori, un processo di build
dovrebbe limitarsi a:
Altre figure professionali, come per esempio il personale addetto al deployment, tendono ad
avere maggiori esigenze. In particolare i relativi requisiti possono prevedono la necessità di:
Struttura
Poiché l'illustrazione dello strumento Ant esula dagli obiettivi di questo capitolo, si è deciso di
limitare l'esposizione alla presentazione di un semplice esempio di file di build (denominato
progetto, project), corredato dalle necessarie spiegazioni, demandando al lettore l'approfondi-
mento dell'argomento e dei comandi (chiamati task) messi a disposizione dei programmatori
per redigere versioni più complesse.
Gli script Ant sono scritti in XML. Sebbene inizialmente tale scelta sembrò essere la miglio-
re, con il passare del tempo sono emersi diversi problemi. Lo stesso autore, James Duncan
Davidson, ha scritto "In retrospettiva, e molti anni dopo, XML, probabilmente, non fu una
scelta poi così buona come sembrò allora. Attualmente, ho visto file build Ant lunghi centinaia
o, addirittura migliaia di linee e, con queste dimensione, si evidenzia come XML non sia poi un
formato così "friendly" da maneggiare, come sperai. Inoltre, quando si mischiano XML e gli
interessanti elementi basati sulla riflessione interni di Ant che ne permettono una facile
estensibilità definendo propri task, si ottiene un ambiente che ha la potenza e la flessibilità dei
linguaggi di script, ma che anche dà nel suo complesso un certo del mal di testa nel tentativo di
esprimere questa flessibilità attraverso delle parentesi angolari".
Questi file XML, tipicamente ma non necessariamente, realizzati per automatizzare i processi di
build, vengono eseguiti dal tool Ant, invocabile da linea di comando (c:\prj\myproject\build> ant).
Qualora non si specifichi alcun parametro sulla linea di comando, Ant, per default, cerca il file
build.xml e se lo trova esegue il target (questo elemento è spiegato in dettaglio di seguito) di default.
11 tool Ant prevede tutta una serie di parametri illustrati in dettaglio nella relativa documentazione.
In Ant un concetto fondamentale è quello di task, che, come ne suggerisce il nome, rappre-
senta un comando o "codice" che può essere eseguito. La relativa dichiarazione richiede, ovvia-
mente, opportuni elementi xml. In particolare, la struttura generale è:
La riga successiva mostra un esempio di utilizzo del comando per la cancellazione dei file:
dove delete è il nome del task, file è un attributo che dichiara il file da eliminare, e failonerror è
l'attributo che specifica se interrompere o meno l'esecuzione dello script in caso di errore (in
questo caso no).
Ant, come lecito attendersi, dispone di una miriade di task predefiniti, vi sono per esempio
task per compilare sorgenti, per creare file .jar, .war, .ear, per interagire con il file system, per
manipolare file zip/tar, per cambiare i diritti di accesso di file e directory, per interagire con
sistemi come CVS, ClearCase, e così via. Altri task Ant sono continuamente sviluppati da terze
parti e donati ad Apache. Inoltre, è possibile definire propri task. Il sito Ant fornisce tutte le
informazioni necessarie per generare ulteriori task, nonché i codici sorgenti di molti task
predefiniti. Di seguito sarà riportato il listato con un esempio di progetto:
<?xml version="1.0" encoding="UTF-8"?>
<project name="myProject" basedir="..V." default="all">
<description>
This buildfile is used an example for myProject within the XYZProject
</description>
</project>
Il primo elemento che si incontra, dopo la definizione del file XML, è l'elemento progetto
(project) dotato di tre attributi:
1. name: attributo non obbligatorio, che permette di dichiarare il nome del progetto;
2. default: rappresenta la destinazione (target) di default, ossia il target da utilizzare quando
nell'invocazione da linea di comando non sia presente alcun target;
3. basedir: directory di base dalla quale sono derivate tutte le altre definite successivamente.
Tale definizione può impostata anche attraverso la proprietà (property) "basedir". In tal
caso, questa impostazione dovrebbe essere rimossa dalla sezione project. Qualora lo script
non preveda alcuna definizione basedir, la directory "genitore" viene assunta come
directory base.
Nel listato visto poco sopra, il nome del progetto è myProject, la directory di default è A. e
ciò accade perché si suppone che lo script Ant sia memorizzato nella directory myproject\build\ant
e quindi la directory di default coincide con quella del progetto, mentre il target di default è
ali. Sempre opzionalmente è possibile specificare un elemento (description) con la descrizione
del progetto.
La sezione successiva è relativa alla proprietà. Ogni progetto può dichiarare un insieme
qualsiasi di proprietà attraverso il task property. Volendo è possibile impostarle al di fuori di
Ant. Una proprietà è data dalla classica coppia (nome, valore) dove, secondo tradizione Java, il
nome è case-sensitive (pertanto libdir è diverso da libDir). Una volta definite le varie proprietà, è
possibile utilizzarle riportandone il nome tra i terminatori: $| e ). Alcuni esempi di proprietà
utilizzate nel listato sono:
La prima definisce il path dei sorgenti Java, mentre la seconda definisce il nome del file di
distribuzione. Si tratta di .war molto utilizzati nei deployment di applicazioni all'interno di web
server. La proprietà src è usata nel seguente attributo del task di compilazione illustrato di seguito.
Ant, come è lecito attendersi, dispone della seguente lista di proprietà predefinite (built-in):
Il passo successivo consiste nel definire l'elemento target. In particolare, ogni progetto deve
averne almeno uno, anche se è prassi definirne diversi, come per esempio uno per
l'inizializzazione, uno per la compilazione, uno per la produzione del file jar, .war, .ear, uno per
la rimozione dei file intermedi generati, uno che includa tutti, etc.
Un target può includere diversi task atti ad eseguire un ben definito compito. La forma più
semplice prevede di dichiararne semplicemente il nome. Questo è l'unico attributo obbligato-
rio ed è molto importante perché è quello invocabile dalla linea di comando. Tuttavia è possibi-
le definirne eventuali dipendenze. Nel listato visto poco sopra, è definito il seguente target ali
che rappresenta una sorta di standard:
Come si può notare questo target attraverso l'attributo depends dichiara le seguenti dipenden-
ze: init, compile, war e term, nonché l'ordine di esecuzione (da sinistra verso destra). Questo,
tuttavia, potrebbe essere alternato dalle dipendenze presenti nei singoli target. Per esempio, se
per qualche motivo imprecisato, init avesse al suo intero una dipendenza dal target term, allora
ecco che quest'ultimo sarebbe il primo task ad essere eseguito. Un target può includere nella
propria dichiarazione condizioni che ne vincolano l'esecuzione (solo del target in cui sono defi-
nite e non di eventuali che ne hanno dichiarato una dipendenza). In particolare, è possibile
specificare le seguenti clausole:
if = "proprietà-presente"
unless = "proprietà-presente".
La prima specifica che il target deve essere eseguito solo se la relativa proprietà è stata impo-
stata nella linea di comando (clausola -D), mentre la seconda specifica l'opposto.
Per finire, target prevede la possibilità di specificare l'attributo description che, come lecito,
attendersi, permette di associare una breve descrizione al target stesso. Il progetto presentato
nel listato visto sopra prevede i seguenti target:
• ali: rappresenta una sorta di main, il cui compito è di far eseguire, nel corretto ordine,
tutti i target che permettono, a partire dai file sorgenti, di produrre il file di distribuzio-
ne (0 war). In questo progetto non è stata inclusa l'attività di produzione della documen-
tazione JavaDoc.
• init: permette di eseguire una serie di attività propedeutiche allo svolgimento di altri
target. In questo caso vi è il task tstamp (si tratta di una pratica fortemente consigliata) che
si occupa di impostare le proprietà DSTAMP, TSTAMP e TODAY, contenti informazioni di
carattere temporale. Inoltre, vi sono i comandi necessari per eliminare il precedente file
di distribuzione (in questo caso myProject.war), per eliminare e quindi ricreare la directory
con i file .class.
• compile: questo target si fa carico della compilazione dei file sorgente. A tal fine utilizza il
task javac. La configurazione vista nel listato include sia delle librerie esterne, sia librerie
(.jar) parte dello stesso progetto, ossia file di distribuzione generati da altri file Ant.
Questa soluzione non è esente da diversi problemi e anche per risolvere simili problemi
è stato creato il tool Maven.
• war: questo target permette di generare il file di distribuzione. La peculiarità dei file war è
di includere le librerie (.jar) referenziate dal progetto in questione. A tal fine è necessario
utilizzare l'attributo lib. Come si può notare, non disponendo di repositories, la gestione
delle librerie deve avvenire manualmente con tutti i problemi che ne seguono a partire
dalla definizione del corrispondente file system.
• term: si occupa delle pulizie finali, ossia della rimozione dei file temporanei.
A questo punto si dovrebbe disporre del minimo insieme di informazioni per comprendere
sia come funzioni Ant, sia come leggere e/o creare file Ant.
Maven
Dopo aver letto la breve presentazione di Ant riportata nei paragrafi precedenti, la lettura di
questa presentazione di Maven potrebbe portare all'errata conclusione che si tratti "solo" di un
altro strumento di build e/o che si tratti di una mera sostituzione di Ant. Anche se in ciò c'è un
fondo di verità, Maven è molto di più... Maven, principalmente, è uno strumento completo per
la gestione di progetti software Java, in termini di compilazione del codice, distribuzione, docu-
mentazione e collaborazione del team di sviluppo. Secondo la definizione ufficiale (http://
maven.apache.org/), si tratta di un tentativo di applicare pattern ben collaudati all'infrastruttura
del build dei progetti al fine di promuoverne la comprensione e la produttività del team coin-
volto allo sviluppo, fornendo un percorso chiaro all'utilizzo di best practice. Per questo motivo
Maven è definito, sinteticamente, tool per la gestione e comprensione dei progetti. Maven è
quindi un insieme di standard, una struttura di repository e un'applicazione demandati alla
gestione e la descrizione di progetti software. Maven, inoltre, definisce un ciclo di vita standard
per il build, il test ed il deployment di file di distribuzione Java.
Nel libro [BTRMVN] è presente un'interessante definizione, destinata, secondo una nota
degli stessi autori, principalmente al pubblico dei manager: "Maven è uno strumento dichiarativo
per la gestione dei progetti Java che permette di ridurre il tempo totale di sviluppo dei progetti
(time-to-market) attraverso un efficace utilizzo delle sinergie disponibili. Maven permette di
ridurre il numero delle risorse umane e contemporaneamente permette di ottenere elevate
efficienze operazionali".
Le caratteristiche di Maven fanno sì che diverse persone, anche inizialmente estranee al pro-
getto, possano lavorare insieme produttivamente senza dover spendere molto tempo nel cerca-
re di comprendere la struttura del progetto, il funzionamento, il processo di build, etc. Tutti
coloro che si sono trovati, almeno una volta nella loro vita, a dover intervenire in progetti di
grandi dimensioni in corso d'opera, sicuramente sanno quale frustrazione possa derivare dal
dover capire, rapidamente, come le varie parti del progetto interagiscano tra loro, l'ordine e
dipendenze del processo di build, etc.
Le aree prese in considerazione da Maven sono: build (sicuramente), documentazione,
reportistica, gestione delle dipendenze, SCM (Software Configuration Management), rilascio e
distribuzioni di nuove versioni. Tutti i progetti, indipendentemente dal relativo dominio, esten-
sione e tecnologia, presentano una serie di necessità standard, quali: la conversione dei sorgenti
in codici "eseguibili" (build), la verifica (test), l'assemblaggio, documentazione ed eventual-
mente il "dispiegamento" (deployment).
Alcuni di questi vantaggi sono derivati dall'esistenza (non presente in Ant) di un progetto
"genitore" dal quale far ereditare sotto-progetti, etc.
Dalla versione iniziale Maven ha compiuto molti progressi contribuendo enormemente a
semplificare le quotidiane attività del team di sviluppo. L'ultima versione disponibile al mo-
mento della scrittura di questo capitolo (aprile 2008) è la 2.0.8.
Obiettivi di Maven
Gli obiettivi che l'adozione di Maven dovrebbe permettere di ottenere sono:
• setup semplificato dei progetti in funzione delle best practice "implementate". Preparare
un nuovo progetto (partendo da modelli) o estrapolare un modulo richiede un tempo
inferiore al minuto.
• utilizzo consistente indipendente dai singoli progetti, il che equivale a minimizzare la
curva di apprendimento.
• gestione avanzata delle dipendenze corredata da aggiornamento automatico e gestione
delle dipendenze transitive (A — > B, B — > C => A — > C).
• gestione simultanea di diversi progetti.
• continua espansione della già considerevole disponibilità di plug-in (non sempre
stabilissimi), librerie e meta-dati da utilizzarsi per la gestione dei propri progetti. Molti
di questi elementi, inoltre, sono open-source e la relativa comunità è molto attiva.
• estensibilità, è possibile non solo di modificare le impostazioni di default, ma anche
scrivere propri plug-in.
• immediato accesso alle nuove feature con minimo dispendio di tempo.
• disponibilità di task Ant per la gestione delle dipendenze e per il deployment esterno
all'ambiente Maven.
• build basati su modello. Maven è in grado di eseguire il build di una serie di progetti e di
includere le relative distribuzioni in appositi file .jar, war etc. senza dover ricorrere ad
alcuno script.
• sito coerente di informazioni relative al progetto. A tal fine Maven utilizza gli stessi dati
utilizzati dal processi di build. Maven, inoltre è in grado di genere tutta una serie di
rapporti in una serie di formati, incluso il PDF.
• gestione del processo di rilascio di nuove versione e pubblicazione dei file di distribuzione.
Ciò avviene in maniera quasi trasparente anche in presenza di sistemi di Control
Managment come CVS e ClearCase.
• gestione delle dipendenze. Maven incoraggia l'utilizzo di repository sia locali sia remoti,
per la memorizzazione dei file di distribuzione. Inoltre, dispone di una serie di meccanismi
che permettono di eseguire il download, da un sito globale, di specifiche librerie richieste
dal proprio progetto. Il che semplifica il riutilizzo degli stessi file jar da parte di diversi
progetti fornendo anche informazioni necessarie per gestire problemi relativi alla
retrocompatibilità (back.wa.rd compatìbìlity).
Vantaggi di Maven
Date le caratteristiche di Maven evidenziate nei paragrafi precedenti, dovrebbero essere ormai
chiari quali siano i vantaggi derivanti dall'utilizzo nella gestione dei progetti software, specie in
quelli non semplici. In particolare:
Project O b j e c t M o d e l
(POM)
pom xml
File di d i s l r i b u z i o n e
(jar, w a r , e a r , p o m )
Local Repository.
Remóle Repository
• riutilizzo: si tratta uno degli elementi alla base di Maven, il cui utilizzo, di fatto è già un
primo riutilizzo di best practice. Un ulteriore livello di riutilizzo è garantito dal fatto che
la business logie è incapsulata in comodi moduli (plug-in).
• maggiore agilità: utilizzando Maven si semplifica il processo di generazione di nuovi
componenti, di integrazione tra componenti, di condivisione di file eseguibili, inoltre, la
curva di apprendimento di ciascun progetto viene incredibilmente ridotta, etc.
• semplificazione della manutenzione: il tempo necessario per manutenere gli ambienti e
script di build è significativamente ridotto.
Purtroppo va anche detto che ci sono alcuni problemi di stabilità sia di Maven sia dei suoi
stessi plug-in. Maven èl'esempio di una idea brillante non implementata adeguatamente.
• il file pom.xml, (POM, Project Object Model): è la descrizione dichiarativa del progetto
ossia sono i meta-dati del progetto stesso. Include sezioni per il build, per la gestione
delle dipendenze, per la gestione del progetto in generale, per i test, per la generazione
della documentazione, e così via.
• goal: è un po' l'equivalente Maven dei task Ant . In questo caso però si tratta di una
funzione eseguibile che agisce su un progetto. Questi possono essere sia specifici per il
progetto dove sono inclusi, sia riusabili. In termini programmativi, si può immaginare il
progetto e i relativi meta-dati come le caratteristiche strutturali di un oggetto (attributi e
relazioni), mentre i goal ne rappresentano le caratteristiche comportamentali, ossia i
metodi che agiscono sull'oggetto.
• plug-in: si tratta di goal riutilizzabili e cross-project.
• repository: si tratta di un meccanismo che permetto di memorizzare file di distribuzione;
in pratica è una sorta di cartella strutturata per la gestione delle librerie. Maven permette
di definire repository condivisi, remoti, e repository locali, aggiornati automaticamente
dai primi.
Archetipo
Una delle caratteristiche particolarmente apprezzate di Maven è la fornitura di un insieme di
standard che rendono possibile l'applicazione di tutta una serie di best practice. Uno di questi
standard è costituito dalla struttura della directory del progetto, denominata archetype
(archetipo). La relativa applicazione sebbene sia fortemente consigliata, coerentemente con
l'impostazione di Maven, non è assolutamente obbligatoria: Maven fornisce una serie di stru-
menti che permettono di utilizzare strutture customizzate. Questa possibilità, tuttavia, dovreb-
be essere utilizzata con parsimonia, in casi in cui sia veramente necessario. Deviare dalle
impostazioni, infatti, tende ad invalidare, o comunque a ridurre, la portata di tutta una serie di
vantaggi relativi sia al fatto che si tratta di uno "standard", sia al fatto che la struttura proposta
abbia raggiunto un certo grado di maturità.
La figura 9.2, mostra la struttura generale Maven standard. Questa può essere creata manual-
mente (non tutte le sub-directory sono necessarie, tuttavia la presenza di alcune è richiesta da
specifici plug-in), o, in alternativa, è possibile delegare questo compito direttamente a Maven. A
tal fine (una volta correttamente installato Maven) è necessario eseguire il seguente comando:
La presenza del file pom.xml nella directory principale di un progetto è il "marchio" che si
tratta di un progetto gestito da Maven, in quanto, come illustrato di seguito, contiene la descri-
zione dichiarativa del progetto.
Nella directory principale sono ubicati i file: pom.xml, profile.xml, LICENSE.txt e README.txt. Il
primo contiene la descrizione dichiarativa del progetto, profile.xml è utilizzato per definire valori
che permettono di configurare diversi elementi dell'esecuzione di Maven. Questo file non do-
vrebbe contenere configurazioni relative a specifici progetti, né dovrebbe essere distribuito.
Esempi di elementi che si possono trovare in questo file sono l'indirizzo del respository locale e
di quello remoto, e informazioni relative all'autenticazione. I restanti due file contengono, ri-
spettivamente, il testo della licenza e informazioni utili per gli sviluppatori relative al progetto.
La directory src è destinata ad ospitare tutti i tipi di file sorgente. L'attenzione quindi non è
limitata ai soli file Java che, una volta compilati, andranno a formare il file di distribuzione
(dopo essere stati spostati nella directory target/classes), bensì include anche file di test, eventua-
li file di script, comandi SQL, etc.
La prima sottodirectory di src è main, destinata ad ospitare i sorgenti Java (java), i file di test
(test), i sorgenti scritti in eventuali altri linguaggi e la configurazione per il sito (site). Come da
standard Sun, la directory src/main/java rappresenta la root dei sorgenti Java, pertanto è necessa-
rio introdurne altre necessarie per specificare in maniera univoca il progetto e i relativi file. Ciò
<prenci-nome (9rtilactl(il>
aU f»"1 >m|
pratile «mi
J com
- l mokabyte
J linnnco
; sql
- resources
i m o k a by le
j li nane e
classes
si ottiene creando, in cascata, le seguenti directory: tipo dell'organizzazione (corri, org, etc.),
l'azienda, il dipartimento, eventualmente il sotto-dipartimento, il nome del progetto, e così via
(per esempio com\mokabyte\finance\priceblotter). La directory src/main/resources memorizza even-
tuali file di risorse Java, per esempio i file properties, file xml, da copiare nella directory target/
classes. La regola è che Maven include nel file di distribuzione tutti i file e le directory presenti
in src/main/resources con la stessa struttura con cui sono memorizzati.
La directory site contiene informazioni utilizzate per generare la documentazione relativa al
progetto (a tale fine si deve utilizzare il comando mvn site) copiate nella directory target/site. Il
framework utilizzato per la generazione delle informazioni è Doxia (http://maven.apache.org/doxia/
book/index.html). La directory target, infine, ospita i vari file prodotti a partire da quelli presenti
nella direcory src. Per esempio, le classi Java, i JavaDoc, la documentazione del progetto, il file
di distribuzione, e così via.
POM
Il diagramma di figura 9.3 illustra la struttura concettuale del file pom.xml la cui implementazione
è riportata nel listato poco sotto.
Dall'analisi del diagramma è possibile evidenziare una delle maggiori differenze con i file
build.xml di Ant: il file pom.xml contiene la dichiarazione del progetto e non le azioni da eseguire
durante per il processo di build (ossia le procedure). In ogni caso, è possibile includere nei file
Figura 9.3 - Struttura concettuale del file POM.
pom.xml una serie di maven-antrun-plugin che permettono di eseguire task Ant, ma va ricordado
che i file pom.xml definiscono "chi", "cosa" e "dove", mentre i file di build si limitano al "quan-
do" e al "come".
<project>
<modelVersion>4.O.0</modelVersion>
groupld
artifactld
/ Parent < version
P O M Relationships ( relalivePath
Inheritance
Dependency
Dependencies Dependency
Management
groupld
artifactld
version
( ype
Dependencies Dependency
;scope
^systemPath
optional
groupld
Exclusions Exclusions
artifactld
Figura 9.4 - Struttura della sezione dedicata alle relazioni tra POM.
Di questi campi, il primo ed il terzo possono essere ereditati da un POM genitore. Il groupld
identifica univocamente un insieme di progetti appartamenti alla stessa organizzazione, team,
etc. Per esempio il core Maven dichiara la seguente stringa per il groupld: org.apache.Maven. La
notazione con il punto, per quanto sia un'ottima convenzione, non è né obbligatoria né tantomeno
deve corrispondere alla struttura in package del progetto. Tuttavia, una volta memorizzato il
file di distribuzione nel repository, il groupld si comporta come i package Java: rappresenta il
percorso relativo all'interno del repository a tal fine il punto viene sostituito dallo barretta
obliqua dipendente dal sistema operativo sottostante ("/" in UNIX).
Il campo artitactld indica il nome del progetto. La combinazione con il campo precedente
genera il nome univoco del progetto tra l'insieme di tutti i progetti (in maniera del tutto analo-
go alle classi Java) a netto della versione.
Il campo version è l'ultima parte necessaria per completare il nome. In particolare, indica una
specifica versione del progetto. Questo campo dovrebbe essere mantenuto sincronizzato con le
variazioni che avvengono nel sistema di versionamento che gestisce i sorgenti del progetto.
Inoltre, è utilizzato nel repository dei manufatti (tipicamente file jar) per mantenerli separati
(diversi sistemi, per esempio, potrebbero utilizzare diverse versioni di una stessa libreria).
Sempre dall'analisi del diagramma di figura 9.3, si può notare come il file POM, in prima
analisi, possa essere suddiviso nei seguenti cinque macro componenti: relazioni tra POM,
impostazioni utilizzate dal processo di build, informazioni relative al progetto, impostazione
dell'ambiente di build e impostazione dell'ambiente di Maven.
La sezione relativa alle relazioni tra POM (POM relationships, figura 9.4) permette di orga-
nizzare i progetti attraverso una serie di file POM opportunamente relazionati. In particolare,
le relazioni disponibili sono dipendenza, ereditarietà e aggregazione;
Nella parte dedicata alle impostazione necessarie al processo di build (build settings, figura
9.5) sono definite le varie informazioni richieste dal processo di build, come, per esempio, le
proprietà utilizzate, il goal, le varie directory, i plug-in da utilizzare incluse le relative relazioni
e le risorse da utilizzare.
La sezione inerente alle informazioni relative al progetto (project information, figura 9.6) è
dedicata ad eventuali informazioni supplementari che è possibile definire per un progetto,
come il nome del progetto, la descrizione, l'URL, le persone che vi hanno preso parte, etc.
Il segmento relativo all'ambiente di build (build environment, figura 9.7) contiene le varie
informazioni relative all'ambiente di build, come le impostazioni del software utilizzato per
TestResources Resource
Plugins Plugin
Dependencies Dependency
Executions Execution
m henlefl
Configuration
management
soureeO>reclory
s cnpl S Ou rceO i rec tory
lestS ou rceOirectwy
OuIputDiieclOfy
tes lOutpul D irec tory
Figura 9.5 - Struttura del POM relativa alle impostazioni utilizzate dal processo di build.
,name
/ description
/¿uri name
/// InceptionYear url
Licenses License V ...
comments
dislribulion
name
Organization
,
• uri
id
/
name
1 email
P r o j e c t Information
r
ff-
" Developers | i Developer url
organization
i organizationURL .. role
i
Roles role
\ 1 timezone
\ Properties
Contributors j Contributor
J
Figura 9.6 - Struttura del POM inerente alle informazioni del progetto.
¡ssueManagemenl •
type
sendOnEmx
sendOnFailure
ciManagemenl sendOnSuCcess
sendOnWamnmg
name
Environment . configuration
subscribe
settings . unsubscribe
mailingList post
otherArchives
connection
developerConnection
scm <r ^
. '. tag
urt
gestire i problemi (software come JIRA), del software per l'integrazione continua, per la gestio-
ne della configurazioni, etc.
La sezione relativa all'ambiente Maven (Maven environment, figura 9.8) è demandata alla
configurazione dell'ambiente Maven, pertanto è possibile impostare i vari repository, la gestio-
ne della distribuzione, i profili utilizzati per modificare il comportamento di Maven in situazio-
ni ben definite, e così via. Su MokaByte (http://www.mokabyte.it) è disponibile una descrizione
dettagliata delle varie sotto-parti, nella serie di articoli Maven: Best practice applicate al processo
di build e rilascio di progetti java scritti dall'autore e pubblicati tra gennaio e settembre 2007.
I repository di Maven
I repository server (più semplicemente repository) sono una sorta di file system strutturato,
disegnato per memorizzare i manufatti dei progetti. Questi non sono altro che le librerie utiliz-
zate (file jar), i distribuibili prodotti (jar, war, ear), i vari plug-in, etc. Come illustrato precedente-
mente, ogni manufatto è univocamente identificato dalle proprie coordinate ottenute per mez-
zo dalla tripla: group id, artifact id e numero di versione. Maven prevede due tipologie di
repository: locali e remoti.
I repository appartenenti alla prima categoria sono copie installate tipicamente nei dischi
rigidi dei vari computer e svolgono il duplice compito di funzionare da cache, riducendo il
numero di download remoti, e di memorizzare e rendere disponibili i manufatti temporanei
risultato del processo di build (file non ancora distribuiti in versione stabile, ossia gli snapshot).
I repository remoti, invece, sono, nella maggior parte dei casi, repository esterni alla propria
organizzazione e quindi gestiti da terze parti. Si ricordi che Maven permette di definire la lista
dei repository che si desidera utilizzare in due file:
. enabled
Releases updalePolicy
checksum
. enabled
Repository Snapshots updalePolicy
.4 checksum
id
name
Repositories uri
layout
pluginRepository
downloadURL
status
id
site name
distribution uri
Management . groupld
artifactld
relocation version
message
Maven
Prerequisiles
Environment
activation
build
modules
repositories
profiles profile
pluginRepositories
dependencies
reporting
ii dependency
Manangement /'
distribution
Manangement
Figura 9.8 - Struttura del POM relativa all'ambiente Maven (Maven environment).
migliorie sempre in termini di performance. La figura 9.4 mostra un esempio tipico di struttura
dei repository Maven.
I repository remoti sono tipicamente acceduti attraverso diversi protocolli di rete, tra cui
file://, ftp://, http:// etc. Per la precisione, Maven per la gestione e il deployment dei manufatti, si
affida al framework Wagon (http://maven.apache.org/wagon/) che, attualmente, supporta i proto-
colli File, HTTP, H T T P lightweight, FTP, SSH/SCP, WebDAV e SCM (quest'ultimo non è
stato ancora rilasciato).
II flusso tipico dei manufatti prevede un passaggio dai repository centrali via via a quelli
locali; come dire dal governo centrale via via a quelli locali.
Il repository centrale dell'azienda esegue il download dei manufatti direttamente dai repository
centrali "globali". Questi sono normalmente organizzati in un repository centrale ed una serie
di mirror distribuiti per le varie regioni geografiche.
Il repository centrale dell'azienda, a sua volta, si incarica di mantenere aggiornati i repository
locali dei vari progetti che, a loro volta, mantengono aggiornati i repository locali dei computer
di "sviluppo". Questa organizzazione piuttosto gerarchica, non è tuttavia sempre rispettata. In
effetti, i manufatti distribuiti dalla Sun sembrerebbero venir prelevati direttamente dal relativo
repository (probabilmente per problemi di licenze).
Il flusso tipico dal globale via via verso il locale è invertito qualora si desideri pubblicare i vari
manufatti. Lo scenario tipico consiste nell'avere i manufatti prodotti da uno specifico progetto
pubblicati a ritroso fino a raggiungere il repository del progetto o anche quello centrale del-
l'azienda. Qualora invece si dovesse lavorare in progetti open source non sarebbe infrequente
che il flusso a ritroso giunga fino alla pubblicazione sul repository centrale globale. La pubbli-
cazione dei vari manufatti avviene attraverso il plug-in deploy che si occupa, tra l'altro di gene-
rare i vari file di checksum.
I file checksum sono utilizzati per il controllo di integrità (checksum hash) e servono per verificare/
assicurare l'integrità dei vari manufatti d o p o il loro trasferimento tra i vari repository (il servizio
una cifratura di lunghezza molto inferiore a quella del file iniziale, legata al contenuto del file
stesso. Pertanto, avendo a disposizione il file originario è sempre possibile eseguire l'algoritmo di
hashing e quindi confrontare la cifratura ottenuta con quella originaria. Se le due differiscono,
allora si è verificato un errore, per quanto possa anche accadere che il file di controllo sia corrotto.
In figura 9.10 è mostrato un esempio della nuova struttura dei repository Maven, introdotta
dalla versione 2. Questa non dovrebbe presentare troppe sorprese al pubblico di programma-
tori Java. In effetti presenta grosse similitudini con la convenzione utilizzata per i package. In
particolare, la genarchia del repository centrale prevede:
I comandi di Maven
I comandi Maven presentano una struttura standard descritta nella tabella 9.1. Per esempio:
Si
Comando Descrizione
m v n test-compile c o m p i l a , ma non esegue, i test J U n i t
m v n test c o m p i l a e d e s e g u e i test J U n i t
Definizione di goal/task
plug-in -
cross-project
XML-Java
Definizione di goal/task XML-Java
(Jelly fino alle versione 2)
2 0 minuti (5 riutilizzando le
Tempo per iniziare un nuovo progetto
5 minuti impostazioni di un precedente
(semplice)
progetto)
Giorni/Settimane (differente
Ore
filosofia di intendere il processo di
Tempo di apprendimento (struttura e funzionamento
build; meccanismo di
sono immediati)
funzionamento dei repository, etc.)
Tabella 9.4 - Un ulteriore confronto tra Ant e Maven, in termini di uso e apprendimento.
Una delle componenti di Maven di maggior successo è sicuramente costituita dai repository,
tanto che, molto probabilmente, anche gli script Ant potrebbero beneficiare enormemente dal
loro utilizzo. La tabella 9.4 illustra ulteriori informazioni circa l'uso e la curva di apprendimen-
to dei due tool.
!
i
In figura 9.12 è presentata una struttura utilizzabile per memorizzare i manufatti che non
siano strettamente codici sorgenti. Sebbene tale struttura dovrebbe essere auto-esplicativa, un
dubbio che potrebbe sorgere è relativo alla locazione in cui crearla. Le alternative sono al
livello dell'intero progetto (come per esempio globalfrontoffice) e/o per ciascuno dei sotto-siste-
mi. La risposta dipende da diversi fattori, quali per esempio la dimensione del progetto, la
granularità dei sotto-sistemi, etc. Tuttavia è abbastanza frequente il caso in cui almeno alcune
sue parti siano necessarie per ciascun tipo elemento. Per esempio, anche una semplice libreria
o un elementare componente includano, cosa molto consigliabile, alcuni manufatti di disegno
oppure un database di cui è disponibile il disegno dello schema. Inoltre, in un normale proget-
to è tipico iniziare a modellare 0 sistema, soprattutto in termini di requisiti, come un'unica
entità, per poi, scinderlo in sotto-sistemi e quindi sotto-progetti. Ciò porta ad avere requisiti,
disegni di architettura, etc. sia al livello dell'intero progetto, sia versioni più dettagliate al livello
di sotto-sistema. Ciò porta alla conclusione che la struttura di figura 9.12 debba essere creata,
con opportune personalizzazioni, sia al livello di intero progetto (requisiti business, disegno
dell'architettura globale), sia per ciascun sotto-sistema (requisiti di dominio, disegno del sotto-
sistema, del relativo database, e così via).
Direttive
Nei paragrafi successivi sono riportate una serie di linee guida relative al processo di build e
dintorni. Si è deciso di focalizzare l'attenzione solo su pochi argomenti particolarmente im-
portanti: non tutto ciò che riguarda file system, build e deploy è contenuto in queste direttive,
ma ci sono solo alcuni consigli strategici su cui basare un ulteriore approfondimento.
9.1 Utilizzare strumenti moderni per il build
Dalla lettura del presente capitolo dovrebbero essere ormai chiare le motivazione pratiche che
hanno portato all'implementazione di strumenti quali Ant e Maven. Prima dell'avvento di que-
sti tool, imperavano strumenti quali make, gnumake, namake e jam. Questi permettevano di
implementare il processo di build attraverso appositi script. Veri e propri miniprogrammi dif-
ficili da scrivere, manutenere e verificare. Il relativo utilizzo presentava una serie di seri proble-
mi, tra i quali i più importanti sono:
• dipendenza dalla piattaforma. Le versioni per ambienti Unix erano degli script Shell.
• mancanza di standardizzazione.Apprendere il funzionamento di un uno di questi file
era pressoché equivalente ad apprendere il funzionamento di un programma.
• complessità. Si trattava di script spesso difficili da implementare e mantenere.
• ridotte performance. Il build delle applicazioni Java richiede di compilare tutte le singole
classi. L'esecuzione di uno script Make in tali circostanze richiede di eseguire, per ogni
singola classe il compilatore Java (JavaC). Moltiplicando ciò per mille o diecimila classi,
l'impatto era decisamente notevole.
- <progetto>
- J requirements
j usecasediagrams
- ^J use_case_speciflcalions
- , business rules
- i domain _object_model
-. non_functional_requirements
- ^j other spec
- j architecture
- ^J interfaces
- / layers
- I design
- I components
- ^J database
• ripetibilità: è necessario poter eseguire il processo di buil del sistema esattamente nello
stesso modo ogniqualvolta si voglia, indipendentemente da chi lo esegua e da quale
computer/ambiente sia lanciato.
• riproducibilità: questa proprietà è necessaria per poter copiare il sistema su diversi server;
• elevato livello di standardizzazione: i vari progetti devono presentare medesime proce-
dure e best practice.
L'utility JavaDoc
L'utility JavaDoc è uno degli strumenti di supporto alla produzione di software forniti con il
J D K fin dalle sue prime apparizioni. In particolare si tratta di un'applicazione che, utilizzando
i servizi del compilatore Java (javac), è in grado di esaminare sorgenti Java, la cui lista è specifi-
cata nella linea di comando, e di produrre la relativa documentazione. Questa, per default, è
organizzata in un insieme di pagine HTML ed è ottenuta estraendo, dai file sorgenti considera-
ti, alcuni costrutti fondamentali e particolari elementi di documentazione denominati doc
comments (commenti doc). Questi sono inseriti nel codice, immediatamente prima dell'entità
che si intende documentare (dichiarazione di classi, interfacce, metodi, attributi e parti di codi-
ce), e pertanto sono utilizzati, prevalentemente, per documentare le API del codice prodotto. I
commenti doc sono identificabili dal seguente formato: /** doc comment */.
Javadoc, inoltre, offre la possibilità di organizzare la documentazione in formati e strutture
diverse da quella HTML standard. Per esempio è possibile cambiarne la struttura oppure pro-
durre file di tipo completamente diverso, come file testo, XML, e così via. A tal fine è necessa-
rio implementare opportune classi che utilizzino le Sun Java doclet API (cfr. http://java.sun.com/
j2se/1.5.0/docs/guìde/javadoc/doclet/overview.html).
La struttura dei commenti doc è una combinazione di testo e di speciali etichette (tag) intro-
dotte dal carattere "chiocciola" (@). Queste rappresentano parole chiavi della documentazione
Javadoc e sono utilizzate per identificare particolari informazioni come per esempio l'autore
del codice (@author <nome autore>), i parametri formali di un metodo (@param <nome parametro
<descrizione>), l'eventuale valore di ritorno (@return descrizione valore>).
Per esempio, nel listato seguente, si vede un frammento di JavaDoc della classe java.lang.enum.
• block tag (detti stand-alone). Questi sono introdotti dal semplice carattere chiocciola
(assumono la forma @<tag>) e devono essere inseriti dopo la descrizione principale pre-
sente nel commento. Inoltre, per essere considerati correttamente, devono inclusi al-
l'inizio linea a meno di caratteri asterisco, spazi bianchi e il delimitatore stesso del com-
mento (/**), ignorati durante la produzione della documentazione. Alcuni esempi sono
mostrati nel listato precedente.
• inline tags. Questi possono essere inseriti in qualsiasi parte della documentazione ed
assumono la forma |@<tag>). Un esempio è costituito dall'etichetta |@link| utilizzato per
inserire HTML link nella posizione dove appare all'interno del commento.
Tag JavaDoc
@Author
Utilizzo: @author <lista_ autori»
J D K : 1.0
Questo tag permette di includere l'indicazione lista_autori nella documentazione generata da
JavaDoc. lista_autori può essere una singola stringa riportante un solo autore, oppure una lista
separata da virgole e spazi. Ciascun elemento (autore), tipicamente, è costituito dal nome del-
l'autore. Comunque c'è chi preferisce inserire le iniziali o un soprannome (nickname) o addirit-
tura il nome di un intero team. Una notazione alternativa per riportare una lista di autori sta
nell'includere diverse righe, ciascuna dotata del tag @author seguita dal nome dello sviluppatore.
@code
Utilizzo: (@code <testo>l
JDK: 1.5
Questo tag permette di riportare un frammento di codice utilizzando il relativo font, evitan-
do che il testo sia interpretato come tag HTML o JavaDoc. Ciò permette di utilizzare senza
problemi, caratteri o stringhe (come per esempio <, >, ->) che altrimenti potrebbero creare
problemi di interpretazione da parte dei parser HTML.
Questo tag è molto simile a literal, con la sola differenza che il testo è visualizzato con il font
prestabilito per frammenti codice. Si tratta di una documentazione in linea e quindi può essere
inserita, virtualmente, in qualsiasi commento.
@deprecated
Utilizzo: @deprecated <descrizione>
JDK: 1.0
Questo tag permette di documentare elementi deprecati e che quindi se ne dovrebbe evitare
l'utilizzo. Questo perché, sebbene siano presenti nella versione attuale del codice, non c'è garanzia
che vengano mantenuti in versioni successive. La descrizione dovrebbe riportare la versione in cui
la deprecazione è avvenuta e un'indicazione di quale API utilizzare in alternativa. Per esempio:
@docroot
Utilizzo: |@docroot]
JDK: 1.5
Questo tag rappresenta, per ogni pagina, il riferimento relativo (indiretto) al percorso della
radice della documentazione (JavaDoc) generata. Ciò significa che se l'etichetta (@docroot) è
inserita nella documentazione di una classe appartenente a un package che si trova a due livelli
di profondità (java.lang), questo tag sarà sostituito dalla stringa: "..V.". Per una classe di un
package di profondità tre (java.lang.reflect), questo tag sarà invece sostituito dalla stringa: "..VA..".
In genere (@docroot) è utile in tutti quei casi in cui si voglia aggiungere alla documentazione
generata un file (per esempio il copyright) o un'immagine. Un tipico esempio è dato da:
<a href="../../copyright.html">
@exception
Utilizzo: @exception <nome_classe_eccezione> <descrizione_occorrenza>
JDK: 1.5
Questo tag è equivalente a @throws (cfr.) che di solito viene preferito.
@inheritDoc
Utilizzo: (@inheritDocl
JDK: 1.5
Questo tag permette di copiare (ereditare) la documentazione doc dalla superclasse più vici-
na da cui eredita quella attuale, o dall'interfaccia implementata dalla classe corrente. E possibi-
le utilizzare questo tag nella documentazione doc relativa a un metodo e con i tag @param,
©return e @throws. Qualora questo tag non sia presente, continuano a valere le regole di
ereditarietà automatica della documentazione doc. La differenza è che in presenza di questo
tag è possibile aggiungere documentazione specifica a quella più generale "ereditata".
@link
Utilizzo: {@link <percorso> <etichetta>)
J D K : 1.2
Questo tag permette di inserire degli hyperlink all'interno della documentazione generata,
dove il percorso è un riferimento indiretto all'elemento da collegare, mentre l'etichetta contie-
ne il testo da mostrare. Poiché si collegano elementi di codice, il campo <etichetta> è mostrato
con il font previsto per i frammenti di codice. La struttura di <percorso> prevede:
<package>.<classe>#<membro>. Da notare che gli elementi <package> e <classe> devono essere in-
seriti solo qualora ci si riferisca a package e classi differenti da quelli attuali.
Questo tag è molto simile al tag @see con la differenza che mentre @link è un tag in linea e
quindi l'hyperlink è inserito esattamente nel luogo dove è presente il tag, gli hyperlink generarti
dal tag @see sono inseriti in un'apposita sezione ("see also").
I tag ©link possono essere inseriti in ogni tipo di commento doc.
@linkPlain
Utilizzo: (@linkPlain <percorso> <etichetta>)
JDK: 1.4
Questo tag è simile al precedente (cfr. @link) con la sola differenza che il campo <etichetta>
non è mostrato con il font relativo al codice.
@literal
Utilizzo: {@literal <testo>)
J D K : 1.5
Questo tag è assolutamente simile a @C0de (cfr.) con la sola differenza che il testo non è
mostrato utilizzando il font relativo al codice.
@param
Utilizzo: {@param <nome_parametro> <descrizione_parametro>)
J D K : 1.0
Questo tag permette di descrivere i parametri di metodi e classi. Quest'ultimo caso è divenu-
to necessario con Java 1.5 e in particolare con l'introduzione delle classi template (generici).
Parametri di questo tipo sono detti parametri tipo (type parameter) e possono essere presenti
anche in metodi. Si contraddistinguono da quelli normali poiché vanno scritti tra parentesi
angolari, anche nel commento doc. Per esempio:
Da tener presente che è necessario riportare il nome del parametro, non il tipo, e la descrizione
separati da uno o più spazi.
@return
Utilizzo: {©return <descrizione_valore_di_ritorno>)
JDK: 1.0
Questo tag permette di descrivere il valore di ritorno di un metodo. Tipicamente, la descri-
zione riporta l'insieme dei possibili valori di ritorno.
@see
Utilizzo: |@see «riferimento]
JDK: 1.0
Questo tag aggiunge un riferimento alla sezione "see also" della corrente pagina di docu-
mentazione. Ogni documento doc può contenere un qualsiasi numero di tag @see. Il campo
riferimento può assumere tre diverse forme:
1. "stringa". In questo caso nella sezione "See also" è incluso il testo della stringa (che
deve essere racchiuso tra doppi apici). Nessun collegamento è generato.
2. <a href="URL#value">label<la>. In questo caso viene aggiunto un collegamento allo URL
indicato mostrando il testo value.
3. package.class#member label. Questo caso è del tutto equivalente a quanto riportato per la
tag @link, con la consueta differenza che gli elementi referenziati sono inseriti nella sezio-
ne "see also". Il campo label è opzionale.
Nella tabella A.l sono mostrati alcuni esempi di utilizzo del tag:
@serial
Utilizzo: l@serial <descrizione_campo> | include | exclude }
JDK: 1.2
Questo tag è utilizzato per documentare campi serializzabili, ossia attributi di classi che
implementano l'interfaccia Serializable, la cui dichiarazione non include la parola chiave transient.
Come tali, i relativi valori sono memorizzati e riacquisiti nello e dallo stream di serializzazione.
Il campo <descrizione_campo> è opzionale e come al solito fornisce una descrizione del campo
inclusi i relativi valori. Il flag include/exclude indica se il package o la classe debbano essere
inseriti in una particolare documentazione, denominata Serializable form page.
r"
' Serializable fields for Biglnteger.
@serialData
Utilizzo: |@serialData <descrizione_dati>)
J D K : 1.2
Questo tag è utilizzato per documentare il tipo e l'ordine dei dati nella forma serializzata.
Tipicamente è utilizzato per commentare i metodi writeObject, readObject, writeExternal, readExternal,
writeReplace, e readResolve. Nel listato viene riportato 0 commento doc del metodo writeObject
della classe java.util.ArrayList.
' Save the state of the < c o d e > A r r a y L i s t < / c o d e > instance to a s t r e a m (that
" is. serialize it).
' WserialData The length of the array backing the < c o d e > A r r a y L i s t < / c o d e >
instance is emitted (int), f o l l o w e d by all of its elements
(each an < c o d e > O b j e c t < / c o d e > ) in the proper order.
7
private void writeObject(java.io.ObjectOutputStream s)
@since
Utilizzo: (Osince d e s t o ]
JDK: 1.1
Questo tag permette di dichiarare, nella corrispondente sezione, da quale versione della
relativa API quella determinata caratteristica (package, classe, interfaccia, metodo, etc.) o va-
riazione è disponibile. Il testo non prevede alcuna specifica struttura. Questo tag può essere
utilizzato in tutti i commenti doc.
@throws
Utilizzo: (@throws <nome_classe_eccezione> <descrizione_occorrenza>]
JDK: 1.2
Questo tag è del tutto equivalente a @exception, però è tipicamente preferito sia perché è più
sintetico, sia perché la stessa dicitura è ripetuta nella dichiarazione della firma del metodo.
Questo tag è utilizzato per dichiarare le eccezioni lanciate da un metodo o da un costruttore,
riportando una breve descrizione della motivazione per cui l'eccezione è scatenata. Qualora
un metodo possa scatenare diverse eccezioni, ognuna deve essere commentata con un apposito
tag @throws o @exception. Il motivo principale per definire questo tag è informare programma-
tori che intendano utilizzare una classe o, più semplicemente un singolo metodo, circa le ecce-
zioni che questo può scatenare. Pertanto è necessario documentare tutte le eccezioni checkcd
ed anche alcune runtimc specifiche del metodo che quindi il programmatore potrebbe essere
interessato a gestire.
@value
Utilizzo: (@value <package>.<classe>#<campo>)
JDK: 1.4
Questo tag è utilizzato per riportare il valore di una costante (campo statico) evitando di
doverne effettuare l'hard-coding nella documentazione. Questo tag può essere utilizzato sia
per mostrare il valore di una costante documentata di seguito (in tal caso va utilizzata senza
parametri), oppure per riferirsi al valore di una costante definita in qualche altra parte del
codice. In questo secondo caso, i vari campi <package>, <classe> e <campo> vanno riportati accu-
ratamente.
@version
Utilizzo: (@version <versione>)
JDK: 1.0
Questo tag è utilizzato per riportare l'indicazione della versione dell'elemento che si intende
documentare. Tipicamente è utilizzato per documentare classi e interfacce.
Il campo versione non prevede alcuna particolare struttura. Comunque, una convenzione
spesso utilizzata prevede di utilizzare una terna di contatori gerarchicamente dipendenti:
<riscrittura>.<importanti_variazioni>.<fixing_minori> (00.00.000). La prima parte è aggiornata ogni
qualvolta si dia luogo a una riscrittura completa. Chiaramente, un avanzamento di questo valo-
re genera l'azzeramento di quelli successivi. Il campo importanti_variazioni, come logico attender-
si, va incrementato ogniqualvolta si eseguano significativi aggiornamenti del codice, come ag-
giunta o rimozione di metodi. L'aggiornamento di questo campo comporta l'azzeramento di
quello successivo. Infine l'ultimo campo si cambia ad ogni fixing o piccola variazione. Spesso a
questa terna di numeri si aggiunge la data dell'ultimo aggiornamento.
Tuttavia è consigliabile demandare la gestione di questa informazione al sistema di gestione dei
sorgenti, come per esempio CVS, SVN e ClearCase, ognuno dei quali presenta una specifica
sintassi.
Tao HTML
In questa appendice è presentata una concisa e utile illustrazione dei principali tag di
formattazione HTML utilizzabili per la produzione di commenti doc. Per ogni tag viene ripor-
tata una breve descrizione e un semplice esempio di utilizzo.
>
Questa etichetta {grccitcr-than) serve per generare il carattere maggiore (">"). Questo non può
essere utilizzato direttamente poiché verrebbe interpretato, erroneamente, come delimitatore
HTML di chiusura tag. Si consideri ad esempio il seguente frammento di commento doc:
<
Questa etichetta (less-thcn) serve per generare il carattere minore ("<"). Questo non può essere
utilizzato direttamente poiché verrebbe interpretato, erroneamente, come delimitatore HTML
di apertura tag. Si consideri ad esempio il seguente frammento di commento doc:
 
Spaces. Questa stringa serve per includere un certo numero di spazi bianchi. E necessario ricor-
rere a questa etichetta poiché la maggior parte dei parser HTML sostituiscono una ripetizione
di spazi bianchi con un solo carattere di spazio. Per l'esempio, cfr. <pre>.
<a> </a>
Il tag dell'ancora è usato per inserire degli hyperlink all'interno della propria documentazione:
<a href=URL> link text </a>. Si consideri ad esempio il seguente frammento di commento doc:
' This interface is a m e m b e r of the
' <a h r e f = ' i ® d o c R o o t ! / . . / g u i d e / c o l l e c t i o n s / i n d e x . h t m r >
" Java C o l l e c t i o n s F r a m e w o r k < / a > .
<b> </b>
Questi due delimitatori sono utilizzati per riprodurre in carattere neretto (bold) il testo incluso.
<br>
Line Break. Questo tag serve a forzare l'avanzamento di riga ed è necessario perché i parser HTML
ignorano i caratteri return. Si consideri il seguente frammento di commento doc:
<code> </code>
Questi tag sono utilizzati per inserire frammenti di codice utilizzando liberamente elementi,
come le parentesi angolari, che altrimenti creano problemi con i tag HTML. Si consideri ad
esempio il seguente frammento di commento doc:
<prexcode>
/ / B a s e G M T offset: + 1 : 0 0
// DST starts: at 1 : 0 0 a m in UTC t i m e
,'/' o n the last S u n d a y In M a r c h
// DST ends: at 1 : 0 0 a m in UTC t i m e
// on the last S u n d a y in O c t o b e r
// Save: 1 hour
SimpleTimeZone (3600000.
"Europe/Paris".
C a l e n d a r . M A R C H . -1. Calendar.SUNDAY.
3 6 0 0 0 0 0 . S i m p l e T i m e Z o n e . U T C TIME.
Calendar.OCTOBER. - 1 . Calendar.SUNDAY.
3600000. SimpleTimeZone.UTC TIME.
3600000)
' </codex/pre>
<dl>
<dt>
</dt>
<dd>
</dd>
</dl>
I tag <dl> e </dl> permettono di definire particolari liste formate da item corredati dalla relativa
descrizione. Gli item vanno riportati all'interno dei tag <dt> e </dt>, mentre le descrizioni vanno
riportate all'interno dei tag <dd> e </dd>. Si consideri ad esempio il seguente frammento di
commento doc:
* <DL>
' <DT> read < D D > read p e r m i s s i o n
" <DT> write <DD> w i i l e permission
* <DT> execute "
" <DD> execute p e r m i s s i o n . A l l o w s < c o d e > R u n t l m e . e x e c < / c o d e > to
be called. C o r r e s p o n d s to < c o d e > S e c u r i t y M a n a g e r . c h e c k E x e c < / c o d e > .
* <DT> delete
' < D D > delete p e r m i s s i o n . A l l o w s < c o d e > F i l e . d e l e t e < / c o d e > to
be called. C o r r e s p o n d s to < c o d e > S e c u r i t y M a n a g e r . c h e c k D e l e l e < / c o d e > .
* <<DL>
<em> </em>
Questi due delimitatori sono utilizzati per riprodurre in carattere enfatizzato il testo incluso. Si
consideri ad esempio il seguente frammento di commento doc:
<h1> </h1>
<h2> </h2>
<h3> </h3>
Le tags <h1> </h1> sono utilizzate per specificare la sezione di header di più alto livello. Oltre a
questi, è possibile utilizzare header di livello inferiore come <h2>, <h3>, etc. Si consideri ad
esempio il seguente frammento di commento doc:
<hr>
Horizontal Rule. Questa etichetta genera una riga orizzontale nella pagina HTML. Si consideri
ad esempio il seguente frammento di commento doc:
' <p><hrxbiockquole><pre>
" class P r i m e T h r e a d e x t e n d s T h r e a d I
long m i n P r i m e :
PrimeThread(tong minPrime) I
this.minPrime = minPrime;
public v o i d r u n ( ) I
// c o m p u t e p r i m e s larger t h a n m i n P r i m e
 :. . :.
* </prex/blockquotexhr>
" <P>
* The f o l l o w i n g code w o u l d t h e n create a t h r e a d and start it r u n n i n g :
' <pxblockquotexpre>
* PrimeThread p = new PhineThread(143):
* p.slart():
* </prex/blockquote>
* <P>
<i> </i>
Questi due delimitatori sono utilizzati per riprodurre in carattere corsivo (italic) il testo inclu-
so. Si consideri ad esempio il seguente frammento di commento doc:
<img>
Questo tag singolo permette di includere delle immagini all'interno della documentazione.
<img src="URL immagine"> . Si consideri ad esempio il seguente frammento di commento doc:
* <p>
' < i m g s r c = " d o c - f i l e s / L i s t - 1 .gif"
" a l t = " S h o w s a list c o n t a i n i n g : V e n u s . Earth. J a v a S o f t , a n d M a r s . J a v a s o f t
• is selected." A L I G N = c e n t e r H S P A C E = 1 0 V S P A C E = 7 >
<0l>
<li>
</li>
</ol>
I tag <ol> e </ol> sono utilizzati per creare un elenco puntato ordinato i cui elementi (voci) sono
specificati all'interno dei tag <li> e </li>. Si consideri ad esempio il seguente frammento di com-
mento doc:
" <0L>
* < L I > l f C declares a public field w i t h the n a m e specified, that is the
field to be r e f l e c t e d . < / L I >
* < L I > l f no field w a s f o u n d in step 1 above, this a l g o r i t h m is applied
recursively to each direct s u p e r i n t e r f a c e of C. The direct
s u p e r i n t e r f a c e s are s e a r c h e d in the o r d e r t h e y w e r e d e c l a r e d . < / L I >
' <LI> If no field w a s f o u n d in s t e p s 1 a n d 2 above, a n d C has a
s u p e r c l a s s S. t h e n t h i s a l g o r i t h m is i n v o k e d r e c u r s i v e l y u p o n S
If C has no s u p e r c l a s s , t h e n a < c o d e > N o S u c i i F i e l d E x c e p t i o n < / c o d e >
is t h r o w n . < / U >
' </0L>
<P> </p>
Questi tag sono i demarcatori di paragrafo (inizio e fine rispettivamente) e fanno sì che tutto il
testo riportato sia visualizzato in una nuova riga. Per questo motivo, tipicamente, si utilizza la
sola etichetta: <p>. Si consideri ad esempio il seguente frammento di commento doc:
' The f o l l o w i n g code w o u l d t h e n create a t h r e a d a n d start it r u n n i n g :
' <|)><blockquote><pre>
' P r i m e T h r m i d p - n e w P r i m c T h r e a d i 143).
' p.starti):
' <;'pre^<,blockquote>
<pre> </pre>
Questi due tag indicano porzioni di testo preformattate. Pertanto elementi quali indentazioni e
ripetizioni di spazi bianchi, normalmente, ridotte a uno solo dai browser HTML, sono rispettati. Si
consideri ad esempio il seguente frammento di commento doc:
' <pxhr><blockquote><pre>
class P r i m e Thread e x t e n d s T h r e a d I
long m i n P r i m e :
PrimeThread(long minPrime) I
this m i n P r i m e - m i n P r i m e :
public v o i d r u n ! ) I
// c o m p u t e p r i m e s larger t h a n m i n P r i m e
&nb:-,|) & n b s p . . & n h s p :
' <'[)ie--</hlockquote:.<Jir
strong> </strong>
Questi due delimitatori sono utilizzati per riprodurre in carattere enfatizzato e neretto il testo
incluso. Si consideri ad esempio il seguente frammento di commento doc:
<table>
<tr>
<th>
<th>
</tr>
<tr>
<td>
<td>
</tr>
</table>
I tag <table> e </table> permettono di definire una tabella. Questa tabella è organizzata in tante
righe quante sono le coppie di tag <tr> e </tr>. Ogni riga poi è organizzata in celle: <td> e </td>.
Inoltre è possibile specificare una linea per il titolo (header) con i tag <tr> </tr>. Si consideri ad
esempio il seguente frammento di commento doc:
* < b l o c k q u o t e x t a b l e s u m m a r y = " E l e m e n t types and e n c o d i n g s " »
* < t r x t h > Element Type < t h > E n c o d i n g
* < t r x t d > boolean <td a l i g n = c e n t e r > Z
* < t r x t d > byte <td a l i g n = c e n t e r > B
* < t r x t d > char <td a l i g n = c e n t e r > C
* < t r x t d > class or Interface <td a l l g n = c e n t e r > L < i > c l a s s n a m e : < / i >
* < t r x t d > double <td a l i g n = c e n t e r > D
' < t r x t d > float <td a l i g n = c e n t e r > F
* < t r x t d > int <td a l l g n = c e n t e r > I
* < t r x t d > long <td a l l g n = c e n t e r > J
* < t r x t d > short <td a l i g n = c e n t e r > S
" </tablex/biockquote>
Da notare che spesso in HTML i terminatori di tag sono omessi. Questa però non è una
buona norma considerando evoluzioni del tipo XTML.
<tt> </tt>
Teletype font. Questi tag specificano che il testo inserito debba essere riportato in un font
teletype. Spesso questa etichetta viene erroneamente utilizzata al posto di <C0de> (cfr.).
<ul>
<li>
</li>
</ul>
I tag <ul> e </ul> sono utilizzati per creare un elenco puntato (unordered, non ordinato) i cui
elementi (voci) sono specificati all'interno dei tag <li> e </li>. Si consideri ad esempio il seguente
frammento di commento doc:
* <pxul>
* <ll>An < c o d e > l t e m U s t e n e r < / c o d e > object Is registered
" via < c o d e > a d d l t e m L i s t e n e r < / c o d e > .
" < l i > l t e m events are enabled via < c o d e > e n a b l e E v e n t s < / c o d e > .
* </ul>
Hashing
Introduzione
In questa appendice viene analizzato il concetto dell 'hashing. Si tratta di un'importante nozione
di programmazione che, sebbene molto utilizzata ai giorni d'oggi, risale agli albori dell'informa-
tica. La teoria deriva dagli studi eseguiti da Hans Peter Luhn presso i laboratori di ricerca della
IBM, pubblicati nel 1953. L'obiettivo consisteva nell'individuare una funzione in grado di mani-
polare chiavi di ricerca al fine di produrre un insieme uniformemente distribuito di variabili
randomiche. Una trattazione approfondita dell'argomento può essere trovata nel libro [ARTCP3].
La teoria dell'hashing è correntemente utilizzata in diversi ambiti dell'informatica e non solo:
dalle strutture dati, agli algoritmi di crittografia, alla verifica della correttezza delle trasmissioni
e così via. In questa appendice, però, l'attenzione è focalizzata esclusivamente sull'utilizzo
delì'hashing nel contesto delle strutture dati (hash table), che permette di organizzare i dati in
modo tale che la relativa ricerca sia estremamente efficiente (accesso diretto nei casi migliori).
Nel linguaggio di programmazione Java, il concetto òeW'hashing è costantemente utilizzato, sia
indirettamente (per esempio attraverso il ricorso a collezioni di dati come Hashtable e HashMap, sia
direttamente, ovcrriding del metodo hashCode ereditato dalla classe java.lang.Object. Specialmente
questo secondo utilizzo, sebbene presente sin dagli albori del linguaggio Java, continua a genera-
re una certa mescolanza di concetti e dubbi anche tra programmatori non più junior. Obiettivo di
questo capitolo, pertanto, è presentare una panoramica generale del concetto di hashing, corre-
data da esempi pratici, al fine di fugare dubbi e perplessità che circondano il concetto.
Un po' di teoria
L'hash table è una particolare struttura dati utilizzati per memorizzare insieme di informazioni
in modo tale che le operazioni di ricerca dei singoli elementi presentino una complessità asintotica
teorica del tipo 0(1). Ciò equivale ad affermare che le operazioni di ricerche possano avvenire
direttamente, ossia senza dover eseguire una serie di iterazioni. Come si vedrà di seguito, ciò è
possibile solo in condizioni "ottimali", mentre, nel peggiore dei casi (situazione assolutamente
poco probabile) si ottiene una complessità asintotica lineare all'input ( 0(n) ). Questo caso è
dato dallo scenario in cui tutti gli elementi generino il medesimo indirizzo relativo! Questa,
fortunatamente, è una situazione assolutamente inconsueta, mentre in casi normali le perfor-
mance di questi algoritmi sono molto elevate. In effetti sia il caso migliore, sia quello medio,
danno luogo a una complessità 0(1), anche se spesso a discapito di un'elevata occupazione di
memoria.
Come termini di paragone, si ricordi che ricerche in vettori non ordinati presentano una
complessità asintotica pari a 0(n)\ ciò significa che, nel caso peggiore (elemento non presente
nel vettore, o ultimo elemento ricercato), è necessario scorrere l'intero vettore prima di poter
addivenire a tale conclusione. Mentre ricerche di dicotomie in array ordinati e ricerche in alberi
binari (bilanciati) consentono di ottenere complessità asintotiche del tipo 0(log n). Analoga-
mente al caso delle hash tablc, anche gli alberi binari possono presentare, in situazioni sporadi-
che in cui gli alberi sia degenerati in liste, complessità asintotiche pari a 0(n).
L'idea alla base delle tabelle hash consiste nel sostituire il concetto di comparazione delle
tradizionali ricerche (l'elemento corrente è uguale a quello desiderato?) con quello di determi-
nazione a priori della posizione dell'elemento ricercato. In particolare, si prendono in conside-
razione funzioni in grado di trasformare la chiave degli elementi da memorizzare nel relativo
indirizzo. Queste funzioni sono per l'appunto chiamante funzioni di hash (Figura C.l).
In termini più formali, le funzioni hash, h, sono funzioni tali che
dove X è un elemento appartenente all'insieme dei possibili elementi (U) da dover memorizzare
nella struttura dati e N è l'insieme dei numeri naturali. h(x), pertanto, è una funzione che deve
essere in grado di associare un numero naturale (l'indirizzo) a ciascun elemento %.
Più precisamente, indicando con la lettera U l'universo degli elementi da memorizzare, e con
la lettera u la relativa dimensione (si assume che tale insieme sia finito, questa è un'assunzione
necessaria solo in teoria, mentre nella pratica può essere semplicemente trascurata assumendo
U molto grande), e T[1..m] (con m uguale alla dimensione del vettore), ne segue:
Pertanto la funzione h associa a ogni possibile % e U una ben definita posizione all'interno del
vettore T.
Si consideri, per esempio, il caso semplice in cui si abbia a che fare con insiemi di dati le cui
chiavi possano variare in un insieme ben definito, per esempio considerando una chiave di un
solo carattere alfabetico, si avrebbe una variazione nell'intervallo: A..Z. La realizzazione della
funzione hash, in questo caso, sarebbe piuttosto immediata: sarebbe sufficiente eseguire una
semplice sottrazione tra il carattere considerato e il valore ASCII del carattere A: 65.
Nei casi più frequenti, invece, l'insieme dei possibili elementi è di qualche ordine di grandez-
za superiore alla dimensione del vettore, tale da non rendere possibile il ricorso a un array la cui
dimensione m sia uguale a u. Pertanto si rende necessario considerare funzioni hash più com-
plesse e non esenti da problemi.
Figura C.l - Rappresentazione grafica del concetto di hash table.
La funzione hash inclusa nella classe Java String, per esempio, implementa l'algoritmo mo-
strato nel listato seguente. Il valore hash è ottenuto sommando il valore ASCII di ciascun carat-
tere per il prodotto del numero primo 31 per il valore hash calcolato nell'iterazione precedente:
i = x.lenglh
Non a caso il verbo hash significa "sminuzzare" e, per estensione, "pasticciare", "creare
contusione". Ed ecco Implementazione della funzione hash nella classe String.
it (h == 0) I
int off = offset;
char val[] = value;
int len = count;
hash = h;
I
return h;
Come si può notare, l'implementazione del metodo hashCode utilizza l'attributo hash della
classe String (e quindi non locale al metodo) per memorizzare il valore precedentemente calco-
lato. Questa tecnica è molto utile in generale (come si vedrà di seguito le operazioni di inseri-
mento, ricerca ed eliminazione di elementi da strutture dati hash possono richiedere di calcola-
re il valore hash degli elementi diverse volte) e in particolare per oggetti immutabili come quelli
di tipo stringa. In effetti, una volta che un oggetto di tipo stringa è stato impostato, non è
possibile variarne il contenuto a meno di non creare un nuovo oggetto.
Collisioni
La situazione in cui sia possibile dichiarare un array la cui dimensione (m) equivalga a quella
dell'universo dei possibili valori (u) è piuttosto rara. Anche qualora ciò fosse possibile, si prefe-
risce ricorrere ad array di dimensioni minori onde evitare di allocare rilevanti spazi di memoria
destinati a rimanere prevalentemente vuoti. Ciò, unito ad altri fattori, rende impossibile indivi-
duare, in casi non semplici, funzioni hash in grado di generare una corrispondenza biunivoca
tra l'insieme dei possibili elementi da memorizzare e l'insieme finito degli indirizzi. Per esem-
pio (cfr. [ARTCP3]), se si considera un insieme di 31 chiavi da memorizzare in una tabella hash
di 40 elementi, è possibile individuare 4 1 " = IO5" funzioni hash, di cui solo 41-40-39-38. ..11/
10! = 10 4i (una ogni 10 milioni di funzioni) permettono di realizzare una corrispondenza
biunivoca. Pertanto, funzioni che permettono di evitare duplicati (anche conoscendo a priori
l'insieme delle chiavi, scenario poco frequente) sono piuttosto rare da individuare, anche per
grandi dimensioni della tabella di hash.
Un esempio molto interessante è dato dal paradosso del compleanno (I.J. Good, Probability
and Weighing of Evidcnce, Griffin 1950), il quale afferma che se si raggruppa un insieme di 23
o più persone in una stanza, ci sono ottime possibilità che due o più persone condividano il
giorno del compleanno (chiaramente si considera esclusivamente il giorno e il mese). Pertanto,
considerando una funzione hash che esegue il mapping di 23 elementi in una tavola di 365
elementi (15.87 volte più grande dell'insieme degli elementi) la probabilità che non esistano
due elementi con il medesimo valore hash è pari a 0.4927 (meno della metà)! Pertanto, dati due
elementi diversi, è possibile che la funzione hash h generi uno stesso indirizzo:
h(x) = h ( y ) , x ! = y
In termini Java:
Questo scenario, denominato conflitto, è facilmente riscontrabile anche dall'analisi della fun-
zione utilizzata nel listato visto precedentemente: la funzione hash implementa un algoritmo
randomico e la chiave restituita è un intero (int).
Pertanto, le strutture dati hash table necessitano di stabilire e implementare opportune poli-
tiche per la gestione dei conflitti. Le strategie disponibili sono essenzialmente due e si differen-
ziano per via della struttura dati utilizzata per memorizzare i conflitti. In particolare è possibile
ricorrere a:
1. gestioni interne alla tabella hash (tecnica utilizzata nelle prime implementazioni della
Hashtable);
2. gestioni esterne alla tabella hash (tecnica utilizzata nelle versioni recenti della classe
Hashtable e nella classe HashMap);
Francesco
Roberto
)h(x) = h(x) + 1 = 6
Linear probing
Figura C.2 - Funzionamento della gestione interna di conflitti utilizzando l'esplorazione lineare.
4.3 aggiungi 1 all'indice /'+= 1
5. se la posizione non è libera (v[h+1] = nuli)
5.1 assegna i = -1\
5.2 finché la posizione non è libera (v[h+i] == nuli] e non si è giunti all'Inizio del vettore (h-i = 0);
5.2.1 verifica che l'elemento non sia uguale a quello di inserire ! v[h+1].equals(x)
5.2.2 se è uguale allora termina
5.2.3 sottrai 1 all'Indice i '= 1;
6. se la posizione è libera allora memorizza l'elemento v[h+i] = x
7. altrimenti genera eccezione.
Una variante di questo algoritmo consiste nello scorrere il vettore in un solo verso e, qualora
tutte le posizioni successive a quella home risultino occupate, procedere con l'ingrandimento
del vettore di base (rehash). Dall'analisi dello pseudocodice del listato appena visto è possibile
notare che la complessità asintotica dell'algoritmo, nel caso peggiore, non è più 0(1), bensì
dipende dal maggiore raggruppamento di conflitti. Pertanto, al fine di mantenere questa com-
plessità più prossima possibile al valore desiderato, un'efficace gestione delle tavole hash deve
prevedere meccanismi in grado di individuare possibili casi di degenerazione e quindi intra-
prendere opportuni provvedimenti, come per esempio ristrutturare l'intera tabella.
La gestione della struttura di dati bash presenta una discreta complessità non solo per l'inse-
rimento degli elementi, ma anche per le operazioni di reperimento e, addirittura, per la cancel-
lazione. In questo caso l'eliminazione di un elemento richiede di traslare tutti gli elementi suc-
cessivi con lo stesso valore di hash della chiave.
Un algoritmo che tenta di introdurre una prima astuzia nell'individuazione della successiva
posizione disponibile consiste nell'assegnare un incremento uguale alla radice quadrata del
valore hash. Questo algoritmo è detto squared probing ("esplorazione quadratica").
Questa tecnica chiaramente non risolve il problema di elementi diversi che generano la stessa
chiave. Invece tenta di ridurre il problema di conflitti "artificiosi", ossia di conflitti dovuti a
elementi che trovano la propria posizione home occupata da un elemento finito in quella posi-
zione dopo essersi imbattuto, a sua volta, in una serie di conflitti. Pertanto, sebbene la situazio-
ne non migliori per elementi che generano lo stesso indirizzo iniziale (questi generano lo stesso
pattern di indirizzi), tenta di risolvere situazioni in cui i raggruppamenti prolificano, cercando
di distribuire le aree dei conflitti. Il problema di queste due strategie, infatti, consiste nella
generazione di gruppi di elementi intorno ad aree occupate (cluster) creando problemi di per-
formance. In altre parole non si ha una distribuzione uniforme degli elementi. Questa situazio-
ne ha luogo dopo che un certo numero di inserimenti si siano risolti in una allocazione contigua
(priva di elementi disponibili) per via dei conflitti. Con il crescere di queste aree cresce anche la
probabilità che la posizione home dei nuovi elementi finisca all'interno dei vari raggruppamen-
ti, accrescendo ulteriormente il raggruppamento stesso, e quindi creando nuovi problemi di
performance. Inoltre la crescita dei raggruppamenti tende ad aumentare rapidamente non ap-
pena raggruppamenti continui comincino a fondersi creando raggruppamenti di dimensioni
ancora maggiori.
Una strategia utilizzata per risolvere parzialmente questo problema consiste nell'assegnare al
valore hash un fattore che non dipende dall'hash stesso bensì dalla chiave che lo ha generato. In
questo caso si parla di doublé hash (doppio hash). La funzione tipicamente utilizzata è:
h = ( h + h(x) ) mod m
Pertanto il nuovo valore di hash è ottenuto dal modulo della somma del valore attuale di
hash e di quello originario. Da notare che il valore attuale di hash non è necessariamente uguale
a quello originario, soprattutto dopo la reiterazione di conflitti.
L'ultima variante di gestione delle collisioni interne alla tabella considerata in questa appen-
dice è nota con il nome di ovcrflow. Questa consiste nel suddividere l'intera tabella in due aree:
una primaria destinata alla normale allocazione degli elementi, e un'altra, detta secondaria o di
ovcrflow, destinata a memorizzare gli elementi che danno luogo a collisioni. In particolare,
quando un elemento genera un conflitto, questo viene memorizzato in un'opportuna zona del-
l'area di ovcrflow il cui indirizzo è memorizzato nell'elemento che ne occupa la posizione home.
Se poi questo elemento già puntava a un altro elemento, allora si procede con l'aggiornare la
catena, aggiungendo il nuovo arrivato in testa alla coda (si fa riferire l'elemento nella lista pri-
maria al nuovo elemento inserito, al quale viene assegnato il riferimento all'elemento preceden-
temente riferito dall'elemento nella lista primaria). Questa tecnica è molto simile a quella delle
liste concatenate, ampiamente illustrate di seguito, con la variante che le liste sono memorizzate
all'interno dell'unica tabella. Pertanto si hanno liste logiche, con riferimenti a posizioni del
vettore piuttosto che riferimenti a posizioni in memoria. Per quanto con questa tecnica sia
necessario mantenere delle liste concatenate, la dimensione della tabella risulta limitata e l'ac-
cesso ai dati dovrebbe risultare più efficiente giacché i conflitti non inficiano l'area principale e
si accede sempre alla stessa tabella.
\ „ ^ \
Andrea
d
J * Francesco
^„ Enzo
^ s
Luca to AnnaMaria
)* s • s /
Vera
Figura C.J — Gestione esterna dei conflitti. Il diagramma mostra l'inverosimile situazione in cui
h(Francesco) = h(Andrea), h(Enzo) = h(Luca) = h(AnnaMaria).
K,V
Dictionary
K,V
Hashlable
h Hasblablei)
i- Hashtable(initialCapacity: int, loadFactor: float)
«tfilerface» [ K,V
k Hashtable(initialCapacity: int)
Map L"
t;
0 1 J, 1 ;
T
K,V
Enlry KeySet ValueColleclion Enumerator
1
- hash : ini
• key : K
- value : V EntrySet Empty Enumerator Emptylterator
conflitti). Pertanto, elementi diversi le cui chiavi danno luogo allo stesso valore hash sono me-
morizzate in una medesima lista di conflitti (figura C.3).
In questo caso, la complessità asintotica dell'algoritmo è data dalle dimensioni della lista di
conflitti di maggiori dimensioni. Nel diagramma precedente, il caso peggiore consiste nella
ricerca di un elemento non presente nella tabella la cui chiave generi un valore hash uguale a 4.
Per comprendere più chiaramente il funzionamento di questa struttura, si consideri come la
classe java.util.Hashtable implementa il metodo di get. Prima di procedere però è necessario forni-
re alcuni dettagli circa la struttura di questa classe (figura C.4).
Per chiarimenti circa la notazione UML si rimanda al Capitolo 7 del libro [UMLING].
Quello che interessa in questo contesto è che la classe java.util.Hashtable dispone di diverse
classi annidate (queste sono mostrate nel diagramma attraverso linee spezzate unite alla classe
Hashtable per mezzo di una circonferenza con il simbolo + inscritto). Una di queste è la classe
Entry, utilizzata per incapsulare gli elementi inseriti nelle istanze della classe Hashtable (table :
Entry[]). In particolare, ogni istanza di questa classe mantiene una copia del valore òeWhash
dell'elemento, la chiave (key), l'elemento stesso (value) e un riferimento al l'elemento Entry suc-
cessivo, se presente. Questa relazione di associazione è utilizzata per creare le liste concatenate
dette liste dei conflitti.
Per comprendere l'utilizzo degli elementi Entry, si consideri il listato che riportiamo poco
sotto, relativo al metodo put che permette di inserire nuovi elementi all'interno della collezione.
Come mostrato nella figura C.4, uno degli attributi principali della collezione Hashtable è un
array di oggetti di tipo Entry (table : Entry[]). Le istanze di questo tipo servono per incapsulare gli
elementi da inserire nella lista aggiungendovi, contestualmente, caratteristiche strutturali e
comportamentali necessarie per poterli organizzare in liste concatenate necessarie per gestire i
conflitti. La classe Entry (che vedremo successivamente), pertanto, memorizza una copia del
valore hash al fine di migliorare le performance, la chiave, l'elemento e l'indirizzo dell'elemento
successivo:
17 modCount++;
18 if (count >= threshold) I
19 '7 Rehash the table it the t h r e s h o l d is exceeded
20 rehash();
21 tab = table;
22 index = (hash & 0x7FFFFFFF) % tab.length;
23
29
Le istruzioni 3, 4, 5 servono per verificare che non si stia tentando di inserire un elemento
nullo. Da notare che invece ciò è possibile con la classe java.Util.HashMap. Le istruzioni 8 e 9
servono per generare il valore hash della chiave e per normalizzarlo in funzione alla dimensione
del vettore delle liste di collisione. In particolare la variabile index contiene l'indirizzo della lista
dei conflitti dove il nuovo elemento dovrà essere memorizzato. Il ciclo for (istruzione 10) è
utilizzato per scorrere l'intera lista dei conflitti al fine di individuare un elemento con la stessa
chiave. Se ciò accade (istruzione 11) allora l'elemento individuato viene restituito al metodo
chiamante subito dopo che al suo posto è stato inserito il nuovo elemento. Qualora non esista
un duplicato, si incrementa il contatore di variazioni strutturali (modCount, istruzione 17). Se
questo valore supera il fattore di soglia (threshold = capacità * fattore di caricamento), allora il
metodo procede con il riorganizzo della tabella interna (istruzione 20). In particolare, il meto-
do di rehash incrementa la dimensione della tabella interna e quindi riorganizza i vari elementi
presenti. Ciò determina anche la necessità di normalizzare nuovamente il valore di hash della
chiave del nuovo elemento (istruzione 22). Il compito delle istruzioni dalla 25 poi è di memoriz-
zare il nuovo elemento nella prima posizione della lista. A tal fine, viene memorizzato il riferi-
mento al primo elemento della lista di collisioni. Quindi al suo posto è inserito il nuovo elemen-
to, subito dopo averlo immesso in un nuovo oggetto di tipo Entry, il cui riferimento all'elemento
successivo contiene proprio l'indirizzo dell'elemento che precedentemente occupava la prima
posizione.
Si consideri ora l'implementazione del metodo get riportato nel listato seguente. In partico-
lare, dopo aver ottenuto il valore hash dell'elemento da cercare, il metodo esegue una sua
normalizzazione e quindi accede alla relativa lista di conflitti. Questa viene scorsa fino a quan-
do o si trova l'elemento cercato (in questo caso si utilizza la funzione equals: e.key.equals(key)),
oppure si giunge alla fine della lista.
return null;
I listati mostrati evidenziano come questa gestione dei conflitti permetta di avere una tabella
degli elementi di dimensioni prefissate e liste di conflitti in grado di crescere, teoricamente,
all'infinito.
Per quanto questo algoritmo sia molto semplice e di efficiente esecuzione, soffre del proble-
ma di generare valori molto simili e quindi, in ultima analisi, di dar luogo a un numero signifi-
cativo di conflitti.
Rotative Hash (hash rotativo)
Questa funzione permette di generare valori di hash eseguendo operazione al livello di bit sui
caratteri che costituiscono la stringa. In particolare, si imposta il valore iniziale di hash uguale
alla lunghezza della stringa, quindi, per ogni carattere, si eseguono le seguenti operazioni: si
sposta a sinistra di 4 posizioni il corrente valore hash (in sostanza lo si moltiplica per un fattore
pari a 16), quindi lo si mette in xor con il valore hash questa volta spostato a sinistra di 28
posizioni, il tutto viene posto nuovamente in xor con il valore ASCII del carattere corrente.
Ecco un esempio di rotative hash.
Questo algoritmo permette di individuare buoni valori di hash, senza aggravare il costo
computazionale che resta decisamente contenuto: si eseguono esclusivamente operazioni binarie.
Bernstein's Hash
L'algoritmo di Bernstein è molto semplice e. al contempo, permette di ottenere interessanti risul-
tati. In questo caso il valore di hash, inizialmente impostato uguale a zero, viene sommato al
valore ASCII di ogni carattere componente della stringa, dopo essere stato moltiplicato per il
numero 65.
RETURN ( I N T ) ( M A T H . A B S ( H A S H ) % SIZE);
Anche in questo caso si hanno risultati molto buoni con ottime performance.
CBU hash
Il nome è legato alla Università di Canterbury, di Christchurch, Nuova Zelanda (Canterbury
University) dove è stato ideato. Anche in questo caso il valore hash è azzerato inizialmente, e poi,
per ogni carattere delle stringa, è spostato di 2 posizioni a sinistra e addizionato al valore ASCII
del carattere corrente. Questo algoritmo, oltre ad essere molto efficiente, ha dimostrato di fun-
zionare bene soprattutto con stringhe di caratteri: caratteristiche che lo rendono molto interes-
sante, tanto che sue variazioni sono utilizzate in altri algoritmi come il Revision Control System.
PKP hash
Questo algoritmo deve il proprio nome alle iniziali del suo inventore: Peter K. Pearson il quale
pubblicò nel 1990 un algoritmo in grado di generare numeri pseudorandomici nell'intervallo
da 0 a 255 (byte). L'idea base era molto semplice e consisteva in un primo passo di inizializzazione
necessario per generare un vettore con i primi 255 numeri inseriti in ordine casuale. Dopodiché,
il codice ASCII di ciascun carattere della stringa, dopo essere stato posto in xor con il corrente
valore hash, veniva usato come indice del vettore al fine di prelevare il numero corrispondente.
L'algoritmo è strutturato come mostrato nel listato seguente.
long hash = 0;
lor (int i=0; i < source.Iength(); i++) I
hash = pTab[ hash A source.charAt(i)];
I
return hash;
I
Questo algoritmo funzionante al livello di byte, per essere esteso al condominio dei long ri-
chiede di considerare ben otto contributi (uno per ciascun byte che forma il long). Pertanto, si
può immaginare di eseguire in parallelo 8 PKP hashing, ognuno relativo ad uno dei byte del tipo
long. La versione estesa di questo algoritmo (listato riportato poco sotto) necessita pertanto di
' Default c o n s t r u c t o r
public PKPHashFunction() (
initPTabs();
I
/ "
(
// r a n d o m i s e the array
Random randomGenerator = new Random();
for (int ind = 0; Ind < size; ind++) (
// select the position t o s w a p w i t h the c u r r e n t one
int pos = randomGenerator.nextlnt(256);
/ / s w a p bytes
int currByte = randomArray[ind];
randomArray[ind] = randomArray[pos];
randomArray[pos] = currByte;
I
return randomArray;
Un test informale
Gli algoritmi presentati precedentemente sono stati sottoposti a una serie di verifiche. In parti-
colare, si sono considerati tre insiemi di input e, per dimensioni crescenti della tabella di hash,
si è misurato il numero dei conflitti generati. Sebbene questo valore da solo non sia molto
significativo (sarebbe necessario analizzarlo congiuntamente con altri dati quali dimensioni dei
cluster, distribuzione dei conflitti, etc., ma ci vorrebbe un testo solo per questo), è comunque
utile per fornire un iniziale apprezzamento circa la bontà delle varie funzioni. Poiché una qua-
lità importante richiesta agli algoritmi hash consiste nel distribuire uniformemente le chiavi di
ingresso sulla relativa tabella, sono riportati due grafici con le distribuzioni generate da due ben
definiti insiemi di input su due tabelle di dimensioni ben definite.
I tre vettori di input utilizzati sono, rispettivamente, un insieme di nomi di personaggi cele-
bri, i codici ISIN di alcuni strumenti finanziari dell'India e, per terzo, i codici di svariate com-
pagnie aree.
Di seguito sono riportati i valori degli array utilizzati.
Vettore dei codici (ISIN) di alcuni prodotti finanziari indiani (178 elementi)
private static String e l e m e n t l S I N s [ ] = I
"INA05A000079", "INA12A000120", "INA15A000150", "INB10A002614", "INE253B01015", "INA03A730016",
"INA14A830045", "INA12A840053", "INA14A840101", "INA15A850075", "INE761C01015", "INE762C01013",
"INE673C01012", "INE130C07010", "INE130C07044", "INE130C07051", "INE130C01013", "INE226B01011",
"INE208A14014", "INE208A14022","INE208A14030", "INE208A07018", "INE208A07026", "INE208A07034",
"INE208A07042", "INE208A01011", "INE293C01019", "INE046C01011", "INE441A01018","INE363A01014",
"INE237D01014", "INE787D14029", "INE294A14022", "INE294A14030", "INE294A14048", "INE294A14055",
"INE294A07018", "INE828A01016", "INE972C01018", "INE436C01014", "INE160C01010", "INE279C01018",
"INE824B01013", "INE604B01019", "INE069C01013", "INE986A01012", "INE605B01016", "INE819C01011",
"INE702C01019", "INE829B01012", "INE751C01016", "INE151D01017", "INE084D01010", "INE419D01018",
"INE085A01013", "INE194C01019", "INE713D01014", "INE368D01017", "INE974C01014", "INE442C01012",
"INE953B01010", "INE777B01013", "INE088A01017", "INE488A01019", "INE178A01016", "INE217C01018",
"INE132B01011", "INE974B01016", "INE489A01017", "INE937D14012", "INE937D14020", "INE820A14016",
"INE820A14024", "INE820A01013", "IN9820A01011", "INE086A01011", "INE095B01010", "INE319D01010",
"INE285A01019", "INE671B01018", "INE652C01016", "INE541A01015", "INE353B01013", "INE311B01011",
"INE106B01015", "INE426D01013", "INE038A08017", "INE353A01015", "INE074C01013", "INE310C01011",
"INE549A14011", "INE549A14029", "INE549A01018", "INE551A01014", "INE005A11887", "INE005A11895",
"INE005A11903", "INE005A11911", "INE005A14048", "INE005A14055", "INE005A14063", "INE005A14071",
"INE005A14089", "INE005A14097", "INE005A08BW8", "INE005A11853", "INE005A08BN7", "INE849D14019",
"INE444C01018", "INE008A08101", "INE008A08127", "INE069A07030", "INE069A07048", "INE069A07055",
"INE786B01022", "INE569A01024", "IN9569A01030", "IN9569A01048", "INE966C01010", "INE058D01014",
"INE575A01013", "INE916B01017", "INE307C01017", "INE028C01019", "INE802B01019", "INE635B04017",
"INE770B01018", "INE166B01019", "INE980A01015", "INE946A01016", "INE206D08014", "INE206D08022",
"INE699B01019", "INE843A01015", "INE787B01012", "INE844A01013", "INE855B01017", "INE165C01019",
"INE269D01017", "INE700B01015", "INE179A01014", "INE002A08062", "INE002A08104", "INE002A08112",
"INE002A08120", "INE002A08138", "INE266C01015", "INE640C01011", "INE455D01012", "INE075B01012",
"INE001C01016", "INE298B01010", "INE710B01014", "INE461D01010", "INE311D01017", "INE295B01016",
"INE722B01019", "INE017A07369", "INE017A07377", "INE017A07385", "INE188D01019", "INE017B01014",
"INE822C01015", "IN9965A01014", "IN9965A01022", "IN9965A01030", "IN9965A01048", "IN9965A01055",
"INE216B01012", "INE551D01018", "INE650C01010", "INE429B01011", "INE718A01019", "INF725B01060",
" I N F 7 2 5 B 0 1 1 3 6 " , " I N F 7 2 5 B 0 1 1 4 4 " , " I N F 7 2 5 B 0 1 1 0 2 " , " I N F 7 2 5 B 0 1 1 1 0 " |;
V e t t o r e n o m i (228 e l e m e n t i )
Size Rotating Bernstein Default SBU PKP Media
97 139 143 141 137 136 139.2
109 135 133 137 127 133 133
127 122 116 117 124 116 119
149 107 107 115 113 111 110.6
173 98 101 99 99 103 100
223 83 79 93 75 88 83.6
239 82 83 77 87 73 80.4
263 69 74 78 74 73 73.6
317 65 63 58 67 63 63.2
503 52 41 50 41 48 46.4
719 33 35 34 26 31 31.8
1019 16 29 23 33 19 24
1187 11 21 18 28 22 20
Media 77.84615 78.84615 80 79. 30769 78. 15385
Tabella C.l - Numero di conflitti generati con il vettore dei nomi per dimensioni crescenti della
tabella di hash.
v e t t o r e ISIN ( 1 7 8 e l e m e n t i )
Size Rotating Bernstein Default SBU PKP Media
97 91 96 94 96 100 95.4
109 88 91 93 89 92 90.6
127 84 80 80 79 86 81.8
149 71 80 73 79 79 76.4
173 62 64 63 67 77 66.6
223 53 63 55 49 51 54.2
239 52 58 55 52 63 56
263 53 51 52 50 55 52.2
317 45 36 38 43 45 41.4
503 30 29 25 31 29 28.8
719 17 17 20 24 26 20.8
1019 8 11 13 10 12 10.8
1187 8 19 11 10 16 12.8
Media 50.92308 53.46154 51.69231 52.23077 56.23077
Tabella C.2 - Numero di conflitti risultanti con il vettore dei codici dei prodotti finanziari per
dimensioni crescenti della tabella di hash.
v e t t o r e c o d i c i v e t t o r i a e r e i (198 e l e m e n t i )
Size Rotating Bernstein Default SBU PKP Media
97 113 116 113 114 115 114.2
109 109 96 103 100 111 103.8
127 95 92 96 97 94 94.8
149 90 86 82 89 85 86.4
173 70 74 69 81 79 74.6
223 64 63 67 81 70 69
239 58 55 51 77 62 60.6
263 54 74 51 79 62 64
317 45 39 45 75 45 49.8
503 25 34 22 71 35 37.4
719 30 21 15 72 31 33.8
1019 22 7 10 72 18 25.8
1187 26 18 7 77 20 29.6
Media 61.61538 59.61538 56.23077 83. 46154 63. 61538
Tabella C. 3 - Conflitti risultanti con il vettore dei codici delle linee aeree per dimensioni crescenti
della tabella di hash
0 1 2 3 4 5 6 7 8 9 10 11 12 1 ] 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
Rotating 1 0 2 3 1 1 0 1 0 0 0 1 0 0 0 1 0 2 1 1 1 1 0 1 0 0 2 1 3 0 0 4 2 0 2 2 0 2 1 1 0 1 1
Bernsteinl 0 0 2 2 0 0 0 0 1 0 1 0 0 2 2 2 0 0 0 2 4 1 0 1 0 0 1 1 0 2 0 2 2 1 0 2 0 0 2 1 3 2
Default 4 1 0 1 3 2 0 0 0 2 0 0 2 2 0 1 0 0 0 0 1 0 1 0 0 0 2 3 2 1 1 1 0 2 1 0 0 1 0 2 0 1 3
SBU 1 1 3 0 1 0 0 1 0 2 2 1 1 1 0 1 1 1 1 5 2 0 0 0 3 0 2 0 3 1 0 1 1 0 1 1 0 1 1 0 0 0 0
PKP 1 1 2 0 1 1 1 0 2 0 3 0 2 2 0 0 2 0 0 1 0 1 0 0 1 2 1 0 1 2 1 1 1 1 1 1 3 0 2 0 1 0 1
Tabella C.4- Distribuzione dei primi 4 elementi dell'array dei nomi in una tabella di 43 posizioni.
L'analisi del numero di conflitti, come detto sopra, considerato da solo, non è un fattore
molto significativo. In effetti, le varie funzioni hash sembrerebbero fornire le stesse prestazioni
in termini del numero di conflitti. Tuttavia, è possibile trarre alcune considerazioni iniziali. In
primo luogo, è possibile notare che con tabelle di bash di dimensioni circa uguali a quelle
dell'insieme dei vettori di input si ottiene circa il 35-40% dei conflitti. Mentre per scendere ad
un numero di conflitti oscillanti tra il 20-25% è necessario ricorrere a una tabella hash di di-
mensioni doppie rispetto al numero dei valori di input. La presenza di conflitti, però ancora
non permette di trarre delle conclusioni circa le prestazioni. Per poter valutare questa caratte-
ristica è necessario considerare la distribuzione delle chiavi sulla tabella hash. A tal fine si è
deciso di
Dall'analisi delle distribuzioni di tabella C.4 è possibile notare che, nel caso in cui la dimen-
sione della tabella di hash sia circa uguale a quella dell'insieme delle chiavi di input, la ricerca di
un elemento tende a presentare una complessità media veramente del tipo 0(1 ). Il caso peggiore
è dato dall'algoritmo CBU che alloca 5 elementi nella posizione 19. Pertanto, utilizzando tale
funzione, nel caso in cui si cerchi un elemento non presente nella tabella il cui valore hash della
chiave sia 19 la complessità è 0(5). Sempre dall'analisi della tabella C.4, è possibile notare che le
varie funzioni, per quanto concerne la distribuzione degli elementi, hanno comportamenti molto
differenti. Tra questi si evidenzia negativamente la distribuzione generata dall'algoritmo CBU.
6
Rotating • Bernstein * Default SBU * PKP
5
0 1 2 J 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
Figura C.5 - Grafico delle distribuzioni dei primi 40 elementi dell'array dei nomi in una tabella di
43 posizioni.
Mentre, sempre da questo test, sembrerebbe che la funzione hash in grado di generare un
comportamento maggiormente uniforme sia la PKP.
In figura C.6 è mostrata la distribuzione generata da 40 elementi distribuiti su una tabella
hash di dimensioni poco superiori al doppio di tali elementi. In questo caso è possibile notare
che la complessità asintotica è più marcatamente di tipo 0(1). Questo test, inoltre, mostra un
pessimo comportamento delle funzioni hash CBU (3 posizioni con 3 elementi e 5 con 2) e PKP
(2 posizioni con 3 elementi e 6 con 2) e un ottimo comportamento della funzione hash standard
e di quella di Bernstein.
Figura C.6 - Grafico delle distribuzioni dei primi 40 elementi dell'array dei codici dei vettori aerei
memorizzati in una tabella di 89 posizioni.
Multi-threading
Introduzione
Questa appendice è dedicata alla presentazione della tecnica di programmazione nota con il
nome di multithreading (MT). In particolare, illustriamo sia i necessari elementi teorici di base,
sia gli strumenti tradizionali del linguaggio Java atti a supportare il MT - per esempio il costrut-
to synchronized, i metodi wait and notify - sia quelli più innovativi introdotti con la versione 5 (il
package java.util.concurrent).
Poiché il MT è una tecnica di programmazione, il linguaggio selezionato finisce giocoforza
per offrire una serie di opportunità peculiari e porre immancabili vincoli la cui intima com-
prensione è propedeutica alla produzione di efficaci sistemi MT (cioè... che funzionano). Vista
la complessità della materia le varie nozioni sono esposte in maniera intuitiva, includendo esempi
pratici, tralasciando, spesso e volentieri, il sicuro ambito dell'esposizione accademica. Il tutto
per favorire una comprensione operativa delle varie nozioni. Ciò nonostante, alcuni argomenti
presentati, potranno essere apprezzati nella loro interezza solo dopo una attenta rilettura del-
l'intera appendice.
Una volta introdotti i concetti base del MT, con particolare riferimento all'ambiente Java,
viene presentato il package inerente la concorrenza (java.util.concurrent) introdotto inizialmente
con la piattaforma Java 2 versione 5.0, di cui è stato effettuato il hackporting nella versione 1.4
(pur con qualche difficoltà dovuta anche alle recenti modifiche della java Virtual Machine).
Questo package permette di realizzare avanzate applicazione MT, rimuovendo l'onere di dover
codificare in termini di primitive per la concorrenza a basso livello di astrazione. Tuttavia, non
elimina (e non potrebbe) la necessità di un'approfondita comprensione delle problematiche
del MT. Inoltre, la padronanza di questi concetti permette di comprendere più intimamente
meccanismi base, funzionamenti, limiti e problematiche, di sistemi a maggior grado di astrazio-
ne - quali per esempio web server , application server, micro container - in cui la gestione MT
avviene in modo trasparente al programmatore.
Il package java.util.concurrent è costituito di ben 47 classi e 15 interfacce. Tale ricchezza, pur-
troppo, ne impedisce una trattazione esaustiva: Doug Lea ha scritto un intero libro dedicato a
questa libreria (cfr.: [CNPRGJ]). Ci auguriamo di semplificare il lavoro dei lettori alle prese con
la progettazione di applicazioni MT Java, evitando eventuali inutili investimenti di energie volti
a realizzare strumenti già disponibili.
Obiettivi
L'obiettivo di questo capitolo è quello di illustrare la programmazione MT. A tal fine è stato
organizzato in tre parti logiche. La prima presenta una serie di importanti nozioni teoriche. In
particolare, partendo da concetti come thread e processi, si prosegue con l'illustrazione del
mapping con i rispettivi elementi del sistema operativo, la gestione della memoria, le
problematiche tipiche del MT, e così via. La comprensione di questa sezione è pertanto fonda-
mentale per poter comprendere pienamente le problematiche che circondano il mondo della
programmazione MT.
La seconda parte illustra i tradizionali meccanismi messi a disposizione dal linguaggio Java
per la produzione di applicazioni M T : synchronized, i metodi wait and notify, etc. Come si vedrà,
l'iniziale disegno Java non è stato sempre privo di problemi. Questa sezione è molto utile, sia
perché molte importanti applicazioni sviluppate in Java sono ancora basate sui meccanismi di
M T "vecchio stile", sia per comprendere più approfonditamente i vantaggi e i meccanismi
introdotti con il nuovo package relativo alla concorrenza.
L'ultima parte è dedicata a una panoramica del package java.util.concurrent, frutto del lavoro di
Doug Lea.
Nozioni di base
Thread e processi
Che cosa è un thread (71? La definizione formale, forse eccessivamente astratta, recita che: un
thread è un singolo flusso sequenziale di controllo appartenente ad un processo...
Molto probabilmente, i primi programmi Java scritti da ciascun programmatore erano orga-
nizzati secondo una rigida successione di istruzioni che, a partire dal metodo statico main, evol-
veva fino al relativo completamento. La relativa esecuzione del programma era basata sul cosid-
detto modello "single-threaded" (a singolo flusso) caratterizzata da un'esecuzione assoluta-
mente deterministica.
In tale modello, la J V M inizia eseguendo la prima istruzione del metodo main, esegue la secon-
da, poi la terza, e così fino al completamento dell'esecuzione dell'ultima istruzione (figura D.l).
Quindi ogni istruzione, ogni metodo viene eseguito completamente, secondo la rigida sequenza
definita dal programma, prima di passare all'esecuzione dell'istruzione/metodo successivo. In
particolare, all'atto dell'avvio del programma, la JVM, esegue l'impostazione iniziale, e poi si
occupa di generare un singolo T (di tipo non-daemon) incaricato dell'esecuzione del programma.
Da notare che, anche qualora il programma preveda un solo T, la J V M c o m u n q u e ne crea e
gestisce diversi: oltre a quello dedicato all'esecuzione del m a i r i , infatti, vi sono una serie T di
l'allocazione di determinate risorse, tra cui la C P U . Tuttavia, poiché questi non vanno ad agire
sugli stessi elementi del nostro programma (a m e n o che questi non siano stati rilasciati), la loro
Java: infatti questo conclude la propria esecuzione quando tutti i thread non-dacmon finiscono la
1. codice;
2. dati;
3. stack;
4. file di I/O;
5. tabelle dei segnali.
prerequisiti richiesti. Tuttavia, è possibile continuare nella lettura del presente capitolo anche
1. uno a molti;
2. uno a uno;
3. molti a molti.
Nel primo caso, diversi T al livello utente sono associati a uno solo al livello kernel (modello
denominato "green threads"). Ciò fa sì che l'applicazione possa creare un numero qualsiasi di T,
la cui attività però è ristretta allo spazio utente. Pertanto, se un T effettua una chiamata bloc-
cante, tutto il processo viene interrotto, e, poiché solo un T alla volta può accedere al kernel, ne
segue che non è possibile dar luogo ad un vero parallelismo, anche in presenza di diverse CPU.
Il secondo caso, uno a uno, potrebbe sembrare quello più naturale, e in effetti offre diversi
vantaggi. Purtroppo però, questi sono vincolati a un certo numero di rilevanti problemi relativi
a restrizioni inerenti il numero di T che un'applicazione può eseguire, eccessivo utilizzo delle
risorse, con conseguente degrado delle performance, etc.
La soluzione ottimale è pertanto la terza, rappresentata dal compromesso di un mapping
molti a molti, denominata modello a due livelli (two levels model). Si tratta di quella adottata
dalla maggioranza degli OS, ed è in grado di fornire una buona approssimazione dei vantaggi
delle precedenti strategie. In particolare, un'applicazione può operare con un numero arbitra-
rio di T senza essere particolarmente penalizzata dai costi dell'esecuzione di ulteriori T. La
qualità di questo modello dipende moltissimo dallo scheduler operante al livello di user space.
Molti a uno Uno a uno Molti a molti
X
Applicazione Java Applicazione Java Applicazione Java
£
Usjer spac< ¡er spac^ Usersbace
!
I I
£
I •
Figura D.3 - Schematizzazione concettuale del mapping tra thread delle applicazioni Java e dei
corrispondenti al livello di kernel.
1. una serie di stack (pila, aree di memoria gestite secondo il principo LIFO, Last In First
Out);
2. heap (mucchio);
3. l'area dei metodi.
Ogniqualvolta la JVM genera un nuovo T, questo è fornito di uno stack dedicato, destinato
ad ospitare variabili locali al T stesso (e quindi non accessibili da parte di altri T), parametri,
valori e punti di ritorno dai metodi invocati dal T. Questa area può ospitare esclusivamente tipi
primitivi e puntatori a oggetti (reference). In Java "collezioni" come String, Array, etc. sono rap-
presentate da classi e quindi oggetti a tempo di esecuzione. Il linguaggio Java, inoltre, prevede
il passaggio dei parametri by value con alcune cautele. Per esempio il passaggio di un valore
intero (int), viene implementato copiando effettivamente il relativo valore nello stack: quindi se
il valore della variabile è 3, 3 è il valore che verrà copiato nello stack. L'adozione della stessa
tecnica tuttavia non sarebbe invece fattibile (sarebbe assolutamente inefficiente e complessa)
qualora fosse necessario passare un oggetto. Pertanto, in questa circostanza, il valore copiato
nello stack non è l'oggetto stesso ma il relativo puntatore. Ciò fa sì che sia il metodo chiamante,
sia quello chiamato, conservino un puntatore distinto allo stesso oggetto. Quindi se l'oggetto
UserPojo ha indirizzo 2231231, sullo stack non verranno copiato lo stato dell'oggetto UserPojo,
ma l'indirizzo 2231231 che verrà assegnato ad un'altra variabile.
Gli oggetti risiedono esclusivamente nello spazio di memoria heap. La JVM ne gestisce uno
solo "condiviso" da tutti i thread. L'altra area di memoria gestita dalla JVM è quella riservata ai
metodi e alle costanti (attributi statici) ed è condivisa da tutti i thread.
Il fatto che ogni T sia fornito di un proprio stack fa sì che metodi che operino esclusivamente
sui parametri di ingresso e variabili locali, non necessitino di alcun tipo di sincronizzazione.
Questo tipo di metodi, molto vantaggioso per la programmazione concorrente, sono detti rien-
tranti. Ecco un semplice esempio di metodo rientrante: metodo Shuttle della classe
java.util.Collections.
Si tratta del metodo shuttle incluso nella classe java.util.Collections. Il suo compito è quello di
cambiare randomicamente la posizione degli elementi presenti nella lista fornita come parame-
tro. Questo algoritmo al suo interno esegue diversi compiti e nel caso di liste genera anche un
array temporaneo, etc. Ciò nonostante non richiede sincronizzazione in quanto, utilizzando
solo i parametri di ingresso e variabili locali, allocati nello stack riservato al T, non deve accede-
re ad aree condivise. Ciò fa sì che diversi T possano eseguire questo metodo codice contempo-
raneamente senza interferire gli uni con gli altri.
In particolare la definizione di metodo rientrante, per quanto strettamente legata al concetto
del MT, può essere formulata come segue: "un metodo è detto rientrante se una sua esecuzione
da parte di un qualsiasi T può essere interrotta in ogni momento, e il metodo stesso può essere
eseguito da un altro T fino al suo completamento, senza che le due evoluzioni interferiscano in
alcun modo e senza che il primo T debba essere eseguito nuovamente per consentire al secondo
di poter terminare".
Un modo per ottenere ciò consiste nel far in modo che il metodo utilizzi aree di memorie non
condivise: tutti i dati sono passati come parametri e/o valori di ritorno, come nel listato riporta-
to poco sopra.
La proprietà di rientranza è particolarmente utile in quanto permette di ottenere elevati
livelli di concorrenza ed invocazioni ricorsiva dei metodi.
\
Thread
Thread
\ P A R A L L E L I S M O C O N C O R R E h IZi
Thread n ' « H i Thread n - 1 Thread n - i f l
|
Thread 2 ' i i H a l H Thread Thread-
3
Thread V-ftj
ì .
r
I I I 1
tempo di esecuzione empo d esecuzione
Come visto in precedenza, la maggior parte di OS utilizza scheduler la cui politica di assegna-
zione della CPU è basata sulla selezione del T in stato di attesa a più alta priorità. Il problema
è che diversi OS prevedono differenti livelli di priorità. Qualora questi siano superiori o uguali
ai dieci livelli (per esempio, Sun Solaris assegna 31 bit, quindi 2 " = 2 Giga, all'attributo di
priorità) si tratta di risolvere un semplice esercizio di mapping, mentre quando il numero è
inferiore (Windows NT dispone di appena sette livelli di priorità), la situazione diviene più
problematica (bisogna associare più priorità concettuali ad una stessa fisica). A complicare le
cose poi intervengono alcuni servizi particolari di specifici OS (per esempio Window NT) che
operano sulla priorità dei T, aumentandola o riducendola, in funzione dell'esecuzione di
prestabilite operazioni. Questa tecnica è nota con il nome di priority boosting e tipicamente può
essere disabilitata attraverso opportune chiamate native, quindi non incluse nel linguaggio Java,
magari effettuate attraverso il linguaggio C.
Problematiche del MT
Il funzionamento tipico di applicazioni MT prevede che i vari T evolvano indipendentemente
gli uni dagli altri e di tanto in tanto interagiscano, più o meno direttamente, per sincronizzarsi,
per scambiare informazioni, per aggiornare dati memorizzati in aree condivise, e così via. Per-
tanto, molto frequente è il caso in cui diversi T debbano condividere appositi oggetti, e quindi
aree di memoria heap. La condivisione di risorse, se non progettata e implementata corretta-
mente, come lecito attendersi può dar luogo a diversi problemi piuttosto seri, spesso di difficile
identificazione. I problemi più noti sono riportati di seguito.
Starvation (inedia)
Questo problema genera un risultato per volti versi simile a quello prodotto da un deadlock, ma
in questo caso le cause del blocco hanno una diversa connotazione. In particolare, si ha una
situazione di inedia quando, sebbene in linea di principio uno o più T possano evolvere, prati-
camente, non lo fanno poiché non riescono ad ottenere una o più risorse necessarie per il
proseguimento della loro esecuzione. La risorsa che più frequentemente può generare l'inedia
di un T è indubbiamente la CPU. In questi casi un T non riesce a proseguire per via dell'impos-
sibilità di aggiudicarsi cicli di CPU. Situazioni di inedia si possono generare in diversi modi, tra
i quali i più comuni sono il risultato di una inappropriata manipolazione della priorità di un
insieme di T, della generazione di cicli infiniti da parte di un T che mantiene bloccata una
risorsa condivisa, etc. Un particolare esempio di inedia, denominato blocco di vita (livelock), si
ha nel caso in cui un T, non bloccato ma attesa della disponibilità di una risorsa, non riesca a
proseguire per la propria esecuzione in quanto impedito dalla necessità di ripetere l'esecuzione
di un'istruzione che fallisce.
public ThreadExampleO I
try I
w h i l e (¡Thread.curren/777reac(().islnterrupted()) I
Sy ste m. ouf. p ri ntl n ( " — > thread:"
+ Thread.currentThreadQ.geVdQ + "- count:" + (count++) + " — " ) ;
Thread.s/eep(500);
I catch (InterruptedException e) I
// ignore this exception
S y s t e m . o u f . p r i n t l n f — > thread:" + Thread.currentThread{).ge\\û() + " Interrupted");
I
try I
Th read. s/eep(5000) ;
) catch (InterruptedException e) I
e.printStackTracef); // ignore this exception
)
— > thread:7- c o u n t : 0 —
— > thread:8- c o u n L O —
— > thread:9- c o u n t : 0 —
— > thread:8- c o u n t : 1 —
— > thread:9- c o u n t : 1 —
— > thread:7- c o u n t : 1 —
— > thread:9- c o u n t : 2 —
— > thread:8- c o u n t : 2 —
— > thread:7- c o u n t : 2 —
— > thread:7- c o u n t : 3 —
— > thread:8- c o u n t : 3 —
— > thread:9- c o u n t : 3 —
— > thread:7- c o u n t : 4 —
— > thread:8- c o u n t : 4 —
— > thread:7- c o u n t : 9 —
— > thread:8- c o u n t : 9 —
— > thread:9- c o u n t : 9 —
— > thread:7- c o u n t : 1 0 —
— > thread:9- c o u n t : 1 0 —
— > thread:7 Interrupted
— > thread:7 stopped- Count:11
— > thread:8- count: 1 0 —
— > thread:9 Interrupted
— > thread:9 stopped- Count:11
— > thread:8 Interrupted
— > thread:8 stopped- Count:11
Come si può notare il thread id inizia da 7. Proprio a testimoniare la presenza di altri thread
in esecuzione. Inoltre, le diverse esecuzioni della stessa classe tendono a generare diversi output,
proprio perché l'esecuzione dei tre T è assolutamente indeterministica.
Si analizzi ora la versione della classe che, invece di implementare l'interfaccia Runnable, estende
la classe java.lang.Thread. In questo listato è riportata la classe ThreadExample che estende la classe
java.lang.Thread.
public ThreadExampleO I
I
try!
w h i l e ( !islnterrupted() ) I
System.oi/f.println("—> thread:"+ Thread.currentThread().ge\\à() + "- count:"+ (count++) + " — " ) ;
Thread.s/eep(500);
I
I catch (InterruptedException e) {
// ignore this exception
S y s t e m . o u f . p r i n t l n f — > thread:" + T h r e a d . c u r r e n t T h r e a d Q . g e M Q + " Interrupted");
)
try I
Thread.s/eep(5000);
I catch (InterrupledExcepllon e) {
e.printStackTrace(); / / ignore this exception
Il diagramma di figura D.5 illustra una prima versione semplificata del ciclo di vita dei T in
Java. Questo diagramma presenta un elevato livello concettuale compatibile con i concetti in-
trodotti fino a questo punto. Nei seguenti paragrafi (figura D.7) è presentata una versione a
maggior livello di dettaglio.
Un oggetto di tipo T, una volta creato, resta in stato di inattività (NEW) fintanto che non ne
viene invocato il comando Start. L'esecuzione di questo fa sì che il T passi allo stato di pronto
all'esecuzione. Quindi, tutto è pronto e il T attende che lo scheduler gli assegni la CPU, natural-
new Thread ()
New
starti)
stop!).
termine del metodo run()
Ready -to-run
CPU
yield!) Assegnata assegna
ad un altro T CPU
Running
stop!),
termine del metodo run()
Dead
mente contesa da diversi T, per iniziare ad eseguire il metodo run. Una volta ottenuta anche
questa preziosissima risorsa, il T passa finalmente nello stato di esecuzione. La CPU, tipica-
mente, viene assegnata per intervalli di tempo ben definiti (time-slice), scaduti il quale, il T
torna allo stato di pronto per essere eseguito. Alcuni modelli MT invece prevedono che la CPU
sia assegnata ad un T fintanto che questo non decida di rilasciarla. Ciò avviene attraverso l'ese-
cuzione del metodo yield che forza il T a rilasciare esplicitamente la CPU. Un T termina per
sempre, o quando finisce l'esecuzione del proprio metodo run (tipicamente ciò avviene uscendo
da un opportuno ciclo) oppure quando viene invocato il metodo di Stop. Come si vedrà di
seguito, l'invocazione del metodo stop forza la terminazione brusca del T e pertanto non do-
vrebbe mai venir utilizzata. I T dovrebbero essere sempre conclusi in maniera programmatica.
I monitor e la sincronizzazione
L'esempio mostrato nei listati precedenti è molto semplice: non ci sono problemi di risorse condi-
vise, i T non necessitano di interagire, etc. Purtroppo scenari del genere sono molto rari e rappre-
sentano le condizioni ideali per ottenere il massimo delle prestazioni da codici concorrenti.
In situazioni reali i diversi T devono interagire e quindi si hanno i vari problemi di sincroniz-
zazione, di condivisione delle risorse, etc. Queste problematiche, che rappresentano una delle
sfide principali nel disegno ed implementazione di programmi MT, tradizionalmente, erano
risolte utilizzando apposite parole chiavi (come: syncronized e volatile) e metodi (quali, per esem-
pio, le primitive: waitQ, notify(), notifyAII(), contenuti nella classe antenata - java.lang.Object - da cui
ereditano tutte le altre) utilissimi per la programmazione MT... I programmatori C++ sanno
benissimo quali ostacoli derivino dalla mancanza di queste primitive...
Nella quasi totalità delle applicazioni MT si verifica il caso in cui diversi T debbano conten-
dersi l'accesso a risorse condivise e che quindi gli accessi debbano avvenire in mutua esclusio-
ne. A tal fine ogni linguaggio di programmazione fornisce appositi meccanismi atti a regolare
l'accesso a tali risorse. La strategia tradizionalmente utilizzata dalla JVM consiste nell'associare
un apposito oggetto lock ("serratura") ad ogni classe e istanza (figura D.6). Da tener presente
che le aree condivise da tutti i T sono lo heap e l'area dei metodi dove tutte le classi sono
memorizzate. Quindi, quando la J V M carica in memoria la definizione di una classe, vi associa
immediatamente un'istanza (un oggetto) della classe java.lang.Class che, tra le altre responsabili-
tà, si occupa anche di gestire il lock per l'accesso alla classe stessa.
I lock, in ogni istante di tempo, possono essere acquisiti da uno e un solo T, pertanto quando
un T acquisisce un lock nessun altro può accedere alla relativa area bloccata. Un T che intenda
eseguire una porzione di codice ad accesso ristretto (sincronizzato) deve richiedere
preventivamente l'acquisizione del relativo lock, quindi attenderne l'acquisizione, qualora un
altro T ne sia in possesso (in questo caso lo scheduler forza il T a rilasciare la CPU in quanto
posto in stato di attesa, figura D.7). Il lock è poi sbloccato automaticamente una volta che il T
termina l'esecuzione delle istruzioni appartenenti all'area protetta.
In Java le primitive necessarie per l'acquisizione, il rilascio dei lock, la verifica della disponi-
bilità etc. sono trasparenti al programmatore il cui unico compito consiste nel definire le aree
ad accesso ristretto. Ogni lock è gestito da un apposito oggetto denominato monitor, ciascun di
questi si occupa di gestire uno e un solo oggetto e si fa carico di sorvegliare le relative aree
accesso esclusivo. Pertanto, qualora un T intenda entrare in un'area protetta (eseguirne la pri-
ma istruzione) di un oggetto, il relativo monitor si occupa di eseguire una serie di operazioni
primitive necessarie per acquisirne il lock. Queste primitive, come vedremo di seguito, sono
inserite, automaticamente, nel bytecode dal compilatorejava.
Uno stesso T può acquisire più volte il lock associato ad un medesimo oggetto (per esempio
invocando un metodo ad accesso controllato di una determinata istanza che a sua volta ne
invoca un altro sempre della stessa istanza, ancora ad accesso ristretto). Questa proprietà equi-
vale a dire che lock Java sono rientranti. Si tratta di una caratteristica fondamentale per evitare
che un T possa, autonomamente, generare una situazione paradossale di deadlock: rimanere i
attesa di acquisire un lock già acquisito.
I monitor sono in grado di gestire queste situazioni utilizzando un apposito contatore che
viene incrementato ogniqualvolta il lock viene acquisito e decrementato quando questo viene
rilasciato.
I monitor, dunque, si occupano di sorvegliare zone ad accesso esclusivo e di avviare le opera-
zioni necessarie per acquisire i lock. Tutto quello che deve fare il programmatore è definire
queste zone ad accesso esclusivo. A tal fine è possibile utilizzare la parola chiave/costrutto
synchronized. Le aree incluse nel costrutto sono definite sincronizzate.
Un T che intenda eseguire la parte di codice inclusa in un costrutto synchronized, come visto
in precedenza, deve necessariamente acquisire il relativo lock, e quindi rilasciarlo all'uscita del
costrutto. Queste operazioni richiedono, al livello di bytecode, l'esecuzione delle due primitive
due primitive: monitorenter e monitorexit. Si tratta di istruzioni inserite dal compilatore java per
delimitare, rispettivamente, l'inizio e la fine della parte sincronizzata. Ecco l'mplementazione
della classe SeatsReepository. N.B.: la quasi totalità dei commenti e delle linee bianche non
sono presenti per esigenze editoriali di spazio.
il (threadSafety) I
synchronized (this) I
success = internalSeatBooking(seatld, owner);
I
I else I
success = internalSeatBooking(seatld, owner);
I
return success;
il (attempts == null) I
throw new NlegalArgumentExceptlon("Seat not valid ("+seatld+")");
)
bookingAttempts.put(seatld, ++attempts);
String booked = seatsAvailable.get(seatld);
il (AVAIL_SIGN.equals(booked)) I
seatsAvailable.put(seatld, owner);
) else I
success = false;
I
return success;
1. impostare gli array relativi alle righe (rowlds) e alle colonne (collds) disponibili alle rispet-
tive istanze di array fornite attraverso i parametri di ingresso;
2. impostare tutte le poltrone allo stato "libere": seatsAvailable.put(currSeatld, AVAIL_SIGN);
3. di azzerare il contatore di richieste di prenotazione (bookingAttempts.put(currSeatld, 0)).
Come si può notare è stato sufficiente inserire un semplice zero, invece che il relativo
wrapping object Integer, grazie alle nuove feature dell'autoboxing del J D K 5.
this.rowlds = rowlds;
this.collds = collds;
// initialise the seat situation
lor (int i=0; krowlds.length; i++) I
lor (int j=0; j<collds.length; j++) I
String currSeatld = getSeatld(rowlds[i],j);
seatsAvailable.put(currSeatld, AVAIL_SIGN );
bookingAttempts.put(currSeatld, 0 );
I
it (threadSafety) I
synchronized (this) I
System. o u t . p r i n t l n f S y n c h ");
success = internalSeatBooklngfseatld, owner);
I
I else I
System.out.printlnfNo synch ");
success = internalSeatBookingfseatld, owner);
return success;
private boolean internalSeatBooking(String seatld, String owner) {
bookingAttempts.putfseatld, ++attempts);
if (AVAIL_SIGN.equals(booked)) (
seatsAvailable.put(seatld, owner);
) else I
success = false;
)
return success;
I
w h i l e (keepRunning) I
Random randomGen = n e w Random();
il (booked) I
bookedSeats.add(seatld);
try I
Thread.sleep(delay);
I catch (InterruptedException e) I
// ignore this exception
e.printStackTrace();
I
I
System.out.printlnf Thread ("+name+") stopping . . . " ) ;
System.out.println(toString());
System.out.println(seatsRepository.toStringO);
System.out.println("Application stopped...");
I
I
Il primo caso, come legittimo attendersi, non dà mai luogo a problemi di race conditions,
invece presente nel secondo scenario.
Di seguito è riportato un frammento della parte finale dell'output prodotto da 10 T con
l'oggetto SeatsRepository impostato ad un funzionamento MT. Per semplificare l'ispezione ma-
nuale dell'ouput, l'oggetto SeatsRepository di tipo è stato impostato con i seguenti due array:
char rowlds[] = l ' A ' , 'B', 'C\ 'D', 'E', 'F', 'G', ' H \ T , 1 ' , 'M', ' N \ O'I;
int collds[] = 1 0 1 ;
Application stopped...
Thread (T7) stopping ...
Thread (T8) s t o p p i n g . . .
Thread (T6) stopping ...
BookingThread: T 7 Booked:L1,M1, Size:2
Thread (TO) stopping ...
BookingThread: T6 Booked:H1, Size:1
BookingThread: TO Booked:A1, Size:1
BookingThread: T 8 Booked:G1, Size:1
Thread (T1) stopping ...
BookingThread: T1 Booked:E1,F1, Size:2
Thread (T3) s t o p p i n g . . .
BookingThread: T3 Booked:G1, Size:1
Thread (T4) stopping ...
BookingThread: T4 Booked:N1, Size:1
Thread (T5) stopping ...
Thread (T9) stopping ...
Thread (T2) stopping ...
BookingThread: T5 Booked:l1,C1, Size:2
BookingThread: T 9 Booked:01, Size:1
BookingThread: T 2 Booked:B1,D1, Size:2
thread (le relative specifiche fanno parte del J M M , Java M e m o r y Model). In particolare, ogni
thread è fornito con un'apposita cache la cui politica di aggiornamento dei valori è fortemente
influenzata dalla presenza di aree sincronizzate. In assenza di queste, un thread è lasciato libero
di evolvere a c c e d e n d o alla copia "locale" dei valori delle variabili memorizzate nella propria
cache. Pertanto, (sempre secondo quanto stabilito dal J M M ) , i thread sono autorizzati ad avere
valori diversi relativi alla stessa variabile. L a situazione cambia notevolmente in presenza di aree
sincronizzate. In questo caso le direttive richiedono che un thread invalidi la propria cache, e
quindi la aggiorni con i valori presenti nella memoria "principale", non appena acquisito un lock
principale, appena prima di rilasciare il lock (uscita dall'area sincronizzata). Pertanto, è facile
1. wait();
2. wait(long timeout);
3. wait(long timeout, int nanos);
Quando un T invoca il metodo wait (figura D.7), questo viene forzato a lasciare la CPU e,
contestualmente a rilasciare eventuali lock acquisiti. Chiaramente ciò è utile per consentire ad
altri T di poter essere eseguiti senza causare situazioni di deadlock. Da notare che è possibile
invocare il metodo wait solo all'interno di un'area sincronizzata, questo comporta che, corretta-
mente, 0 metodo possa essere invocato esclusivamente dal T che possiede il lock dell'oggetto.
Quando un T esegue l'istruzione wait, viene fatto transitare allo stato di waiting, e quindi
perde, temporaneamente, la possibilità di andare in esecuzione (ricevere l'assegnazione di tem-
pi di CPU). Per uscire da tale stato, deve verificarsi uno dei seguenti eventi:
La controparte del metodo wait sono i metodi notity e notifyAll. Questi due metodi si occupano
di risvegliare T transitati nello stato di attesa per mezzo dell'invocazione del metodo wait. La
differenza tra le due diverse versioni di notifica consiste nel fatto che, mentre il primo metodo
è in grado di risvegliare un solo T selezionato a discrezione dello scheduler qualora diversi siano
sia in attesa, il secondo risveglia tutti i T eventualmente in attesa sull'oggetto destinatario della
notifica. Una volta che un T è risvegliato, transita nello stato Ready-to-run (figura D.7) e quindi,
nuovamente lo scheduler decide se interrompere o meno il T notificante e a quale assegnare la
CPU (passaggio allo stato di running). Qualora nessun T sia in stato di attesa presso l'oggetto
notificato, non succede nulla e quindi l'evento di notifica viene semplicemente ignorato.
Come lecito attendersi, i metodi notify e notifyAll, presentano diverse analogie con il metodo
wait. In particolare:
• sono definiti nella classe java.lang.Object e quindi sono implicitamente presenti in tutte le
classi Java;
• possono essere invocati solo all'interno di un'area sincronizzata, ciò implica che il T
invocante deve possedere il lock dell'oggetto a cui viene inviata la notifica.
new Thread I)
New
stopi).
termine del metodo run ()
r D e a d J
(?)
public SimpleQueue() I
I
try I
aLock.wait();
) catch (InterruptedException ¡Exp) (
// ignore this exception
endOfProcessing = true;
I
I
// thread awaked...
// Is there an element available in the queue or
// Is it the end of p r o c e s s i n g ?
il (Iqueue.isEmptyO) {
ret = queue.remove(O);
return ret;
I
synchronized (aLock) (
// send an alert t o threads that m i g h t be blocked on this object
aLock.notifyAII();
I // — end of class —
// METHODS SECTION
public SimpleProducer(SimpleQueue queue) (
this.queue = queue;
I
iterationsCounter = 0;
try I
while ( lislnterruptedO) I
iterationsCounter++;
String element = "PRODUCER COUNTER ;"+iterationsCounter;
System. oi/f.printlnf'PRODUCER ADDED ELEMENT: >"+element+"<");
queue.addElement(element);
Thread. s/eep(pauseTlme);
I
I catch (InterruptedException e) I
// ignore this exception
// n o t h i n g to do
I
try I
La classe mostrata nel precedente listato è analoga a quella implementata per il produttore.
La sola differenza risiede nel corpo del ciclo. In questo caso, infatti, invece di generare un
nuovo elemento, questo viene prelevato dalla coda e quindi "consumato" per la produzione di
un importante servizio: la stampa a video della stringa letta.
Per terminare, si consideri il seguente listato che riporta la classe che avvia il tutto:
consumer.start();
producer.start();
tryl
Thread.sleep(5000);
) catch (InterruptedException e) I
// ignore this excetion
e.printStackTrace();
I
producer.stopRunning();
aQueue.notifyEndOtProcessing();
Il produttore funziona più velocemente del consumatore, come si evince sia da diverse linee di
output con produzioni consecutive, sia dal fatto che, una volta terminato il produttore, il consu-
matore ha ancora diverse stringhe da consumare prima di potersi fermare a sua volta. Inoltre,
una volta terminato il T che esegue il main (stringa END OF EXAMPLE — ) , termina anche il
produttore, mentre il consumatore è ancora in esecuzione. Quando anche questo termina, tutti
i T utente in esecuzione sulla JVM terminano e quindi anche la JVM termina di funzionare.
Un interessante spunto è relativo all'implementazione della coda per mezzo di un ArrayList.
Come si può notare dal codice, i nuovi elementi sono, legittimamente inseriti nell'ultima posi-
zione disponibile dell'array, mentre il prelievo comporta l'estrazione dell'elemento in prima
posizione (indice = 0). Dall'analisi del codice della classe ArrayList (cfr. listato riportato poco
sotto) si può notare come utilizzare il metodo remove per l'estrazione di un elemento comporti
la traslazione in avanti di tutti gli elementi successivi necessari per ricompattare l'array. Questa
strategia in applicazioni MT gravate da un elevato throughput tende a degradare notevolmente
le performance soprattutto qualora la coda tenda ad essere sempre abbastanza piena. Per que-
sto motivo è conveniente utilizzare implementazioni di code disegnate appositamente per am-
bienti altamente MT come liste concatenate. In effetti questa strategia è utilizzata con le nuove
classi "linked" introdotte con il J D K 5 (di cui parleremo nei paragrafi successivi).
/ "
modCount++;
E oldValue = elementData[index];
return oldValue;
I
Atomicità
Si dicono atomiche le istruzioni la cui esecuzione è indivisibile: una volta avviata la loro esecu-
zione, questa non può essere interrotta dallo scheduler fino al suo completo compimento. In
Java, la lettura e l'aggiornamento di quasi tutti i tipi base, eccetto long e doublé, avvengono in
maniera atomica. Al fine di rendere l'accesso e l'aggiornamento anche di questi campi atomico,
è necessario dichiararli-volatile (per esempio: volatile doublé amount = 0;). Come lecito attendersi,
anche l'accesso e la modifica a variabili contenenti il riferimento in memoria di oggetti avviene
in maniera atomica. Questa proprietà garantisce che, qualora un elemento atomico sia coinvol-
to in un'espressione, il T che lo sta eseguendo accede a un valore consistente. In particolare,
tale valore può essere uguale o al valore iniziale dell'attributo o a quello finale, ottenuto ese-
guendo una determinata espressione su di esso, mentre non può accadere che un T acceda a un
valore "spurio", parzialmente modificato. La mancanza di atomicità "nativa" dei tipi long e
doublé è dovuta al fatto che la relativa rappresentazione prevede 64 bit, mentre molte CPU
lavorano (o lavoravano) a 32 bit. Ciò fa sì che non si possa dare per scontato il fatto che le
implementazioni delle varie J V M eseguano accessi e scritture in maniera atomica: CPU operan-
ti a 32 bit, in presenza di tipi a 64bit, richiedono due istruzioni anziché una singola. Questo
genera la possibilità che l'esecuzione di un determinato T possa essere interrotta proprio tra
due istruzioni consecutive relative al valore della stessa variabile.
Chiaramente, il fatto che un'operazione sia atomica non implica automaticamente che que-
sta non necessiti di sincronizzazione. Per esempio, una variabile di tipo intero è atomica, però
ciò non significa che sia accettabile perdere qualche aggiornamento per via di un accesso simul-
taneo al suo valore da parte di due T (cfr. race conditions)
Visibilità
Con il termine di visibilità si indicano le condizioni che regolano il manifestarsi, ad altri T, delle
azioni, tipicamente aggiornamento dei dati, eseguite da un determinato T. Come è lecito atten-
dersi, queste danno luogo ad una serie di importanti requisiti necessari per l'implementazione
della gestione della memoria. Sebbene queste regole siano utilizzate dai rari team che si occu-
pano dello sviluppo di JVM, la loro comprensione permette di implementare sofisticate appli-
cazioni MT.
Da quanto riportato in precedenza è evidente che un T che non utilizza aree sincronizzate
per un periodo di tempo prolungato, potrebbe trovarsi a utilizzare valori delle variabili non
aggiornati (stale). Pertanto, è necessario porre attenzione qualora si intenda implementare un
T che esegua dei loop in attesa di valori scritti da altri T senza far sì che la variabili, oggetto del
test, sia dichiarata volatile o l'accesso sia sincronizzato.
Ordering
Per quanto concerne l'ordine di esecuzione delle istruzioni, in questo contesto, è necessario
distinguere due casi:
1. esecuzione delle istruzioni di un metodo all'interno di un T; in questo caso non c'è nulla
di nuovo: le istruzioni sono eseguite come se si trattasse di una normale esecuzione
sequenziale;
2. esecuzione delle istruzioni di un metodo i cui effetti sono, in qualche modo, oggetto di
attenzione da parte di altri T. In questo caso, se l'accesso alle aree condivise non è pro-
tetto da accessi a mutua esclusione, può verificarsi ogni possibile interlacciamento delle
istruzioni. Chiaramente la situazione cambia in presenza di zone ad accesso ristretto
(syncrhonized, attributi volatile, lock). In questo caso, chiaramente, l'ordine di esecuzione
delle istruzioni è preservato.
<b
C><|
K
•n»
.O
N
£
<>
i
«il
Sa
O
K
«
ìT
Dall'analisi dell'organizzazione del package originario, è altresì evidente la connaturata in-
tenzione di includere questo package tra le API standard del linguaggio Java (consistenza quasi
totale), come dichiarato dallo stesso autore. Tale risultato è stato raggiunto conferendo partico-
lare enfasi a fattori quali alte prestazioni, facilità di utilizzo, flessibilità, portabilità, correttezza
e conservabilità.
La versione attuale del package java.util.concurrent presenta inoltre un elevato grado di matu-
rità conseguito attraverso oltre quattro anni di sperimentazioni. Queste hanno permesso di
sperimentare approfonditamente il package e di individuare aree deboli, prontamente perfe-
zionate tramite opportuni processi di refactoring.
Allo stato attuale, il package contiene ben 15 interfacce, 47 classi, un nuovo tipo enumeratore
e cinque diversi tipi di eccezione. Questa ricchezza, purtroppo, pregiudica la possibilità di una
esposizione, completa e dettagliata dell'intero package: poche pagine non sono sufficienti.
myLock.tryLock(50L, TimeUnit.MILLISECONDS)
myLock.tryLock(50L, T i m e U n i t . S E C O N D S )
java.util.concurrent panoramica
Il nuovo package della concorrenza è costituito di una serie di classi che forniscono le fonda-
menta per la soluzione di un insieme di problemi appartenenti al particolare dominio delle
applicazioni MT. Soluzioni concrete, ossia specifiche applicazioni MT, possono essere codifica-
te implementando ben definite interfacce e/o estendendo specifiche classi e quindi combinan-
do queste nuove classi con quelle esistenti. La complessità del nucleo della libreria è quindi
accessibile attraverso una serie di semplici interfacce, corredate da opportune implementazioni.
I principali meccanismi introdotti con il package della concorrenza sono:
Locks e Conditions
Come visto in precedenza, tradizionalmente, il linguaggio Java gestiva i lock in maniera del
tutto trasparente, che il programmatore non poteva vedere. Con la versione JDK5 si è introdot-
ta la possibilità di una gestione esplicita.
Gli strumenti lock (serrature) forniscono avanzati meccanismi per controllare l'accesso a
risorse condivise (logiche e/o fisiche) da parte di diversi T. Si tratta di una sofisticata alternativa
al costrutto Java synchronized, la cui gestione basata sul singolo monitor non sempre risulta
essere ottimale in ambienti MT. Il meccanismo dei lock, oltre a presentare migliori prestazioni,
fornisce una serie di utilissimi servizi aggiuntivi, quali:
Le classi del package della concorrenza, come lecito attendersi, utilizzano sistematicamente
il meccanismo dei lock (in particolare la classe ReentrantLock) al posto del costrutto synchronized.
L'unico effetto collaterale dovuto all'utilizzo di questo meccanismo, consiste nel dover codifi-
care esplicitamente l'acquisizione (myLock.lock()) e il rilascio (myLock.unlock()) dei lock-. ciò ov-
viamente, richiede la presenza del costrutto try.. .finally al fine di evitare l'insorgere di situazioni
di dead-lock.
class MyResource I
private final ReentrantLock myLock = new ReentrantLockQ;
ua
eo
Ci
=3
2
<b
ts
£
«
•Si
tC
diritto di accedere alla risorsa condivisa. Tuttavia, esistono altre versioni più sofisticate,
ReentrantReadWriteLock, per esempio, in cui 0 lock consiste di due diversi: uno per gli accessi in
scrittura ed uno per quelli in lettura. Ciò permette di avere diversi T in grado di accedere ad
una risorsa condivisa in lettura, mentre l'accesso in scrittura resta esclusivo.
Oggetti di tipo lock permettono poi di generare istanze di tipo Conditions utilissime per sem-
plificare la sincronizzazione tra diversi T, sostituendosi ai classici meccanismi di wait(), notify() e
notifyAHQ. In particolare, le condizioni forniscono un meccanismo per sospendere l'esecuzione
di un T (myCondition.awaitO) finché giunga la notifica (myCondition.signalQ), da parte di un altro,
del verificarsi di una determinata condizione. Le condizioni sono intrinsecamente associate ad
istanze di tipo lock. In particolare è possibile ottenere il riferimento ad una nuova istanza di
tipo Condition invocando il metodo newCondition() di un'istanza di tipo Lock (Condition myCondition
:= myLock.newCondition()). La transizione di un T allo stato di attesa (myCondition.awaitO), analoga-
mente a quanto avviene per un'invocazione del metodo wait() (java.lang.Object), ne genera il rila-
scio immediato del lock.
La maggior parte delle classi lock, così come gli altri meccanismi del package della concor-
renza, prevedono una versione del costruttore dotato del parametro di equità (boolean fair). Se
questo viene impostato al valore true, ogniqualvolta un lock diviene disponibile, questo selezio-
na il successivo T che lo acquisirà, tra quelli in attesa, utilizzando una rigorosa politica FIFO:
viene selezionato il T che per primo ha effettuato la richiesta di acquisizione e che quindi più a
lungo si trova in attesa. Quando invece il flag non è impostato, o è impostato a false, la selezione
non rispetta alcun particolare ordine. E evidente che una politica di equità genera un impatto
considerevole sulle prestazioni.
Un interessante metodo non bloccante è tryLock: esso tenta di ottenere istantaneamente uno
specifico lock e quindi ritorna l'esito dell'operazione (true = lock acquisito, false = altrimenti).
Questo metodo è, quindi, in grado di aggirare un'eventuale politica di equità. Qualora questo
comportamento non sia desiderato, è necessario utilizzare la variante di tryLock dotata dei para-
metri di timeout, magari specificando un valore di timeout pari a un nanosecondo.
Si consideri la figura D. 11 (per delucidazioni circa la notazione dei diagrammi delle classi si
consideri [UMLING]). La classe astratta AbstractQueuedSynchronizer è un po' il cuore del mecca-
nismo dei lock e poiché quest'ultimo è utilizzato in tutto il package, ne segue che questa classe è
uno dei blocchi fondamentali dell'intero package per la gestione dei meccanismi di concorrenza.
Il diagramma mostra come i lock siano implementati attraverso una coda di nodi collegati, in
cui ciascuno memorizza informazioni circa il T associato (T che ha richiesto l'acquisizione del
lock). La classe LockSupport è stata introdotta per fornire primitive a basso livello per il supporto
al blocco necessario a tutti i programmatori che intendono definire meccanismi di lock
personalizzati. Anche in questo package si utilizzano copiosamente le nuove istruzioni a livello
macchina, il cui collegamento è rappresentato dalla classe sun.misc.Unsafe.
OQ
C>>
c
o
-e
Q.
CO
U
I
04
Q
2
s
Queue e BlockingQueue
L'interfaccia Queue, correttamente inserita nel package java.util, è stata disegnata per definire il
comportamento di oggetti code (figura D.13). Le code sono in genere gestite secondo il princi-
pio del primo arrivato/primo servito (FIFO, First In First Out). L'interfaccia Queue, come lecito
attendersi, è stata disegnata per un utilizzo generale e quindi non contiene metodi specifici per
il supporto al MT. Questi, necessari per la soluzione di scenari del tipo produttore/consumato-
re, sono invece presenti nell'interfaccia BlockingQueue e presentano una nuova sintassi offer, poli,
etc. I metodi disegnati per problematiche MT sono stati implementati con l'idea di porre i
thread client in stato di attesa qualora: un consumatore (consumer) tenti di prelevare un ogget-
to da una coda vuota o un prodotture (producer) tenti di inserire un oggetto in una coda, di
dimensione fisse, piena. Chiaramente, nel caso in cui la dimensione della coda non sia fissa,
l'inserimento di nuovi elementi non è mai un'operazione bloccante. Qualora diversi T siano in
attesa del verificarsi di una medesima condizione (per esempio diversi consumer siano in attesa
che un elemento sia inserito nella coda), la politica utilizzata per la selezione del T da sbloccare,
è decisa dal programmatore agendo su un opportuno parametro di equità (fairness). Analoga-
mente a quanto visto in precedenza, se il flag fair è impostato a true, allora l'oggetto coda si fa
carico di utilizzare risorse specializzate per la gestione F I F O anche dei T in stato di attesa.
L'esecuzione del servizio di equità, presente in molteplici classi del package, avviene a spese di
una diminuzione delle performance, che in alcuni scenari (molti task asincroni con elevata
frequenza di blocco), può divenire rilevante. Il lato positivo è che la necessità del suo utilizzo
non è poi così frequente.
Oggetti di tipo coda, inoltre, non accettano la richiesta di inserimento (offerta) di valori nulli,
che quindi danno luogo ad una NulIPointerException. Questo perché si tratta di un valore (sentinel-
la) utilizzato per verificare 0 successo o fallimento dell'esecuzione del metodo poli (preleva e
ritorna l'elemento in testa alla lista). In particolare, il valore nuli è restituito nella situazione in cui
il tempo massimo di attesa specificato sia trascorso senza che un elemento sia presente nella coda.
Della struttura dati coda sono disponibili diverse versioni, ognuna caratterizzata da un com-
portamento peculiare. La maggior parte di queste utilizza un oggetto di tipo ReentrantLock, dal
quale sono generate oggetti condizioni atti a gestire segnalazioni di coda notEmpty e coda notFull
(ArrayBlockingQueue), available (DelayQueue), etc.
Le diverse implementazioni della struttura dati coda sono:
• ArrayBlockingQueue: coda gestita secondo una politica di tipo FIFO, la cui implementazione
è basata su un array lineare di dimensione prestabilita e non modificabile, specificata nel
costruttore dell'oggetto. Pertanto, si tratta di un'implementazione del concetto di buffer
a dimensioni fisse (bounded buffer).
• DelayQueue: coda di dimensione variabile caratterizzata dal fatto che gli elementi presenti
possono essere prelevati solo dopo che questi si siano trattenuti nella coda per uno
specifico intervallo di tempo. Gli elementi della coda devono implementare l'interfaccia
Delayed, la quale definisce un solo metodo (getDelay) che restituisce il tempo mancante al
raggiungimento del tempo di ritardo sancito dall'oggetto coda. Valore utilizzato dal
metodo poli, il quale ritorna (rimuovendolo) l'oggetto in testa alla coda, oppure nuli nel-
l'evenienza che la coda sia vuota o che nessun elemento sia rimasto nella coda per l'inter-
vallo di tempo prestabilito.
• LinkedBlockingQueue: si tratta di una coda (opzionalmente di dimensione fissa), gestita in
base alla politica FIFO, in cui elementi memorizzati sono strutturati in una serie di nodi
collegati. Questa tecnica, tipicamente, permette di disegnare code con migliori presta-
zioni, soprattutto in termini di througbput l'inserimento e la rimozione degli elementi,
tipicamente, si risolve nell'aggiornamento di un paio di puntatori. Ciò quindi è più effi-
ciente di altre implementazioni le quali frequentemente prevedono l'aggiornamento
dell'intera coda, soprattutto a seguito della rimozione dell'elemento in testa. Per esem-
pio la rimozione di un elemento (remove(int index)) da un oggetto di tipo ArrayList, genera
lo spostamento di tutti gli elementi seguenti tramite l'invocazione del metodo nativo
arraycopy presente nella classe System.
• ConcurrentLinkedQueue: si tratta di un'altra implementazione di coda FIFO, basata su nodi
collegati. Questa implementazione è ottimizzata per applicazioni MT caratterizzate da
un elevato numero di T che condividono uno stesso oggetto di tipo coda. Questa
ottimizzazione è ottenuta grazie all'implementazione di un efficiente algoritmo (cfr.
[SFPNBB]), in grado di eliminare stati di attesa.
• PriorityBlockingQueue: questa è l'implementazione specializzata per ambienti MT dell'equi-
valente struttura di coda a priorità, non sincronizzata, presente nel package delle utilità
Java (java.util.PriorityQueue<E>). In particolare, gli elementi inseriti sono ordinati in base al
criterio specificato nel costruttore per mezzo di un'istanza di tipo java.util.Comparator<T>.
Quest'ultima interfaccia richiede di implementare i due metodi: compare(T objectl, T object2)
e equals(Object obj). Qualora l'oggetto comparatore non sia specificato, si utilizza l'ordine
naturale degli oggetti, rappresentato dall'interfaccia java.lang.Comparable<T>. Anche que-
sta coda prevede una dimensione variabile, sebbene tentativi di aggiungere elementi
nella coda possano risultare in un'eccezione OutOfMemoryError, atta a segnalare l'esauri-
mento delle risorse.
• SynchronousQueue: si tratta dell'implementazione di una coda sincrona intrinsecamente
MT. La sincronicità si ottiene attraverso l'implementazione di un meccanismo bloccante
(ReentrantLock qlock) e di due code: quella dei processi produttori (private final WaitQueue
waitingProducers) e quella dei processa consumatori (private final WaitQueue waitingConsumers).
Qualora un T esegua un'operazione di fornitura di un elemento (offer) a un'istanza di
coda sincrona, questa acquisisce il lock e quindi verifica l'esistenza di un T consumatore.
Se presente, l'elemento è fornito al primo consumatore disponibile e l'esecuzione del
metodo offer termina. Altrimenti, l'elemento oggetto dello scambio viene memorizzato
nella coda dei produttori, il lock è rilasciato ed i T è posto in attesa che un altro T
richieda la consumazione di un elemento. Oggetti SynchronousQueue non sono logica-
mente code: un elemento è "presente nella coda" solo quando un altro T è pronto a
prelevarlo e quindi il concetto di capacità perde di significato. Anche se l'implementazione
utilizza poi delle code, se non esiste un consumatore, il T produttore rimane bloccato
all'atto dell'inserimento di un elemento, e quindi è come se di fatto non depositasse
l'elemento nella coda. Ciò, tra l'altro, determina l'impossibilità di generare un oggetto di
tipo Iterator. Coloro che ricordano la teoria del MT, potranno riconoscere in questa strut-
tura il concetto dei rendezvous channels (canali di incontro), utilizzato in diversi linguag-
gi di programmazione, come specifiche versioni del linguaggio ADA. Un utilizzo classi-
co di oggetti di questo tipo è l'implementazione di workflow caratterizzati da una catena
di T specializzati nell'eseguire specifici compiti, dove, quindi, uno stesso oggetto subi-
sce diversi stati di computazione da parte di diversi T. In tale scenario, le code sincrone
si prestano a risolvere, elegantemente, il problema del passaggio delle consegne da un T
di uno stadio e quello dello stadio successivo.
• • mtpriaco |
T"!
Java: Jang: j t e r a b l e J
interi Kt^ I *•
j a v a u t i l A b s t r a c t C o l l e c t i o nI———
| _J
java:utitCollection k •
interi de»- |
java: u t i t Queue ^
j a v a joiSfcrializable
-1 1
• incettaci? - —J -enumeration..-
BlockinqQyeue TimeUnit
+add(o) ¡boolean
• drainTo(c) :int •
• drainTo(c, maxElements) :int
• offer(o) : boolean
*offer(o, timeout, tvneUntt) : boolean
+ po(((tin>eoul,timeUmt) :E CNJ t-
•put(o)
»remalntngCapadtyO :int /]
•takeO :E x n
" i
I r-- I r i
r
J E _l E J E : Delayed |
ArrayBlockingiÌjeue SynchronousQueue
i j LinkedBlockingQueue
i
|
_i I
PriorityBlockingQueue
I
DelayQueue
_T j
~o-
java:jo:Serializable
Figura D.13 - Strutture dati di tipo coda, del package java.util.concurrent. La struttura delle singole
code non è fornita per questioni di resa grafica.
Collezioni MT
Le collezioni standard Java sono thread-safe: in Java 1 lo sono nativamente (Hashtable, Vector,
etc.), quelle introdotte in Java 2 lo sono su richiesta (Collections.synchronizedMap(),
Collections.synchronizedList(), etc.). Ma queste non sempre risultano particolarmente efficienti e
scalabili per applicazioni MT. In effetti, il relativo utilizzo in questi ambienti finisce frequente-
mente per generare odiosi colli di bottiglia, dovuti principalmente alla strategia del singolo
monitor utilizzato per gestire l'accesso concorrente a tali oggetti. Il nuovo package per la con-
correnza risolve questa limitazione fornendo nuove classi che implementano strutture dati (fi-
gura D . 1 4 ) di tipo lista, set e hashtable, rispettivamente: CopyOnWriteArrayList, CopyOnWriteArraySet
e ConcurrentHashMap, disegnate specificatamente per ambienti MT. Quindi presentano compor-
tamenti ottimizzati per scenari in cui diversi T, "contemporaneamente", necessitano di leggere
e/o scrivere su istanze di tali classi.
.O
As
•(0c
m. 1— ! - a . W
1 i UJ LU
co
— • j
"w
>
co
o
rk
8? £
-Q
S?
xK ' e h
A
<<i
java
5 -g.(J 5 © _
<
.0
m g-a 1 11 o n
l i i l l i l i i f i
s&
a o
N
«i
j's
{ •• • • ì^ « O cte-te--x E
etiÌ¥x9Ìfc 8«
i J i m i H f f l p
j > > tu
I * C,
1J
3
OC
ti
P j f j ¿i s
.o
!|||t|l!ii||
H
SI l i - i l
a
+ +i l+l +a++
l i +t i+l +i ++
: + o
»
s
>-J
Q
2
a
.00
fi
MT richiedono l'implementazione di pool di T e soluzioni a problematiche quali lo scheduling
dei task (T) e il controllo della relativa evoluzione. Il ricorso a pool di T è un'ottima strategia
per aumentare le performance di applicazioni MT, soprattutto quando il sistema richiede la
gestione di un gran numero di task asincroni e/o quando i task devono essere eseguiti il più
rapidamente possibile (disponendo di T pronti ad essere utilizzati, si elimina la latenza richie-
sta dalla creazione di un nuovo T). Il pool di T, inoltre, tende a semplificare il problema dell'as-
segnazione delle risorse ai task: una strategia spesso utilizzata consiste nel pre-assegnare ai T,
all'atto della loro creazione, le risorse necessarie per l'esecuzione dei task. Le interfacce e classi
che implementano i meccanismi base di quest'area sono:
z,
Q. UJX fi
I
§dJ s ^ u
X
c
il
o' ol o' o '
4
-1-1 I «
1 a
È
.0
&
C 3«_>
3 E
P 'E?
c3 ¡=- rtl
=>:
ipr
s; ^ 13 2
sf r* <3
-2J §
EI S
*-' 00 3 -C » (0
O
a U
SA t - C o3 Ì» K
£ X s • •-9 f!u oC- a;9 «J5 O
•E S - M li
«r JÈ jè 2 3?
. , 3 - 0
¡3 I 2 S «J 4» .- D W —
5H
o c C o O V ' ij f8 ¡82 2¡3 5 -i S
Ì 5^ -O ~ "H IL ~ «u hi?£
rtJ 2 =: o o Z ^
•
_ 5OvJ c£ 4J)
O > £-s - x e5 E
(0 ffl
-Sf"0« ES
C O
w > > £J 2 = z = S S
l i 41 c 5 v
5 1 1JÈ 23 fcI Io O iti .*. T o 03
»".11 J
•ti JL
(Q O O O O .c23 0213 A6 J1 OTJS c
1 1 C>>>»/!
C C vi H i/l .c 3 : — c fl •
li" Ì 6
ti ti * : 5 I 5 >•
«^ 8<u S«o-p «ao
Jllsll- £
illjl 3»<5 Sc-pi i .5
J iZ
Uj
i 'i
ii U £t £« 5«
fl 3 io
m
ii
_ E
Q. D ci
S ¡rf
S vi I
a
.00
Le classi che implementano l'interfaccia ExecutorService sono: AbstractExecutorService e
ThreadPoolExecutor (figura D.17). La prima classe, come ben evidenziato dal nome, è una classe
astratta. Pertanto fornisce l'implementazione di una serie base di metodi (submit, invokeAny e
invokeAII), lasciando ad ulteriori classi specializzanti, eventualmente anche definite dall'utente,
l'onere di definire l'implementazione dei restanti metodi. L'implementazione della classe Future
utilizzata è quella fornita dal package, ossia: FutureTask. La classe ThreadPoolExecutor, invece, è
una classe concreta e quindi definisce l'implementazione di tutti i metodi dichiarati dall'interfaccia
ExecutorService. Pertanto, fornisce tutti i meccanismi necessari per utilizzare il TP.
L'ultima interfaccia della gerarchia degli esecutori è ScheduledExecutorService che, come lascia
presagire il nome, si occupa dell'esecuzione ritardata e/o periodica di insiemi di T. In partico-
lare, le varie versioni del metodo schedule permettono di creare degli oggetti task abilitati dopo
lo scadere del tempo specificato (long delay). Questi metodi ritornano un oggetto di tipo
ScheduledTask che può essere utilizzato per verificare l'esecuzione del task ed eventualmente
cancellarlo.
L'esecuzione dei lavori periodici invece avviene attraverso i metodi scheduleAtFixedRate e
scheduleWithFixedDelay. In particolare, il primo permette di eseguire periodicamente delle azioni
che, sono abilitate ed eseguite sia allo scadere del relativo intervallo di tempo (long initialDelay),
sia ad ogni successivo scadere del corrispondente periodo di ritardo (long period). Il metodo
scheduleWithFixedDelay, analogamente al precedente, permette di creare ed eseguire azioni che
abilitate al trascorrere dell'iniziale tempo di ritardo (long initialDelay), sono successivamente ese-
guite allo scadere del periodo di ritardo (long period) considerato a partire dal termine della
O
ExecutorService • invokeAll(Collection tasks) : List
AbstractExecutorService
+ i n v o k e A l l ( C o l l e c t i o n t a s k s , l o n g t i m e o u t , T i m e U n i t u n i t ) : List
+ i n v o k e A n y (C o l l e c t i o n t a s k s ) : T
• i n v o k e A n y ( C o U e c t i o n t a s k s , l o n g t i m e o u t , T i m e U n i t unit) : T
• submit(Callable task) : Future
+submit(Runnable task ) : Future
+submit(Runnable task , T result) : Future
ThreadPoolExecutor
O
ScheduledExecutorService +ScheduledThreadPoolEKecutor (int corePoolSize)
•ScheduledThreadPoolExecutor (int corePoolSize,RejectedExecutionHandler handler)
ScheduledThreadPoolExeculor
• numero di T del pool inferiore al valore del CorePoolSize: in questo caso, la proposizione
di un nuovo lavoro (invocazione del metodo execute) fa sì che il pool crei un nuovo T,
indipendentemente dall'eventuale presenza nel pool di altri T.
• numero di T del pool compreso tra il valore del corePoolSize e quello del maximumPoolSize:
in questo caso se un nuovo lavoro viene sottoposto, allora viene istanziato un nuovo T
solo se tutti i T del pool sono impegnati a eseguire qualche lavoro.
Per creare un pool di dimensione fissa, è sufficiente impostare al medesimo valore i due
parametri, mentre per aver un pool a crescita (virtualmente) infinita è possibile impostare il
secondo parametro al valore lnteger.MAX_VALUE.
L'utilizzo classico prevede che corePoolSize e maximumPoolSize siano impostati, una volta per
tutte, all'atto della creazione dell'oggetto; in ogni modo, nulla vieta di variarli dinamicamente
attraverso i relativi metodi di "set".
Da quanto descritto, è evidente che la classe ThreadPoolExecutor genera nuovi T applicando la
tecnica di lazy initialisation: i T del pool sono generati on-demand. Tuttavia, questa politica
può essere modificata tramite esplicita invocazione dei metodi: prestartCoreThreadQ o
prestartAIIC0reThreads() (attenzione al plurale!).
I nuovi T del pool sono creati ricorrendo all'implementazione di default dell'interfaccia
ThreadFactory (java.util.concurrent.ThreadFactory). In particolare viene invocato il metodo
Executors.defaultThreadFactoryQ il quale ritorna appunto l'oggetto factory di default che si occupa
di generare nuovi T. Questi sono assegnati allo stesso gruppo (in termini della classe
java.lang.ThreadGroup), impostati alla medesima priorità (N0RM_PRI0RITY) e di tipologia "utente"
(non-daemon). Tali impostazioni possono essere variate, specificando una propria
implementazione di tipo ThreadFactory nel metodo costruttore o attraverso il corrispondente
metodo di set.
Il parametro keepAliveTime, in combinazione con il Timellnit, è utilizzato per regolare il tempo
di permanenza in vita dei T che eccedono il valore impostato nell'attributo COrePoolSize. Questo
permette di dar luogo ad un comportamento più scalabile e a un migliore utilizzo delle risorse
dell'intero sistema. Anche questo valore è impostato all'atto della costruzione dell'oggetto
ThreadPoolExecutor e, come da prassi, può essere variato dinamicamente attraverso apposito
metodo di set.
Un altro parametro molto interessante, è workQueue (istanza di tipo java.util.concurrent.
BlockingQueue<E>) utilizzata, in determinati scenari, per trasferire e porre in stato di attesa i
lavori sottoposti. Come specificato in precedenza, BlockingQueue<E> è un'interfaccia che preve-
de implementazioni quali SynchronousQueue, LinkedBlockingQueue, ArrayBlockingQueue, etc. Questo
oggetto coda può essere analizzato a tempo di esecuzione (per finalità di monitoring, debugging,
etc.) invocando il corrispondente metodo getQueue(). L'utilizzo dell'oggetto coda presenta una
forte dipendenza dai i parametri di dimensione del pool visti precedentemente. In particolare,
se, all'atto della sottomissione di un nuovo lavoro, tutti i T del pool sono assegnati ed il loro
numero è uguale o superiore al numero massimo di T, il nuovo lavoro viene inserito nella coda.
Nel caso estremo in cui tutti i T del pool siano assegnati, si sia raggiunto il numero massimo di
T generabili, e una nuova richiesta non possa neanche essere inserita nella coda, il relativo
lavoro viene rifiutato (rejected). Questa eventualità può essere eliminata utilizzando code "in-
finite", il che, ovviamente, genera la conseguenza di non avere mai un pool di dimensioni supe-
riori al parametro corePoolSize.
L'evenienza di un reject non è limitata al solo caso di utilizzo di code a dimensione fissa, ma
può verificarsi anche qualora, dopo l'invocazione del metodo di shutdown del pool, si tenti di
inviare un nuovo lavoro. In ogni modo, qualora si verifichi questo evento, il pool invoca il meto-
do RejectedExecutionHandler.rejectedExecution (interfaccia java.util.concurrent.RejectedExecutionHandler).
L'implementazione di default è quella definita nella classe annidata: ThreadPoolExecutor. AbortPolicy
che si occupa di lanciare l'eccezione runtime RejectedExecutionException.
Alcune alternative sono fornite dalle classi:
ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory ThreadFactory, RejectedExecutionHandler handler)
il cui significato dei parametri è del tutto equivalente ai corrispondenti della classe
ThreadPoolExecutor analizzata poco fa.
A questo punto, i programmatori esperti Java potrebbero chiedersi quale sia il vantaggio di
utilizzare questa classe, invece che per esempio utilizzare java.utiI.Timer e/o un'opportuna
specializzazione della classe astratta java.util.TimerTask introdotte con la versione 1.3 JDK. In
effetti, la domanda è legittima dal momento che queste classi sono state introdotte proprio per
fornire meccanismi convenienti per l'esecuzione posticipata e/o ricorrente di specifici T.
La prima risposta è relativa alla mancanza della gestione di thread pooling. Una seconda
risposta è relativa alla difficoltà di gestione delle eccezioni (per quanto il metodo run() non le
preveda, è possibile implementare un proprio meccanismo di gestione delle eccezioni non ge-
stite) e di ritornare esplicitamente dei risultati. Un'altra potrebbe riferirsi al limitato grado di
flessibilità della soluzione dovuto alla mancanza del supporto Java alla ereditarietà multipla.
Infatti, questo metodo prevede, che un T, per poter essere eseguito in un tempo futuro e/o
ricorrente, erediti dalla classe TimerTask. Pertanto, ciò di fatto elimina la possibilità di ogni altra
ereditarietà, anche se è pur vero che l'ereditarietà può essere simulata con la composizione.
Inoltre, altri servizi forniti non sono abbastanza sofisticati. Per esempio, il metodo di cancella-
zione (public boolean cancelO) può impedire che l'oggetto di tipo TimerTask venga eseguito e/o
rieseguito nuovamente; però, se l'oggetto si trova in stato di esecuzione, l'invocazione di tale
metodo non ne causa l'immediato arresto.
Per i motivi addotti, possiamo tranquillamente affermare che la classe ScheduledThreadPoolExecutor
di fatto ha reso obsolete le altre due, Timer e TimerTask, sebbene in semplici casi caratterizzati da
un solo T temporizzato possano ancora risultare sufficienti.
Gerarchia "futura"
Un concetto più volte menzionato è quello dalle classi di tipo Future (figura D.17) che permet-
tono di monitorare l'evoluzione di elaborazioni asincrone ed eventualmente, a fine elaborazio-
ne, di accedere al risultato prodotto. Quest'ultima operazione è possibile tramite l'invocazione
del metodo get(), il quale attende la terminazione dell'elaborazione e quindi ne ritorna l'oggetto
che incapsula il risultato. Come di consueto, esiste una versione del metodo (overloading) che
permette di specificare il tempo massimo di attesa.
T : Delayed
java: lang : t o m p a r a b l Future
• irlU'üüf^-
ScheduledFuture
implements ScheduledFuture<V>
CompletionService
La trattazione del nuovo package della concorrenza termina con la presentazione dell'interfaccia
CompletionService e della relativa implementazione fornita dalla classe ExecutorCompletionService.
Questo servizio fornisce uno strato di in direzione tra la creazione/esecuzione di nuovi task, e
l'accesso e utilizzo dei relativi risultati. Pertanto, mentre componenti produttori richiedono
l'esecuzione di nuovi task attraverso l'invocazione di una delle due versioni del metodo submit,
i componenti consumatori possono accedere ai relativi risultati attraverso l'invocazione del
metodo take (o poli).
Il caso tipico di utilizzo del servizio di "completamento" è la gestione di operazioni asincrone
di I/O. In questo scenario, è frequente che una parte del sistema effettui la richiesta dell'elabo-
razione di task I/O ed un'altra ne acquisisca, in modo asincrono ed in un ordine non necessaria-
mente equivalente a quello della richiesta, i risultati. Ciò avviene attraverso l'invocazione del
metodo take che reperisce e rimuove il successivo oggetto di tipo Future ottenuto dalla termina-
zione del relativo task. Il metodo poli ne rappresenta una versione caratterizzata da non attende-
re la terminazione di un task (non è bloccante). Pertanto, se all'atto dell'invocazione del metodo,
nessun task ha raggiunto il completamento, il metodo restituisce un valore nuli.
A questo punto dovrebbe essere chiaro che l'implementazione della classe ExecutorCompletionService
è abbastanza semplice. In effetti, l'esecuzione dei task è delegata alla classe di tipo Executor specifi-
cata nel costruttore, mentre per la gestione della coda degli oggetti risultato (Future) viene utilizzato
un apposito oggetto di tipo LinkedBlockingQueue. Qualora il comportamento di questo tipo di coda
non sia confacente alle necessità del sistema, è possibile utilizzare il secondo costruttore che per-
mette di specificare un parametro di tipo BlockingQueue<Future<V».
ADnendiceB J g J U l l U I W
Riferimenti bibliografici
[ U M L C O M ]
JOHN CHKKSMAN. J O H N DANIKI.S, VML Components. A Simple Process for Specifying Component-Based Software,
Addison Wesley
[ADVAUC]
[SJAVCC]
Standard Sun code convention for the Java Programming Language
http://java.sun.com/docs/codeconv/
[WRTRJCJ
Scott W. Ambler, Writing Robu st Java Code
http://www.ambysoft.com/javaCodingStandards.pdf
[WRJDOC]
H o w to write D o c Comments for the J a v a D o c tool
http://java.sun.com/j2se/javadoc/writingdoccomments/
UDOCHP]
J a v a D o c home page
http://java.sun.com/j2se/javadoc/
[THfNKJ]
[JAVNTS]
DAVID FI.ANAGAN, Java in a Nutshell. A desktop quick reference, O'Reilly
[S100PJ]
1 0 0 % P u r e J a v a ™ Cookbook. Guidelines for achieving the 1 0 0 % P u r e Java Standard, Sun MicroSystems
h t t p : / / j a v a . s u n . c o m / p r o d u c t s / a r c h i v e / 1 O O p e r c e n t / 4 . 1 . 1 / 1 0 0 P e r c e n t P u r e J a v a C o o k b o o k - 4 _ 1 _ 1 pdf
[ARTCP3]
DONALD E . KNUTH, The Art of Computer Programming. Volume 3. Sorting and Searching, Addison Wesley
[EFFCJA]
[EXCHIP]
J . B . GOODENOUGII, Exception Handling: issues and a proposed notation, "Communications of the A C M " , Volume 18
, Issue 12 (December 1975), pp. 6 8 3 - 6 9 6
[EXCPRL]
J O H N A Y C O C K , M I K E ZASTRI;, An Exceptional Programming Language
http://pages.cpsc.ucalgary.ca/~aycock/papers/plc05.pdf
[CNPRGJ]
DOUG LF.A, Concurrent Programming in Javatm: Design Principles and Pattern", 2 n J Edition, Addison Wesley, 1999
IJRS166]
J R S 166 - Concurrency Utility
http://www.jcp.org/en/jsr/detail?id=166
[UMLING]
LUCA VETTI TAGUATI, UML e ingcgncria del software. Dalla teoria alia pratica, Tecniche Nuove
[SFPNBB]
MAGED M. MICHAEL, MICHAEL L. SCOTT, Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue
Algorithms
http://www.es.rochester.edu/u/michael/PODC96.html
UUNREC]
J . B . Rainsberger, JUnit Recipes: Practical Methods for Programmer Testing, Manning Publications Co.
UUNACT]
[JUNPCK]
[GDLPRF]
HERNEST NAGEL, JAMES R. NEWMAN, Godel's proof, New York University Press, 2 0 0 1
[APCL4J]
[BTRMVN]
VINCENT MASSOL, JASON VAN ZYL, Better Builds With Maven, Mergere
h[ Tt t Sp T
Ross : / COI.LARD,
/DwSwGw ]. m e r g Test
e r e . cDesign,
o m / m 2 b"Software
o o k _ d o w n lTesting
o a d . j s p and Quality Engineering", August 1999
ALTRI TITOLI DISPONIBILI:
TECNICHE DI PROGETTAZIONE
SQL GUIDA POCKET
AGILE CON JAVA
Jonathan Gennick
Sandro Pedrazzini
• Brossura • 11x18 cm • 152 pagine
• Brossura • 17x24 cm • 298 pagine
ISBN: 978-88-481-1792-0 • 9,90 €
ISBN: 978-88-481-1916-0 • 29,90 €