Sei sulla pagina 1di 368

Luca Vetti Tagliati

lava Best Practice


I migliori consigli
per scrivere codice di qualità

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 2. Programmazione Java


Introduzione 21
Obiettivi 21
Direttive 22
2.1 Investire nello stile 22
2.2 Utilizzare accuratamente le "costanti" 22
2.3 Concludere correttamente i programmi Java 24
2.4 Scrivere correttamente i metodi 26
2.5 Implementare attentamente i metodi "accessori" e "modificatori" (get/set) 32
2.6 Utilizzare con oculatezza la classe java.lang.Runtime 36
2.7 Implementare i metodi Object 36
2.8 Porre attenzione alla chiusura degli stream 40
2.9 Tipi numerici 43
2.10 Selezionare attentamente le collezioni 49
2.11 Lavorare con le date 50
2.12 Problemi con il riutilizzo dei nomi 55

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

¡JU3JJ03U03 ¡SS333B l|iqiSSOd 3)U3U1BU3JJ0} 3JJJS39 / £


68 S BABf p3pB3Ji)j-|i|nuj !U0jZB}¡|ddv 9'£
8L ||BUO|Z¡pBJJ p3pB3il)J-|J|nUI |UO|ZB3||ddV S'£
9i ¡IjqCMCA ¡)U3UJ0ßjB :sßiBJBA fr£

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

Capitolo 7. Test di unità


Introduzione 169
Obiettivi 170
Un po'di teoria 170
I vantaggi dei test di unità 171
Processi iterativi e incrementali 172
La copertura dei test di unità 173
JUnit 178
JUnit prima della versione 4 178
JUnit versione 4 181
Direttive 182
7.1 Investire nei test di unità 182
7.2 Utilizzare JUnit 185
7.3 Test di unità e mock objects 187
7.4 Utilizzare tool di analisi della copertura 190
7.5 Scrivere test chiari ed efficaci 192

Capitolo 8. Test di integrazione


Introduzione 197
Obiettivi 198
Catalogazione dei test di integrazione 199
Sanity Check e Smoke Test 200
Alcuni tool 200
Direttive 201
8.1 Investire negli use case e test case 201
8.2 Gestire correttamente i vari progetti di test 202
8.3 Gestire correttamente i vari ambienti 204
8.4 Pianificare correttamente i test di integrazione 204
8.5 Verificare gli elementi principali 206
8.6 Test di applicazioni Java EE 207
8.7 Investire sui test della GUI 208
8.8 Test di integrazione delle performance 210

Capitolo 9. Organizzazione di un progetto, build e deploy


Introduzione 213
Obiettivi 214
I tool: filosofia e uso di Ant e Maven 216
Ant 216
Struttura 217
Maven 221
Breve storia di Maven 222
Obiettivi di Maven 223
Caratteristiche principali di Maven 224
Vantaggi di Maven 224
Componenti principali di Maven 225
Archetipo 226
POM 227
I repository di Maven 232
I comandi di Maven 236

Confronto tra Ant e Maven 237


Struttura di progetti medio-grandi 239
Direttive 240
9.1 Utilizzare strumenti moderni per il build 241
9.2 Organizzare correttamente il proprio progetto 242
9.3 Automatizzare il processo di build 243
9.5 Utilizzare i repository Maven 245
9.6 Distinguere i processi build e di personalizzazione 245

Appendice A. JavaDoc
Introduzione 247
L'utility JavaDoc 247

Appendice B. Tag HTML 255


Appendice C. Hashing
Introduzione 261
Collisioni 264
Gestione interna alla tabella 265
Gestione esterna alla tabella 267
Funzioni hash e numeri primi 271
Alcuni esempi di algoritmi di hash 271
Un test informale 277

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

Appendice E. Riferimenti bibliografici 349


Introduzione
Prefazione
La maggior parte dei lettori di questo libro, quasi sicuramente, avrà partecipato allo sviluppo
di progetti software scritti in Java, taluni in ambiti più professionali, altri in ambienti squisita-
mente accademici. Altrettanto probabilmente, durante l'attività di codifica, molti si saranno
trovati alle prese con una serie ricorrente di problemi, dubbi e necessità di comprendere, tra le
diverse soluzioni implementative, quale sia quella in grado di fornire migliori prestazioni, sia in
termini di computazione, sia di occupazione di memoria, sia di migliore qualità del codice, sia
di maggiore coerenza (o, come si usa dire con termine adattato dall'inglese, maggiore "consi-
stenza"), e così via. Infine, un gruppo più limitato di lettori, verosimilmente, si sarà trovato a
dover redigere standard aziendali relativi all'attività di programmazione. Ciò sia al fine di ga-
rantire che il software prodotto dalla specifica azienda rispetti prestabiliti livelli di qualità, sia
per assicurare che questo presenti un prefissato livello di consistenza, indipendentemente dallo
specifico progetto e/o team di sviluppo.
L'idea alla base di questo libro è proprio questa: fornire un compendio di risposte univoche,
operative e concise ai succitati problemi e non solo. Pertanto questo libro si propone come un
vademecum per i programmatori Java: una guida volta a fornire best practice, direttive e quan-
t'altro è necessario sia per migliorare la qualità del software prodotto sia per assicurare un
elevato livello di consistenza, non solo all'interno del medesimo gruppo di lavoro, ma anche tra
diversi team della stessa organizzazione.
La lettura di questo libro, inoltre, dovrebbe aiutare i programmatori meno esperti sia a rive-
dere una serie di errori che spesso finiscono involontariamente con il commettere in modo
sistematico, sia a renderli consapevoli di una serie di inesattezze e di insidie che ogni linguaggio
di programmazione pone, più o meno direttamente.
Questo libro è dunque un utile prontuario sia per l'attività di codifica, sia per quella, spesso
trascurata, di revisione del codice. Come tale, non è assolutamente un'alternativa ai libri di
testo che costituiscono il background fondamentale e irrinunciabile di ciascun programmatore
(cfr. la bibliografia); piuttosto ne è un'utile integrazione, una guida rapida e concisa che ogni
sviluppatore Java dovrebbe tenere a portata di mano.
Come ogni vademecum che si rispetti, anche questo intende fornire una serie di norme,
corredate da una breve spiegazione e da eventuali esempi pratici, senza però dilungarsi eccessi-
vamente sulla rispettiva teoria, per la quale si rimanda alla lettura di testi specifici. Tuttavia,
alcune tematiche particolarmente importanti e complesse, per esempio la programmazione multi-
threading, richiedono una trattazione più approfondita: la sola enunciazione delle linee guida
sicuramente non risulterebbe sufficiente alla comprensione da parte del pubblico ampio cui ci
si rivolge. Pertanto, al fine di mantenere coerente lo stile del libro, si è deciso di includere temi
complessi, ma sicuramente interessanti e utili a determinate categorie di lettori, in specifiche
appendici il cui obiettivo è proprio quello di approfondire le tematiche più "particolari".
Per quanto la programmazione OO e il linguaggio Java siano il fulcro del testo, molta impor-
tanza è attribuita anche a tematiche quali i test di unità e di integrazione, l'organizzazione dei
progetti in termini di file system, il processo di build e di deploy, che svolgono un ruolo fonda-
mentale per la produzione di software di elevata qualità. Sebbene l'elevata qualità del codice sia
una caratteristica fondamentale di ogni sistema, questa non è assolutamente sufficiente per
garantire un'altrettanta elevata qualità dei sistemi prodotti nel loro complesso: parafrasando
una frase accademica fin troppo inflazionata, si tratta della famosa CNMNS (Condizione Neces-
saria Ma Non Sufficiente).
Al momento in cui viene scritto questo libro, non esiste una definizione comune universal-
mente accettata di cosa si intenda con il termine di "qualità" del software, sebbene la quasi
totalità della comunità informatica concordi nell'asserire che realizzare software di elevata qua-
lità è un requisito irrinunciabile per l'ingegneria del software. Quello che è certo, tuttavia, è che
la qualità del codice è la punta dell'iceberg e che quindi è strettamente correlata alla modalità
con cui si svolgono tutta una serie di attività, come il disegno del sistema, la definizione dell'ar-
chitettura, l'analisi dei requisiti e così via. Non è infrequente infatti il rilascio di sistemi tecnica-
mente all'avanguardia, con codice di "elevata qualità", che però semplicemente non risolvono
le necessità degli utenti.
La situazione è decisamente diversa per il software di bassa lega. Non sembri un atteggiaménto
sprezzante definire certo software come "di bassa lega": la sua diffusione è un dato di fatto. In
questi casi, è possibile diagnosticare la cattiva qualità dall'analisi di alcuni semplici sintomi, quali:

• 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 1. Elementi base


Questo capitolo rappresenta l'avvio della trattazione e pertanto presenta una serie di linee
guida di carattere generale relative alla programmazione. La maggior parte di queste regole
hanno una validità indipendente dagli specifici linguaggi di programmazione e dal particolare
paradigma, sebbene particolare attenzione sia rivolta ai linguaggi basati sul paradigma OO e a
Java in particolare.
Le direttive presenti in questo capitolo hanno un carattere generale e pertanto introducono
argomenti relativi a come implementare classi ben disegnate, alla scelta dei nomi per le varie
entità di un programma, alla strutturazione dei metodi, all'implementazione di classi a elevata
coesione e minimo accoppiamento, e così via.

Capitolo 2. Programmazione Java


In questo capitolo si assiste alla transizione verso tematiche a maggiore carattere tecnico. Ciò si
riflette anche sul linguaggio di programmazione: Java assume il ruolo di fulcro, sebbene molte
regole continuino ad avere una valenza molto generale. In questo capitolo si affrontano tematiche
come l'utilizzo delle aree static, la verifica dei parametri, l'utilizzo delle interfacce, i down-
casting, problemi spessi ignorati relativi al trattamento di numeri reali, e così via.

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 5. Strategia di gestione delle eccezioni


Indipendentemente dal livello di qualità del codice prodotto, le eccezioni si verificano: e questo
è un dato di fatto. Ciò nonostante, la loro strategia di gestione può fare la differenza tra un
sistema robusto e uno instabile, fragile. Pertanto, è necessario che tutti i membri del team di
sviluppo concordino e applichino una strategia efficace e consistente di gestione delle eccezioni.
Obiettivo di questo capitolo è proprio questo: fornire una strategia consolidata ed efficace
per la gestione delle eccezioni.

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.

Capitolo 7. Test di unità


Una delle poche aree in cui tutti i processi di sviluppo del software concordano è l'importanza
dei test. In questo capitolo, in particolare, si focalizza l'attenzione sui test di unità {unti test)
implementati per mezzo dell'ormai famoso framework JUnit.
Questi test, come suggerisce il nome, sono disegnati per verificare il corretto funzionamento
di singole unità di codice, ossia classi/componenti, considerate isolatamente.
In modo analogo ai capitoli precedenti, il capitolo contiene una serie di linee guida su come
utilizzare efficacemente questo strumento e su come redigere efficaci test di unità.

Capitolo 8. Test di integrazione


1 test di integrazione (integration test) rappresentano un'altra importantissima fase di test an-
cora a carico del team di sviluppo. Mentre i test di unità servono per verificare che tutti i
moduli, considerati singolarmente, presentino il funzionamento atteso, in questa fase si verifica
che anche una volta assemblati tra loro in unità più complesse fino a raggiungere l'intero siste-
ma, questi moduli continuino a esibire il funzionamento previsto.
In questo capitolo si espongono una serie di consigli, relativi alla realizzazione dei test di
integrazione, ai tool disponibili, alle strategia da utilizzare e così via.

Capitolo 9. Organizzazione di un progetto,


build e deploy
In questo capitolo l'attenzione è focalizzata su due argomenti molto rilevanti: struttura del
progetto dal punto di vista del file system e processi di build e deploy. Sebbene coerentemente
con l'impostazione di questo libro, la presentazione dei tool utilizzabili per l'automazione del
processo di build non sia l'obiettivo principale del testo, tali sono l'importanza e il successo di
Ant e Maven che almeno le nozioni base vengono illustrate dettagliatamente. Sebbene per
molte persone il processo di build equivalga a un click del mouse su un apposito bottone del
proprio ambiente di sviluppo, in questo capitolo si vedrà quali rischi possono derivare da que-
sto approccio semplicistico e quali strategie adottare per far in modo che questi elementi con-
tribuiscano effettivamente al successo dell'intero progetto.

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 B. Tag HTML


Questa appendice è dedicata alla presentazione di una serie di tag HTML estremamente utili
per la scrittura di commenti doc.

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.

Appendice E. Riferimenti bibliografici


Una breve ma significativa raccolta con le indicazioni di libri, articoli e siti di riferimento.

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.

Breve biografia dell'autore


Luca Vetti Tagliati è nato a Roma il 03/12/1972. Ha studiato IT da sempre. Ha iniziato a
lavorare professionalmente nel 1991 occupandosi di sistemi firmware e assembler per micro-
processori della famiglia iAPx286. Nel 1994/95 ha intrapreso il lungo cammino nel mondo OO
grazie al meraviglioso linguaggio C++. Nel 1996 si è trovato catapultato nel mondo Java.
Negli ultimi anni ha applicato la progettazione/programmazione Component Based e SOA
in settori che variano dal mondo delle smart card (collaborazione con la Datacard,
www.datacard.com) a quello del trading bancario (www.hsbc.com, www.ubs.co.uk e www.lehman.com).
Attualmente lavora, di giorno, come Senior Architect/Development Manager presso la sede
londinese della banca Lehman Brothers e, nottetempo, si occupa di ricerca di processi di svi-
luppo del software alla Birkbeck University of London dove sta conseguendo un PhD. Ha
scritto il libro UML e l'ingegneria del software, pubblicato da Tecniche Nuove, e ha collaborato
attivamente con esperti del calibro di John Daniels ([UMLCOM]) e Frank Armour
([ADVAUC]).
È rintracciabile presso l'indirizzo lvettitagliati@mokabyte.it

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.

1.1.1 Selezionare nomi (relativamente) brevi


La prima regola per la selezione di un nome opportuno da assegnare a una classe consiste nello
scegliere un nome breve ma significativo. Si faccia attenzione che in questo contesto con il
termine "nome" si intende proprio l'elemento grammaticale: la particella linguistica che indica
esseri viventi, oggetti, idee, fatti o sentimenti. Pertanto, è necessario scegliere una stringa breve,
in grado di indicare le responsabilità principali della classe. Tipicamente, i nomi più efficaci
sono quelli attinti dal dominio che il programma intende automatizzare.

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.

C o m e si può notare, i programmi dovrebbero essere scritti utilizzando la lingua Inglese. Q u e s t o


per garantirne la massima audience possibile e per evitare brutture del tipo getEta, g e t A n n o N a s c i t a .
S e c o n d o le direttive I j a v a C C ] il n o m e delle classi deve essere scritto con la prima lettera in
maiuscolo e le rimanenti in minuscolo. Qualora, il nome della classe sia composto da più termini,
la prima lettera di ciascuna parola c o m p o n e n t e deve essere scritta in maiuscolo.

1.1.2 Valutare attentamente il ricorso alle abbreviazioni


Qualora la selezione di un nome breve sia ottenibile solo introducendo improbabili abbrevia-
zioni, è consigliato rilassare la regola sul contenimento della lunghezza di un nome e quindi

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.

Tabella 1.1- Esempi di nomi di classi derivanti dal dominio di riferimento.


abbandonarsi a nomi più lunghi. In alcune sporadiche occasioni, tuttavia, il ricorso ad abbre-
viazioni è assolutamente necessario per evitare di introdurre nomi troppo lunghi. In questo
caso, occorre utilizzare abbreviazioni in modo oculato, e soprattutto coerente, al fine di evitare
confusione.
Per esempio, per il calcolo del fattore di rischio di diversi trade è necessario consultare spe-
cifici valori di rischio denominati Future Fluctuatìon Risk Factors. Questi elementi sono indicati
con l'acronimo FFR dagli stessi operatori del business. Quindi, per la relativa implementazione,
è possibile e conveniente dar luogo a classi denominate FFR: FfrVO, FfrBS, etc.

1.1.3 Porre attenzione all'utilizzo di "verbi" per i nomi di classi


I nomi delle classi dovrebbero essere costituiti da nomi e non da verbi. Questo perché le classi
dovrebbero rappresentare entità, oggetti del dominio e non azioni. Pertanto, ad eccezione di
alcuni casi molto limitati, qualora si abbia la necessità di denominare una classe con un verbo,
bisognerebbe interrogarsi circa le responsabilità della classe, se abbia veramente senso creare
una classe, e su quali siano proprietà le fondamentali, come coesione e accoppiamento. Nel
caso in cui questo controllo abbia esito positivo, è importante ricordare che quasi sempre è
possibile passare da un verbo ad un nome. Come esempio si consideri l'applicazione del pattern
Command.

Il Command, molto brevemente, è un pattern che permette di richiedere a un oggetto l'esecuzione


di determinate azioni, appositamente incapsulate in istanze di una predefinita classe, senza che
l'istanza richiedente sia al corrente dei dettagli dell'operazione da eseguire né del concreto oggetto
che la eseguirà. L'elemento fondamentale di questo pattern è la classe astratta, tipicamente
denominata Command, che astrae il comportamento comune di tutte le classi che implementano
comandi concreti. Queste, frequentemente, sono denominate con il nome del comando, che
spesso è un verbo, come per esempio Save, Read, Update, etc. Anche in questo contesto, tuttavia,
il nome può essere, frequentemente, ottenuto da una composizione, come per esempio FileSaver,
FileReader, etc. Ed ecco che il nome della classe torna a essere un nome.

1.1.4 Non utilizzare i nomi delle classi delle librerie Java


In alcune occasioni è possibile trovarsi di fronte a classi predefinite che non implementino
esattamente i requisiti richiesti o che necessitano di essere ulteriormente estese per potervi
aggiungere l'ulteriore comportamento necessario. Alcuni sviluppatori risolvono questo scena-
rio implementando delle classi con lo stesso nome di quelle base. Questa stessa tecnica, ahimè,
è stata utilizzata dai disegnatori del linguaggio Java in diversi casi come con le classi java.Util.Date
e java.sql.Date. Questa è una pratica assolutamente sconsigliata, soprattutto qualora si intenda
riutilizzare il nome di classi appartenenti alla piattaforma Java (come per esempio quello delle
classi del package java.lang." automaticamente importato in tutte le classi).
Per quanto Java fornisca una serie di meccanismi per poter distinguere le varie classi grazie al
fully qualìfied name, il codice divine confuso, diviene sempre necessario o opportuno ripetere il
nome completo della classe, il codice è meno elegante, poco leggibile, etc.
Pertanto è sempre consigliabile evitare di riusare il nome delle classi, soprattutto di quelle
appartenenti alla piattaforma Java. E sempre possibile e opportuno definirsi nomi simili ma
non uguali, come per esempio MySystem, BasicString, Enhancedlnteger, etc.
1.1.5 Aggiungere al nome della classe un suffisso
relativo allo strato di appartenenza
Sebbene al momento in cui viene scritto questo libro, non ci sia un accordo nella comunità
informatica circa il significato della parola "architettura", una sua caratteristica su cui è possi-
bile individuare un accordo unanime è che le architetture dovrebbero essere multi-strato (mul-
ti-layered o multi-tiered)-, organizzate in una serie di strati (layer o tier) adiacenti e comunicanti.
Pertanto, in queste architetture è frequente che diverse versioni di una stessa classe esistano in
diversi strati con diverse responsabilità. In questi circostanze è conveniente aggiungere al nome
delle classi, in maniera coerente, opportuni suffissi atti a identificarne lo strato di appartenenza
(tabella 1.2).
Altri utili suffissi sono VO e DTO (TrolleyVO, TrolleyDTO) relativi, rispettivamente, a Value Object
e Data Transfer Object; questi non appartengono necessariamente a un solo strato ma, anzi,
vengono tipicamente utilizzati per trasportare informazioni tra strati differenti.
Questa convenzione offre il grande vantaggio di poter derivare molte informazioni relative a
una specifica entità già dalla semplice lettura del nome. Qualora si decida di utilizzare una
simile convenzione, tuttavia, è di fondamentale importanza utilizzarla con tassativa coerenza
onde evitare inutili confusioni.

1.1.6 Valutare l'utilizzo di appositi prefissi per evidenziare


interfacce e classi astratte
Per quanto non si tratti di una strategia utilizzata dalla Sun, spesso risulta conveniente aggiun-
gere, al nome delle classi, un prefisso, composto da un solo carattere, per evidenziare se la
classe sia astratta o concreta e si tratti o meno di un'interfaccia. Pertanto la convenzione consi-
ste nell'utilizzare:

• prefisso I per le interfacce. Per esempio IRepository, IConfigurationManager, etc.


• prefisso A per denotare classi astratte. Per esempio AltemsRepository, ADoubleLinkedUst.

Il vantaggio offerto da questa nomenclatura consiste nella possibilità di risalire ad alcune


informazioni utili circa una determinata classe o interfaccia già dalla sola lettura del nome.
Qualora si decidesse di utilizzare questa convenzione, è fondamentale applicarla consisten-
temente altrimenti si correrebbe il rischio di generare inutile confusione. Per esempio, la lettu-
ra di un nome di classe non prefissato dalla lettera I o A, porterebbe alla legittima conclusione
che si tratti di una classe concreta e quindi il programmatore sarebbe portato a trattarla di

Strato Dizione inglese Esempi


strato di presentazione presentation layer TrolleyPage, TrolleyHandler
strato di business Business Service layer TrolleyBS
strato di business object Business Object layer TrolleyBO
strato di integrazione integration layer TrolleyDAO

Tabella 1.2 - Strati di una classica architettura multi-tiered.


conseguenza, mentre, invece potrebbe trattarsi di una classe astratta o di un'interfaccia deno-
minata in maniera incorretta.

1.1.7 Utilizzare il suffisso "Exception" per indicare le classi eccezione


L'utilizzo del termine Exception per indicare classi atte a incapsulare eccezione è un'utile con-
venzione utilizzata della Sun che dovrebbe essere sempre rispettata. Per esempio:
NulIPointerException, HlegalArgumentException, etc.

1.2 Selezionare nomi significativi


per attributi/variabili e parametri
Variabili e attributi sono elementi fondamentali della programmazione, tanto che Dijkstra sosteneva
che "una volta che un programmatore ha compreso l'utilizzo delle variabili, ha compreso l'essenza
della programmazione". Gli attributi, poi, sono a tutti gli effetti variabili incapsulate in oggetti,
utilizzate per mantenere lo stato degli stessi. Pertanto, una corretta ed efficace definizione dei
loro nomi costituisce una condizione irrinunciabile per ottenere software di qualità.

1.2.1 Selezionare nomi (relativamente) brevi


Coerentemente a quanto specificato per i nomi delle classi, anche i nomi di attributi/variabili e
parametri devono essere mnemonici e brevi (anche in questo caso diverse letterature, non ec-
cessivamente moderne, identificano in 15 lettere il valore della lunghezza massima del nome di
un attributo). Coerentemente con l'analoga direttiva valida per le classi, anche per i nomi di
attributi, variabili e parametri è necessario cercare di utilizzare sostantivi (nomi). Inoltre, anche
per gli attributi, quando possibile, è utile ricorre a nomi attinti dal dominio di riferimento.
Per esempio, una classe Currency potrebbe disporre dei seguenti attributi: mainllnitName (per
e s e m p i o Euro), secondaryllnitName (per e s e m p i o Cent), symbol, currencySign, decimalPositions, etc.
Se usassimo l'italiano, una classe Libro potrebbe disporre dei seguenti attributi: titolo, autori,
n u m e r o P a g i n e , caseEditrici, prezzoConsigliato, 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).

! "

' C o n s t r u c t s a newly allocateti < c o d e > B y t e < / c o d e > object that


* represents the specifled < c o d e > b y t e < / c o d e > value.
* @param value the value to be represented by the
<code>Byte</code>.
7
public Byte(byte aValue) I
value = aValue;
I

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.

public linai Buffer limit(inl newLimit) I


it ((newLimit > capacity) || (newLimit < 0))
throw new HlegalArgumentException();

limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;

return this;
)

1.2.2. Valutare attentamente il ricorso alle abbreviazioni


I nomi di variabili spesso richiedono la combinazione di due o più termini. Le abbreviazioni, in
linea di principio, tendono a ridurre il grado di leggibilità del codice. Tuttavia, qualora il nome
di un attribuito tenda a divenire eccessivamente lungo, è consigliabile valutare opportune ab-
breviazioni: l'importante però è che queste siano comprensibili e soprattutto utilizzate coeren-
temente. Per esempio:

/" ' If the next character is a line feed, skip it ' /


privale boolean skipLF = false;

/ * * default char buffer size */


private sialic ini defaultCharBufSize = 8192;

1.2.3 Considerare l'utilizzo delle lettere i, j, k per le variabili di ciclo


Le lettere i, j, k, grazie anche ad un retaggio matematico, risultano ottimi nomi per le variabili che
regolano cicli come for e while. Inoltre, per cicli annidati è consigliabile seguire l'ordine alfabetico.
Qualora, il ciclo sia semplice, è possibile utilizzare anche nomi come counter. Infine, qualora si
abbia la necessità di eseguire computazioni su matrici bidimensionali, potrebbe risultare conve-
niente utilizzare i seguenti indici: row, col. Ecco un esempio di uso delle variabili contatore.

lor (ini row=0; row < MAX_R0WS; row++) (

lor (ini col=0; col < MAX_C0LS; col++) I


<istruction>
<instruction>
I

1.2.4 Evitare di utilizzare nomi simili nell'ambito dello stesso scope


Molti linguaggi di programmazione, tra cui Java, prevedono sintassi case-sensitive-, sensibili al
carattere. Pertanto, caratteri maiuscoli e minuscoli sono considerati diversi (per esempio: Count
è diverso da count). In questo caso, è necessario evitare nomi simili che differiscano semplice-
mente per la modalità di scrittura, per esempio: currencylsoCode, currencylSOCode. In generale è
sempre opportuno evitare nomi simili nell'ambito del medesimo scope (campo d'azione).
Nel seguente, su due colonne, sono mostrate due implementazione del metodo rehash della
classe java.util.Hashtable. L'implementazione di sinistra è stata volutamente resa meno leggibile
utilizzando nomi di variabile molto simili tra loro.

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.

Codice sconsigliato Codice più leggibile


protected void rehashf) { protected void rehash() I
int cap = table.length; int oldCapacity = table.length;
Entry[] map = table; Entry[] oldMap = table;

int capacity = cap * 2 + 1; int newCapacity = oldCapacity * 2 + 1 ;


Entry[] m a p p = new Entryfcapacity]; Entry[] n e w M a p = new Entry[newCapacity];

modCount++; modCount++;
threshold = (int) (capacity * loadFactor); threshold = (int) (newCapacity * loadFactor);
table = mapp; table = newMap;

for (int i = cap ; i - > 0 ;) I for (int i = oldCapacity ; i - > 0 ;) I

tor (Entry<K,V> mapn = map[i]; m a p n ! = n u l l ; ) { for (Entry<K,V> old = oldMap[i] ; old != null ; ) (

Entry<K,V> e = mapn; Entry<K,V> e = old;

mapn = mapn.next; old = old.next;

int ¡1 = (e.hash & 0x7FFFFFFF) % int index= (e.hash & 0x7FFFFFFF) %

capacity; newCapacity,

e.next = mapp[i1]; e.next = newMap(index);

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.

1.2.6 Valutare attentamente il ricorso al carattere sottolineato


come prefisso delle variabili di classe
La convenzione di utilizzare il carattere sottolineato basso (underscore, il segno _) per indicare
attributi di classe è una prassi molto utilizzata nella programmazione C++. In Java ciò non è
assolutamente necessario, giacché il linguaggio dispone della parola chiave this. Si osservi que-
sto costruttore della classe java.lang.Boolean.

public Boolean(boolean valué) I


thisvalue = value;
I

Inoltre, come visto in precedenza, in questi casi è sufficiente assegnare al parametro un pre-
fisso costituito dall'articolo indeterminativo: aValue.

1.2.7 Qualora si decida di utilizzare la notazione ungherese,


la si utilizzi coerentemente

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

Tabella 1.3 - Prefissi standard da utilizzarsi per la notazione ungherese.

1.3 Selezionare nomi significativi per i metodi


1.3.1 Selezionare nomi brevi
I nomi dei metodi devono essere sufficientemente brevi ma significativi, in modo da riuscire a
illustrare chiaramente il servizio che forniscono. I metodi rappresentano azioni e pertanto i
loro nomi dovrebbero includere un verbo. Qualora poi non si tratti di metodi di servizio, i nomi
dovrebbero essere facilmente riconducibili al linguaggio business.
Da tener presente che i vari metodi, tipicamente, eseguono qualche azione relativa alla classe
cui appartengono. Pertanto, il nome della classe dovrebbe essere omesso soprattutto per i me-
todi non privati.
Per esempio, qualora si consideri una classe carrello della spesa (Trolley) per il nome del
metodo atto a verificarne la consistenza è sufficiente considerare check invece che checkTrolley.
Altri esempi di metodi della classe "carrello della spesa" sono: addltem, deleteltem, isEmpty, etc.

1.3.2 Valutare attentamente il ricorso alle abbreviazioni


Utilizzare abbreviazioni per nomi dei metodi tende a diminuirne il grado di comprensione e a
creare confusione. Pertanto il loro utilizzo dovrebbe essere sempre limitato a casi in cui non
ricorrere alle abbreviazione genererebbe nomi di metodi troppo lunghi. Qualora si decida di
introdurre delle abbreviazioni, come di consueto, è opportuno utilizzarle in modo standard.
Per esempio, una delle operazioni necessarie per eseguire il settlement (gestione del paga-
mento) di un trade consiste nell'individuare tra le istruzioni di pagamento (Standard Settlement
Instructions) predefinite dalla controparte quella più opportuna. In questo caso, un metodo
potrebbe essere getMatchingSsi. Da notare che l'acronimo SSI è di uso comune anche tra gli
operatori business.

1.3.3 Qualora un metodo svolga più azioni logicamente distinte,


aggiungere le particelle "and" o "or" nel nome
Spesso l'implementazione di un metodo richiede di includere diverse azioni di alto livello che
è opportuno evidenziare chiaramente già dal nome. Non è infrequente il caso in cui sia neces-
sario includere due azioni distinte nello stesso metodo, perché, per esempio debbano condivi-
dere una stessa transazione o perché è conveniente avere una sezione protetta in comune.
Qualora queste azioni siano eseguite sequenzialmente, allora è necessario specificare la con-
giunzione and tra i nomi di queste azioni, mentre qualora le azioni siano eseguite in alternativa,
allora è necessario utilizzare la congiunzione or. Per esempio: updateOrlnsertltem,
saveAndPublishMessage,

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.

1.4 Implementare classi orientate agli oggetti


1.4.1 Evitare l'implementazione di classi di grandi dimensioni
Classi di grandi dimensioni sono sempre da evitare in quanto tendono a diventare difficilmente
comprensibili e quindi manutenibili, a complicare i test, a rendere problematico Io sviluppo in
parallelo da parte di team di diverse persone e a ridurre il riutilizzo.
Classi di grandi dimensioni, spesso, sono il risultato dell'inglobamento di diverse classi in
una e pertanto si prestano a dare luogo a implementazioni a bassa coesione che, come i principi
dell'OO insegnano, andrebbero evitati. La tendenza naturale di sistemi OO è quella di genera-
re molte classi di dimensioni medio-piccole, facili da comprendere, manutenere, verificare e
riusare. Ciò, tra l'altro, favorisce il naturale processo di apprendimento della mente umana che
richiede, in ogni momento, di concentrasi su un insieme limitato di aspetti. Un numero ecces-

Nome metodo Utilizzo


add Inserimento di un elemento in una lista.
addAII Inserimento di una collezione in una lista.
clear Rimuove tutti gli elementi di una lista.
get Reperimento di un elemento specifico di una lista.
remove Rimozione di un elemento.
size Restituzione del numero degli elementi di una lista.

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.

1.4.2 Utilizzare il livello di accesso più ristretto possibile


Il livello di accesso di un elemento (classe, interfaccia, metodo e attributo) determina quali
altre classi appartenenti allo stesso sistema possono accedere all'elemento in questione.
Una delle leggi fondamentali dell'OO, l'incapsulamento (nota anche come principio
dell'information hiding), prescrive che le classi celino al mondo esterno la propria organizzazio-
ne in termini di struttura e logica interna. Il principio fondamentale è che nessuna parte di un
sistema debba dipendere dai dettagli interni di una sua parte. Questo, come è lecito attendersi,
rende possibile modificare la struttura interna delle classi di un sistema evitando che si generi-
no ripercussioni su altre parti dello stesso.
Per esempio, succede varie volte di accorgersi, durante la fase di test, che determinati oggetti
danno luogo a un eccessivo consumo di memoria oppure eseguono specifiche operazioni con
performance insoddisfacenti. In questi casi, qualora si sia utilizzato con intelligenza il principio
dell'incapsulamento, è possibile reingegnerizzare le classi da cui derivano tali oggetti, senza
dover modificare altre parti del sistema.
I linguaggi di programmazione OO tendono a prevedere i seguenti quattro livelli di accesso,
riportati in ordine di accesso crescente: privato, amichevole o di default, protetto e pubblico. Al
fine di evitare qualsiasi confusione, in tabella 1.5 è riportato uno schema dei livelli di accesso Java.

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

Livello di accesso Stessa Stesso Classe Tutte le


classe package ereditante altre
Privato
• X X X
private
Amichevole
• •/ X X
non specificato

Protetto
s V V X
protected
Pubblico
• •/ •
public

Tabella 1.5 - Livelli di accesso in Java.


delle specializzazioni (classi ereditanti) che, per loro natura, sono fortemente legate alla classe
antenata (come per esempio la classe java.util.AbstractCollection e le varie collezioni concrete),
allora potrebbe risultare molto utile dichiarare alcuni attributi con un livello di accesso protet-
to. Il livello di accesso di default, invece potrebbe risultare utile nella realizzazione di package
grafici. Infine il livello di accesso pubblico dovrebbe essere riservato esclusivamente alla di-
chiarazione di costanti.
Dato che una buona implementazione O O prevede che tutti gli attributi siano privati, ne
segue che l'interfaccia propria di una classe sia data dai suoi metodi, la cui invocazione rappre-
senta l'unico modo con cui i vari oggetti possano interagire tra loro. Pertanto, i metodi dovreb-
bero essere dichiarati privati (meglio se protetti, al fine di consentirne l'accesso a possibili classi
ereditanti), a meno che non si tratti di metodi appartenenti all'interfaccia della classe (metodi
che possano essere invocati da altre classi). In questo caso, il livello di accesso da scegliere è
pubblico (se invocabile da qualsiasi classe) o di default (se invocabile da classi appartenenti allo
stesso package).

1.4.3 Massimizzare la coesione interna delle classi


"Un modulo presenta un'elevata coesione quando tutti i componenti collaborano fra loro per
fornire un ben preciso comportamento" (Booch). Dal punto di vista delle classi, la coesione è la
misura della correlazione delle proprietà strutturali (attributi e relazioni con le altre classi) e
comportamentali di una classe (metodi). Nella letteratura informatica è possibile individuare diverse
formule matematiche atte a determinare il grado di coesione di una classe. Una delle più note è:

(m - I ( n r ) / a )
c=
( m -1)

dove

m è il numero di metodi della classe


3 è il numero di attributi della classe.
mj è il numero di metodi che accedono all'attributo i mo.
Urr^) è la sommatoria di tutti i contributi rn calcolati per tutti gli attributi della classe.

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.

1.4.4 Minimizzare l'accoppiamento tra le classi


Un principio di importanza fondamentale nel disegno e nell'implementazione delle classi è rela-
tivo al grado di accoppiamento che, chiaramente, deve essere minimizzato. Massima coesione e
minimo accoppiamento sono due proprietà imprescindibili e fortemente relazionate dell'OO. Il
minimo accoppiamento è particolarmente ricercato in quanto capace di generare tutta una serie
di vantaggi (del tutto equivalenti a quelli generati dalla massima coesione), quali maggiore com-
prensione e facilità di manutenzione del codice, aumento della probabilità di riutilizzo, sempli-
ficazione delle attività di manutenzione, ecc. Il livello di accoppiamento delle classi deve essere
minimizzato ma, ovviamente, non eliminato: non esiste un reale disegno OO senza accoppia-
mento. Ogni volta che un oggetto interagisce con un altro si ha una manifestazione di accoppia-
mento. La mancanza totale di accoppiamento porterebbe generare l'effetto contrario, ossia la
produzione di codice poco leggibile dovuto a classi (scarsamente coese) omnicomprensive.
L'accoppiamento è definita coma la "misura della dipendenza tra componenti software (clas-
si, package, componenti veri e propri, ecc.) di cui è composto il sistema". Sia ha una dipenden-
za tra due classi, quando un elemento (client) per espletare le proprie responsabilità accede alle
proprietà comportamentali (metodi) e/o strutturali (attributi) di un altro (supplier, fornitore).
Ciò implica che il funzionamento del componente stesso dipende dal corretto funzionamento
(e quindi dall'implementazione) degli altri componenti "acceduti"; pertanto cambiamenti in
questi ultimi generano ripercussioni sul componente client. Come scritto poco sopra, il livello
di accoppiamento deve essere ridotto al minimo, ma non eliminato.

1.5 Porre attenzione alla scrittura dei metodi


1.5.1 Implementare metodi che eseguono un solo compito
Come principio di carattere generale bisognerebbe implementare metodi che svolgano una
sola funzione ben definita. Ciò permette di realizzare codice di maggiore leggibilità, quindi più
facilmente comprensibile, manutenibile, riusabile, e così via. Ciò, inoltre, concorre ad aumen-
tarne il livello di coesione. Ecco codice con metodi che eseguono un solo compito ben definito.
' expands the capacity of an A b s t r a c t S t r m g B u i l d e r obejct
' è ' p a r a m m i n i m u m C a p a c i t y m i n i m u n capacity requested to this object.
• t
void expandCapacity(int minimumCapacity) I
int newCapacity = (value.length + 1) * 2;

il (newCapacity < 0) (
newCapacity = lnteger.MAX_VALUE;
I else il (minimumCapacity > newCapacity) I
newCapacity = minimumCapacity;
I

char newValue[] = new char[newCapacity];


System.arraycopyfvalue, 0, newValue, 0, count);
value = newValue;
I

1.5.2 Evitare l'implementazione di metodi lunghi


Metodi la cui implementazione risulti maggiore di circa 15 righe risultano di difficile compren-
sione e quindi di difficile test, manutenzione e riutilizzo. Una tecnica decisamente più conve-
niente consiste nello scrivere metodi la cui intera implementazione sia visualizzabile in una
pagina di un normale monitor evitando così di dover eseguire continui scroll della pagina.
Questo approccio è poi confacente alla caratteristica tipica della mente umana di concentrasi,
in ogni momento, su un insieme ristretto di concetti.
Ogni qualvolta l'implementazione di un metodo ecceda le dimensioni suddette, è meglio
tentare di ridurlo nella composizione di metodi più piccoli di più facile comprensione.

1.5.3 Non scrivere metodi con molti parametri


Ogni qualvolta la firma di un metodo contenga troppi parametri (tipicamente più di quattro o
cinque), è necessario verificare se sia il caso di incapsulare questi parametri in un'apposita
classe o permetterne l'impostazione attraverso opportuni metodi. Questa regola può essere
rilassata per i metodi costruttori di oggetti disegnati puramente per trasportare valori, per esempio
Value Object (VO) e Data Transfer Object (DTO). Anche in questi casi però, disegnare metodi
costruttori con diversi parametri può creare non pochi problemi in presenza di gerarchie di
oggetti. Infatti, la variazione del costruttore di una classe antenata (per esempio l'aggiunta di un
nuovo parametro) finisce con il ripercuotersi in tutte le classi ereditanti.
Il problema è che metodi la cui firma contiene una lunga lista di parametri sono difficili da
utilizzare, il loro utilizzo richiede di controllare continuamente il significato e la posizione dei
parametri, etc. Pertanto, metodi di questo tipo possono essere causa di frequenti errori.
Si consideri come esempio di metodo con molti parametri, il costruttore della classe
SimpleTimeZone: riportato nel listato seguente.

public SimpleTimeZone(int rawOffset, String ID,


int startMonth, int startDay, int startDayOfWeek, _ int startTime,
int endMonth, int endDay, int endDayOfWeek, int endTime)
1.5.4 Eseguire il controllo del valore dei parametri appena possibile
E opportuno far sì che le prime linee di codice dell'implementazione di ogni metodo siano
relative al controllo della validità del valore dei parametri forniti. Se il metodo è destinato a
fallire è opportuno che fallisca il prima possibile. Questa regola è particolarmente importante
per i metodi costruttori. Le tipiche eccezioni (Java standard) considerate per comunicare que-
sto tipo di errore sono NulIPointerException e HlegalArgumentException. Si consideri l'implementazione
di uno dei costruttori della classe java.util.Vector.

public Vector(inl initlalCapacity, int capacitylncrement) I


super();

il (InitialCapacity < 0)
throw new IHegalArgumentExceptionf'lllegal Capacity: "+initialCapacity);

this.elementData = new Object[initialCapacity];


this.capacitylncrement = capacitylncrement;
I

1.5.5 Estrarre il comportamento comune


Nell'implementare metodi di una classe, non è infrequente il caso in cui una determinata sezio-
ne di codice sia ripetuta in diversi metodi, magari con minime differenze. Questa evenienza,
tipicamente, porta ad implementare metodi lunghi e quindi meno leggibili. Inoltre, è naturale
che una stessa porzione di codice sia ripetuta in diverse parti. Ciò fa sì che eventuali modifiche
relative a tale sezione debbano essere ripetute diverse volte con il concreto rischio di tralasciar-
ne qualcuna. In questi casi è opportuno verificare la possibilità di raggruppare il comporta-
mento comune, eventualmente parametrizzandolo, in appositi metodi privati. Si consideri il
codice del listato seguente, con un'improbabile implementazione del metodo setLength della
classe java.lang.AbstractStringBuilder.

public void setLength(int newLength) I


if (newLength < 0)
throw new StringlndexOutOfBoundsException(newLength);
if (newLength > value.length) {

int newCapacity = (value.length + 1) * 2;

ensureCapacity
if (newCapacity < 0) (
newCapacity = lnteger.MAX_VALUE
) else if (newLength > newCapacity) {
newCapacity = minimumCapacity;

char newValue[] = new char[newCapacity];


System.arraycopy(value, 0, newValue, 0, count);
value = newValue
if (count < newLength) I
for (; count < newLength; count++)
resetArray
value[count] = '\0';
I else {
count = newLength;
)

1.5.6 Intervallare il codice con apposite righe vuote


Dall'analisi del codice dei metodi è possibile notare come alcuni gruppi di istruzioni eseguano
compiti ben definiti e quindi risultino fortemente interconnesse. Questi gruppi formano le parti-
celle dell'implementazione del metodo. Pertanto costituisce una buona norma evidenziare questi
gruppi a maggiore coesione separandoli dagli altri tramite righe vuote. Codici organizzati in questo
modo sono più facili da leggere e da comprendere. Vediamo nel listato successivo, su due colonne,
due versioni del metodo equals presente nella classe java.util.AbstractList. In quella di sinistra (sconsi-
gliata) le istruzioni sono scritte in sequenza senza separare i diversi blocchi. In quella di destra,
invece, sono state aggiunte delle righe vuote per assegnare più enfasi a gruppi logici di istruzioni.

Codice sconsigliato Codice consigliato

public boolean equals(Object o) {

Z/ I A\ if (o == this)
return true;

public boolean equals(0bject o) I if (!(o Instanceof List))


If (o == this) return false;
return true;
if (!(o instanceof List)) Listlterator<E> e1 = listlterator();
return false; Listlterator e2 = ((List) o).listlterator();
Listlterator<E> e1 = listlterator();
Listlterator e2 = ((List) o).listlterator(); while (e1.hasNext() && e2.hasNext()) I
while (e1 hasNext() && e2.hasNext()) { Eo1 =e1.next();
E o1 = e1.next(); Object o2 = e2.next();
Object o2 = e2.next();
If (!(o1==null ? o2==null : o1.equals(o2))) If (!(o1==null ? o2==null : o1.equals(o2)))
return false; return false;
} }
return !(e1.hasNext() || e2.hasNext());
I return !(e1.hasNext() || e2.hasNext());

1.5.7 Valutare l'utilizzo delle parentesi graffe


anche quando non è strettamente necessario
L'utilizzo delle parentesi graffe anche quando non strettamente necessario (per esempio nel
caso di cicli con una sola istruzione) è una buona tecnica per migliorare lo stile del proprio
codice. In particolare, semplifica la lettura e la manutenzione del codice, specie qualora il
codice contenga una serie di costrutti annidati, e agevola la manutenzione del codice qualora
sia necessario aggiungere ulteriori istruzioni in un costrutto che originariamente ne conteneva
una sola. Nel listato seguente, su due colonne, due versioni di un metodo costruttore della
classe java.io.File. Quello di destra (corretto) utilizza le parentesi graffe anche in situazioni in cui
non è strettamente necessario al fine di favorire la leggibilità del codice.

Codice sconsigliato Codice consigliato

public File(String parent, String child) I


¡((child == null) {
throw newNullPointerExceptionO;
public File(String parent, String child) { I
it (child == null) ¡»(parent != null) I
throw new NullPointerException(); if(parent.equals("")) I
it (parent != null) this.path =fs.resolve(fs.getDefaultParent(),
if (parent.equalsf")) fs.normalize(child));
this.path = fs.resolve(fs.getDefaultParent(), I elsel
fs.normalize(child)); this.path = fs.resolve(fs.normalize(parent),
else fs.normalize(child));
this.path = fs.resolve(fs.normalize(parent), I
fs.normalize(child)); ) elsel
else this.path = fs.normalize(child);
this.path = fs.normalize(child); 1
this.prefixLength = fs.prefixLength(this.path); this.prefixLength = fs.prefixLength(this.path);

1.5.8 Implementare metodi a minimo accoppiamento


In prima analisi, i metodi di ciascuna classe possono essere suddivisi in tre macro-categorie:

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 Utilizzare correttamente l'ereditarietà


L'ereditarietà è probabilmente la legge più nota dell'OO e, verosimilmente, anche quella più
abusata. Brevemente, l'ereditarietà è un meccanismo attraverso il quale un'entità più specifica
incorpora struttura e comportamento definiti da entità più generali. Se da una parte è vero che,
qualora utilizzata correttamente, essa permette una migliore ristrutturazione gerarchica
(raggruppare il comportamento condiviso da più classi in una versione generalizzata incapsulata
in un'apposita classe antenata), dall'altro bisogna tenere in mente che l'ereditarietà non è esente
da controindicazioni come per esempio il forte legame di dipendenza che si instaura tra classe
antenata e quelle discendenti (modifiche eseguite su una classe antenata tendono a ripercuotersi
su tutte le classi discendenti) e la staticità e rigidità della struttura gerarchica. In particolare, una
volta che una classe sia incastonata in questo tipo di organizzazione, non ne potrà più uscire.

1.6.1 Ereditare il tipo di una classe e non solo gli attributi/metodi


Utilizzare la relazione di ereditarietà per dar luogo ad una "specializzazione" dipendente dal
tipo di una classe e non semplicemente per condividere pochi attributi e/o metodi. Con il tipo
di una classe, brevemente, si intende la sua "interfaccia implicita" (elenco delle proprietà strut-
turali e comportamentali esposte ad altre classi). Il problema è che non è raro il caso di relazioni
di ereditarietà realizzate semplicemente per "ereditare" opportune porzioni di implementazione.
Ciò dovrebbe costituire una conseguenza e non un principio.

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.

Figura 1.1 - Esempio di utilizzo errato della relazione di ereditarietà.


Capitolo 2
Programmazione Java
Introduzione
Dopo aver investigato nel corso del capitolo precedente le nozioni di carattere generale relative
alla programmazione, con particolare riferimento ai linguaggi basati sul paradigma OO, in
questo capitolo l'attenzione è completamente focalizzata sul linguaggio Java. Pertanto, sebbene
alcune considerazioni continuano a mantenere una valenza generale, molte regole qui presenta-
te hanno un dominio di applicazione strettamente limitato al linguaggio Java.
Le nozioni presenti in questo capitolo, come lecito attendersi, presentano un elevato grado
di operatività, e includono indicazioni relative a istruzioni il cui utilizzo dovrebbe essere evitato
(per esempio System.exit), a suggerimenti atti ad utilizzare i formati numerici correttamente
evitando classiche trappole, a spiegazioni relative al sempre enigmatico hash (spiegato in detta-
glio nell'Appendice C), a consigli utili per gestire gli stream, etc.
Alcuni insidie riportate in questo capitolo tendono a essere regolarmente trascurate da molti
programmatori. Il caso più evidente è quello legato ai tipi reali. In questo caso, tutto procede
regolarmente finché, eseguendo particolari calcoli scientifici, o importanti computazioni mo-
netarie, ci si trova nella situazione di dover individuare le ragioni per cui alcune computazioni
generino risultati diversi da quelli attesi. In questo caso, l'esperienza insegna che spesso si fini-
sce con il dissipare moltissimo tempo prima di comprendere che il problema sta proprio alla
radice: i tipi reali base sono intrinsecamente imprecisi. Si tratta di problemi subdoli da risolvere
poiché i relativi effetti si manifestano solo in alcuni casi ed con modalità diverse.

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.

2.1.1 Adottare lo stile di programmazione standard Java


La Sun Microsystem, fin dalle primissime versioni del linguaggio di programmazione Java, ha
pubblicato un documento relativo allo standard di programmazione Java (cfr. [SJAVCC]). Questo
include direttive relative al nome delle classi Java, all'organizzazione dei file, all'indentazione
del codice sorgente, all'organizzazione delle dichiarazioni, dei costrutti e dei cicli, alle conven-
zioni sui nomi, etc.
Disponendo di una convenzione standard ben collaudata, documentata accuratamente, con-
divisa da una larghissima comunità di programmatori distribuiti su l'intero globo terrestre,
perché tentare di inventarne un'altra?
Inoltre, utilizzando questa notazione, si evitano una serie di problematiche, come per esem-
pio Xhiding dei tipi, descritto a fine capitolo.

2.2 Utilizzare accuratamente le "costanti"


Nel linguaggio Java non esistono costanti nel senso tradizionale del termine. Questo è dovuto al
fatto che i progettisti del linguaggio Java hanno deciso di non dotarlo di un precompilatore onde
evitare la proliferazione di alcuni tipici abusi presenti nei linguaggi C e C + + (vedere kilometriche
introduzioni di istruzioni #lfdef). Pertanto in Java non esiste un meccanismo di sostituzione, a
"tempo di compilazione", tra etichette e corrispondenti valori. Come alternativa è possibile definire
delle variabili condivise il cui valore non può essere modificato (final) che, pertanto, conviene
dichiarare statiche (Static).

2.2.1 Non inserire valori hard-coded nel codice


Una direttiva base della programmazione, indipendentemente dal linguaggio considerato, sta-
bilisce di non includere valori invariabili direttamente nelle istruzioni del programma. Tale
pratica dannosa, indicata tipicamente con i termini di hard-coding, è in grado di generare una
serie di problemi, ad esempio l'aumento della resilienza alle modifiche, una minore consisten-
za, e così via.
Pertanto, ogniqualvolta si abbia la necessità di inserire un valore non variabile direttamente
nel codice, è necessario valutare se si tratti di un valore che:

1. non cambierà pressoché mai. In questo caso è sufficiente utilizzare una "costante"
Java. Per esempio:

public static final String N E W _ U N E = System.getPropertyfline.separator");

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().

2.2.2 Valutare il caso di inserire valori costanti


in un'opportuna interfaccia Java
Una buona pratica di programmazione consiste nell'includere dentro un'opportuna interfaccia
Java le costanti largamente utilizzate in un framework, package o sistema.
Per esempio, si analizzi il caso del listato seguente.

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.3 Non eseguire l'hard-coding dei nomi di file


Questa regola rappresenta una ripetizione di quanto esposto nei precedenti punti; ciò nono-
stante, si è deciso di enfatizzarla nuovamente per via dell'elevata frequenza con cui viene disattesa.
Eseguire l'inglobamento dei percorsi direttamente nel codice può porre seri problemi sia rela-
tivi all'istallazione dei sistemi e al riutilizzo del codice, sia alla portabilità del codice. Un caso
frequente è relativo all'inizializzazione di stringhe contenenti la concatenazione del percorso
del file, incluso il relativo nome. Per esempio: private String configFile = "C:\mysys\config\config.xml".
Ciò crea non solo i tipici problemi dovuti aü'hard-codtMg, ma anche di portabilità. Per esem-
pio, nel sistema operativo Unix i percorsi assoluti iniziano con il carattere "/", mentre in Windows,
questi iniziano con una lettera identificante l'unità dati.
Per sottrarsi a questi problemi è necessario:

1. evitare Yhard-coding dei percorsi ricorrendo alle tecniche illustrate precedentemente;


2. costruire i percorsi utilizzando:
a. la classe File(File, String) per costruire il percorso del fine;
b. le proprietà di sistema per costruire i percorsi. In particolare:
System.getPropertyffile.separator") e System.getPropertyfpath.separator")

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:

System.out.print(i+") \n user=currentUser.getLogin() \n attempts="+failedAttempts);

verrebbe mostrata correttamente solo in ambiente Windows

1)
user=vettitagl
attempts=2

Mentre in ambiente Unix produrrebbe il seguente output:

1) \n user= vettitagl \n attempts=2

Per evitare problemi di questo tipo è sufficiente utilizzare come sequenza di nuova riga, il
risultato della seguente istruzione

System.getPropertyfline.separator").

2.3 Concludere correttamente i programmi Java


Quando si scrivono applicazioni Java è necessario tenere a mente le regole di terminazione. In
particolare, un'applicazione Java termina quando terminano tutti i thread non daemon (detti
anche user thread) attivi nella medesima J V M . I tbread creati dal programmatore, tipicamente,
sono non daemon. Qualora l'esecuzione dell'applicazione preveda un solo thread, demandato
all'esecuzione del metodo il main, l'applicazione termina alla terminazione di tale thread.
2.3.1 Non terminare l'esecuzione del programma
con l'istruzione System.exit()
L'esecuzione dell'istruzione System.exit() forza l'immediata terminazione di tutti i thread in ese-
cuzione nella sottostante J V M e quindi termina anche quest'ultima. Pertanto l'utilizzo di que-
sta istruzione dovrebbe essere evitato.
Uno dei problemi centrali è che l'invocazione di System.exit() non permette ai vari thread di
terminare in maniera pulita (gracefully) e non dà l'opportunità di salvare eventuali dati tempo-
ranei, contesti, etc. Tale istruzione, inoltre, limita il riutilizzo del codice e pone seri vincoli
all'integrazione del codice in opportuni sottosistemi. Si consideri per esempio il caso in cui un
codice sia integrato in un sistema più complesso, oppure in un framework di integrazione. In
questo caso l'esecuzione dell'istruzione System.exit() provocherebbe la chiusura di tutte le ap-
plicazioni in esecuzione sulla medesima JVM. Situazione non sempre auspicabile.
Le direttive della Sun stabiliscono "Si tratta di un comportamento troppo drastico. Potreb-
be, per esempio, distruggere tutte le finestre create daH'in terpreter senza dare all'utente la pos-
sibilità di registrarne o addirittura di leggerne il contenuto [...] I programmi dovrebbero ter-
minare, di solito, arrestando tutti i thread di tipo non daemon; nel caso più semplice di un
programma da linea di comando, si tratta semplicemente di un return dal metodo main. System.exit
dovrebbe essere riservato solo all'uscita da un errore catastrofico, o ai casi in cui un prgramma
sia pensato per l'uso come utilità in uno script che possa avere dipendenza da sul codice di
uscita dal programma".
Un'applicazione dovrebbe terminare sempre in modo controllato (gracefully), dando il ne-
cessario tempo ai vari thread di rilasciare le eventuali risorse bloccate, di registrare la persistenza
per eventuali dati temporanei, etc.

2.3.2 Non utilizzare le istruzioni System.runFinalizerOnExit


e Runtime.runFinalizerOnExit
Molti sviluppatori probabilmente, e per loro fortuna, non avranno mai sentito parlare di questi
metodi (se questo è il caso, benissimo: passare alla regola successiva). In effetti sono stati de-
precati fin dalla versione 1.2 per motivi decisamente validi. In effetti sono intrinsecamente
insicuri (unsafe). Ciò perché possono facilmente generare scenari di dead-lock o comunque di
comportamento randomico, dovuti all'esecuzione del dei finalizer su oggetti ancora validi men-
tre altri thread potrebbero trovarsi ad utilizzarli o potrebbero richiederne la manipolazione.

2.3.3 Evitare uscite brusche dal costrutto finally


Ogni qualvolta un costrutto o un blocco di codice lancia un'eccezione o esegue una delle se-
guenti parole chiavi return, break e continue, questo termina bruscamente (abruptly in inglese):
interrompe il proprio flusso di esecuzione senza eseguire la restante parte del codice.
Interrompere bruscamente un costrutto finally non è mai una buona prassi. In primo luogo,
programmi che fanno uso di uscite brusche tendono a essere confusi e quindi noti fà®ìj£pjl
comprendere. Inoltre, poiché istruzioni presenti nella parte finally sono eseguite quasi nella
totalità dei casi (unica eccezione è la presenta dell'uscita dal programma: System.exit), indipen-
dentemente da quello che accade nel costrutto try, eventuali uscite brusche nel costrutto try
verrebbero perse e sostituite da quella presente nel finally. Ciò raramente è un comportamento
desiderato. Un esempio di un pessimo codice è mostrato nel listato successivo. In questo caso,
sebbene il costrutto try tenti di ritornare un valore false, l'uscita brusca presente nel blocco
finally, l'istruzione return, forza sempre a ritornare un valore true.
try I

// some code

return talse;

I tinally {
return true;
)

2.3.4 Valutare il ricorso allo shutdown hook


Lo shutdown hook (gancio di chiusura) è un meccanismo, introdotto con la versione Java 1.3,
che permette di intercettare e quindi gestire in qualche modo lo shutdown della macchina
virtuale Java. Ciò è possibile grazie al metodo addShutdownHook() della classe java.lang.Runtime, il
cui solo parametro è un oggetto di tipo Thread, che, come lecito attendersi, viene invocato du-
rante la terminazione della JVM. L'unico modo per evitare che questo modo sia chiamato con-
siste nell'eseguire il metodo Runtime.halt(). Questo meccanismo è molto utile per intercettare e
gestire chiusure brusche di JVM, inclusi application server, etc. Si tratta di un meccanismo
fondamentale in tutti quei casi in cui l'applicazione gestisca delle risorse da rilasciare corretta-
mente, come per esempio oggetti presenti solo in memoria da persistere sul database (meccani-
smi di write-behind), presenza di file temporanei, etc.

;V add the shutdown thread


Runtime.0efflunf/'me().addShutdownHook(new Thread() {

7 if the shutdown is in progress


public void run() |
// clean resources ...
I
I);

Chiaramente questo metodo poco potrebbe in caso di chiusura dell'applicazione dovuta a


mancanza di alimentazione elettrica: ma questo scenario in ambienti professionali è di fatto
impossibile.

2.4 Scrivere correttamente i metodi


Gli oggetti sono entità in grado di eseguire un insieme ben definito di attività che ne rappresen-
tano il comportamento. Queste attività, in termini implementativi, sono rappresentate dai metodi.
Poiché le leggi dell'OO, specie l'incapsulamento, prescrivono che le classi debbano celare al
mondo esterno la propria logica interna e in particolare le proprietà strutturali (attributi e
relazioni), ne segue che i metodi assumono un ruolo fondamentale: rappresentano l'interfaccia
propria delle classi, ossia gli elementi di un oggetto accessibile da parte di altri, il contratto tra
la classe che fornisce i servizi e le classi che li utilizzano. Pertanto, ogni oggetto possiede una
propria interfaccia (per così dire intrinseca) costituita da un insieme di comandi (i metodi),
ognuno dei quali esegue un'azione specifica. Un oggetto può richiedere a un altro di eseguire
una determinata azione inviandogli un "messaggio".

2.4.1 Assegnare ai parametri il tipo più generico possibile


Nel definire la firma dei metodi è consigliabile assegnare ai parametri formali un tipo che sia il
più generico possibile. Questa tecnica permette di creare un livello di astrazione tra metodi
invocanti e quelli invocati, schermandoli da eventuali variazioni di implementazione dei para-
metri attuali. In questo modo qualora vari il tipo di un oggetto passato come parametro, le
ripercussioni di tale variazione resteranno limitate grazie all'esistenza di metodi che si riferisco-
no al parametro per mezzo di una conveniente astrazione (tipicamente un'interfaccia). Chiara-
mente, il tipo deve essere il più generico tra quelli possibili. Come spiegato nella regola succes-
siva, è una pratica sconsigliata definire un tipo generico per poi doverne effettuare il down-cast
nell'implementazione del metodo per via della necessità di utilizzare metodi specifici di un tipo
discendente. Per esempio, è da evitare il caso in cui un oggetto java.util.Stack sia passato come
parametro di tipo java.util.List, e poi aver necessità di utilizzare il metodo pop, e quindi dover
eseguire un down-cast (Stack givenStack = (Stack) aList; ), per prelevare l'ultimo elemento inserito
e quindi il primo a dover essere reperito. Utilizzare un tipo generico è un principio è necessario
soprattutto nella realizzazione di metodi non privati.
Qualora un metodo manipoli una collezione, l'applicazione di questo principio equivale ad
asserire di riferirsi a tale oggetto per mezzo della relative interfaccia: List, Set e Map. Da notare
che liste e insiemi dispongono di un'interfaccia di livello di astrazione ancora superiore: Collection.
Anche il ricorso ad array (si ricordi che in Java gli array sono oggetti) può risultare utile soprat-
tutto per classi di utilizzo generico.
Per esempio, dovendo implementare un metodo della classe Portfolio atto ad aggiungere i
relativi strumenti finanziari, potrebbe aver senso implementare una versione che preveda come
parametro di input un array di strumenti ed eventualmente eseguirne X overloading utilizzando
l'intefaccia List, piuttosto di implementare una versione che preveda un ArrayList.
La firma dei metodi potrebbe assumere le seguenti forme:

public void addlnstruments(lnstrument[] instruments)

public void addlnstruments(List instruments)

2.4.2 Nell'assegnare il tipo a un parametro,


evitare di dover eseguire il down-casting
Si effettua un'operazione down-cast ogni qualvolta un oggetto trattato come un tipo antenato è
forzato a un tipo discendente. Pertanto si passa da un tipo più generale a uno più specializzato.
Giacché solo a tempo di esecuzione è possibile esaminare il tipo dell'oggetto fornito come
parametro al metodo, ne segue che il controllo di tipo non può essere effettuato a tempo di
compilazione ma solo in esecuzione, quindi il codice è meno type-safe. Ciò implica la possibili-
tà di generare fastidiosi errori a runtime. Si consideri per esempio l'improbabile implementazione
di un metodo atto a memorizzare un oggetto properties in uno stream di output.

public synchronized BooleanstoreProperties (OutputStream out, Dictionary properties)


throws lOException I
BufferedWriter awriter =
new BufferedWriter(
new OutputStreamWriter(out, "8859_1")
);

il (properties != null) I

if (properties linstanceof Properties) I


throw new HlegalParameterExceptionf'par. Properties not valid");
)

prop = (Properties) properties;

for (Enumeration e = prop.keys(); e.hasMoreElements();) I


String key = (String) e.nextElement();
String val = (String) get(key);
key = saveConvert(key, true);

val = saveConvert(val, false);


writeln(awriter, key + "=" + val);
I
)
awriter.close();
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.

public boolean equals(Object o) (


if (o == this)
return trae;

if (!(o instanceof List))


return false;

Listlterator<E> e1 = listlterator();
Listlterator e2 = ((List) o).listlterator();

while (e1 ,hasNext() && e2.hasNext()) I


Eoi = e1.next();
Object o2 = e2.next();
if ( !(o1 ==null ? o2==null : o1.equals(o2)) )
return false;
)
return !(e1.hasNext() || e2.hasNext());
)

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.

2.4.3 Valutare l'opportunità di restituire un valore nuli per collezioni


Dovendo implementare un metodo che restituisca una collezione (array, Iterator, List, etc.), nel
caso in cui tale collezione sia vuota è spesso conveniente non restituire un valore nuli, ma un
oggetto vuoto. Questa tecnica permette di rendere più agevole il codice del metodo chiamante,
eliminando la necessità di effettuare la verifica esplicita per determinare l'eventuale presenza di
un valore nuli.
Le implementazioni delle collezioni Java, per esempio, nel caso in cui le relative istanze siano
vuote, restituiscono comunque un oggetto Iterator.
Nel codice, mostrato al punto precedente, relativo al metodo equals, si può notare che una
volta restituiti gli oggetti Listlterator, non è stato necessario eseguire il test per verificare se siano
o meno nulli; si può passare direttamente al ciclo while, e quindi il codice risulta più lineare.

2.4.4 Fare attenzione al passaggio delle collezioni


Dovendo implementare metodi generici che eseguono delle operazioni su oggetti di tipo colle-
zione, spesso viene la tentazione di inserire del codice in grado di inizializzare una collezione
qualora la relativa variabile sia nulla. Sebbene, in linea di principio ciò potrebbe avere senso, in
pratica non è possibile. Ciò per via del meccanismo Java di passaggio dei parametri per valore.
Nel caso delle collezioni, viene passata una copia dell'indirizzo (il riferimento originale è copia-
to in apposito e disgiunto elemento nello stack); pertanto, qualora questo sia variato, tale varia-
zione andrebbe persa: il nuovo valore non sarebbe copiato nella variabile iniziale.
Si consideri il seguente frammento di codice.

public class Test I

public statlc boolean validateUserData(User aUser, List<String> errors) I

boolean vaIResult = true;


if ( (allser.getNameO == null) || (aUser.getName().trim().length() == 0) ) f
errors = addError{"Hame not set", errors);

// all other validations: middlename. surname, date ot birth, etc

System.ouf.printlnf error size:"+errors.size());

return valResult;

public static List<String> addError(String error, List<String> errors) I

if (errors == null) {
errors = new ArrayList<String>();

errors, add(error);

return errors;
I

public static void main(String[] args) I

List<String> errors = null;


User userl = new User();

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!");

Il frammento di codice ha lo scopo di validare la corretta impostazione di alcuni elementi e di


memorizzare gli errori trovati (nel caso in questione un oggetto di tipo User). Un frammento del
genere (ma corretto) potrebbe risultare utile qualora sia necessario validare una serie di oggetti.
Si consideri per esempio il caso in cui questi oggetti siano impostati attraverso un'interfaccia
utente di un servizio internet: invece di comunicare all'utente un problema alla volta, evidente-
mente è più opportuno comunicare l'intera lista degli errori. Ciò servirebbe ad evitare all'uten-
te frustrazione e perdita di tempo dovuti a continue comunicazione avanti ed indietro con il
server. Al fine di ottenere ciè è necessario memorizzare i vari errori (sicuramente in una struttu-
ra un po' più complessa della semplice stringa) e proseguire con l'analisi del contenuto invece
di fermarsi al primo errore.
Eseguendo il programma, tuttavia, si assiste a un comportamento apparentemente "strano".
Infatti, da una parte viene stampato il messaggio: errar size:1 e dall'altro vi è una nuli point
exception. Quindi, da un lato la lista degli errori ha una stringa, dall'altra però non vi è alcun
elemento! Il problema è che, quando si crea la lista all'interno di un metodo, come aggiornan-
do un parametro, non si fa altro che variare il riferimento di una copia della variabile originale
che quindi viene persa al ritorno dall'invocazione.

2.4.5 Cercare di implementare metodi con un solo punto di ingresso


e un solo punto di uscita
Un valido principio di programmazione strutturata, incluso nella programmazione OO, affer-
ma che ogni metodo dovrebbe essere dotato di un solo punto di entrata e un solo punto di
uscita. Si tratta di una buona regola che facilita la comprensione e la manutenibilità del codice.
In effetti, tipicamente, non è sempre agevole aggiornare un metodo dotato di vari punti di
uscita (return), diversi da quelli semplici posti all'inizio del metodo come controllo dei parame-
tri, soprattutto quando la sezione da modificare è quella inclusa tra diversi punti di uscita.
Una legittima eccezione a questa regola è l'implementazione della serie iniziale di test dei
parametri di un metodo. In questo caso, eventuali ritorni anticipati sono permessi e anzi tendo-
no a semplificare l'implementazione e la lettura del metodo.

2.4.6 Considerare l'utilizzo di una variabile result per i metodi


che restituiscono un valore
Molti tecnici, specialmente quelli provenienti dal linguaggio C, trovano molto leggibile l'utilizzo
del nome convenzionale result per l'eventuale variabile che memorizza il risultato di ritorno dei
metodi. Ecco un frammento di codice atto a verificare l'uguaglianza di due array di oggetti.

public static boolean equals(Object[] o1, Object[] o2) I


boolean result = true;

,7 tlie same obiect or bolli nuli?


it (o1==o2) Il ( (o1==null) && (o2==null) )
return true;

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;

2.4.7 Non utilizzare metodi deprecati


I metodi deprecati sono metodi ancora presenti nella versione del JDK che si sta utilizzando,
ma dei quali è stata pianificata l'eliminazione nelle versioni future. Pertanto è probabile che il
loro utilizzo crei seri problemi di portabilità verso nuove versioni del JDK.
2.4.8 Evitare ineleganti e inefficienti sequenze di instanceof
istanceof è un operatore Java utilizzato per verificare se un determinato oggetto sia o meno del
tipo atteso a tempo di esecuzione. La sintassi è molto semplice: if (objectReference instanceof type).
Sebbene il suo utilizzo sia strettamente necessario in una serie di situazioni, come per esempio
nell'implementazione di metodi generici come equals, in generale deve essere utilizzato con
parsimonia e possibilmente evitato.
Scott Mayers, nel suo libro Effective C++, si esprimeva, in maniera fin troppo severa, come
segue: "Ogni volta che ti trovi a scrivere del codice nella forma 'se questo oggetto è di tipo Ti,
allora fai qualcosa, se il suo tipo è T2 allora fai qualcos'altro', prenditi a schiaffi". Sebbene
Mayers sia un po' duro, in effetti molto spesso ciò può essere tranquillamente evitato ricorren-
do alle leggi del polimorfismo e dell'overriding.
In ogni modo, qualora sia assolutamente necessario eseguire un down cast (da un tipo più
generale a uno più specifico, per esempio da List ad ArrayList) è sempre opportuno schermarlo
con un test di tipo utilizzando appunto istanceof... Sempre che poi si sappia come gestire il caso
in cui questo test restituisca un valore falso.

2.5 Implementare attentamente i metodi "accessori"


e "modificatori" (get/set)
I metodi accessori (getXXX(), ÌsXXX(), hasXXX()) sono funzioni implementate per accedere al valore
degli attributi di un oggetto, mentre i metodi modificatori (SetXXX()) sono implementati per
modificare il valore di questi attributi e quindi variano lo stato del relativo oggetto.
L'utilizzo dei metodi accessori e modificatori è stato, soprattutto in passato, oggetto di appassionati
dibattiti. In particolare, vi sono tecnici contrari al loro utilizzo motivato dal fatto che renderebbe il
codice meno efficiente e che la loro codifica non sia un brillante investimento del tempo a
disposizione. Si tratta di argomentazioni la cui validità è abbastanza opinabile. In effetti, si constata
che per asserire che questi metodi generano colli di bottiglia bisognerebbe riuscire a produrre
codice estremamente efficiente, che non acceda a risorse I/O, in cui ogni dettaglio presenti un
elevato grado di ottimizzazione, etc. Cosa, ovviamente, raramente possibile. Per quanto riguarda
un eventuale migliore l'utilizzo del tempo a disposizione, è sufficiente notare che gli I D E moderni
sono in grado di generare automaticamente questi metodi corredati da opportuni commenti JavaDoc.
In questo testo, questi metodi sono consigliati perché aumentano la leggibilità del codice,
concorrono a renderlo più robusto, ne semplificano la manutenibilità, e cosi via.

2.5.1 Insistere sull'utilizzo di metodi accessori/modificatori


Come riportato sopra, l'utilizzo dei metodi accessori e modificatori aumenta la leggibilità del
codice, concorre a renderlo più robusto (ogni attributo ha pochi e ben definiti punti di acces-
so) e ne semplifica la manutenibilità. Pertanto, è sempre opportuno incapsulare gli attributi di
una classe assegnando loro visibilità privata e implementando opportuni metodi di accesso e
modifica.

2.5.2 Utilizzare i metodi accessori per campi soggetti a validazione


L'utilizzo dei metodi modificatori è necessario in tutti quei casi in cui l'insieme dei valori che
un campo può assumere è vincolato. Il ricorso a tali metodi serve per evitare di dover ripetere
la stessa sequenza di validazione in diverse parti del codice e per semplificare la manutenzione
del codice, qualora si renda necessario dover variare la procedura di validazione. Ecco un
esempio del metodo setGender di una classe Person.

public void setGender(char aGender) |


il (aGender == nuli)
thorw new IHegalArgumentExceptlonfGender = nuli");

aGender = Character.toUpperCase(aGender);
If (aGender != 'M') && (aGender != 'F') (
thorw new NlegalArgumentExceptionfGender = "'+aGender+ );
)

gener = aGender;
)

2.5.3 Utilizzare i metodi modificatori per campi la cui variazione


può influenzare il valore di altri
L'utilizzo dei metodi accessori è assolutamente necessario qualora l'aggiornamento del valore
di un campo influenzi il valore di altri campi o richieda l'esecuzione di determinate azioni. In
questo caso il ricorso a metodi accessori permette di realizzare un codice più chiaro, più robu-
sto e permette di circoscrivere eventuali effetti "ondulatori" sul valore dei campi. Nel listato
seguente c'è l'implementazione del metodo setValue del componente swing JProgressiveBar. Questo
metodo permette di impostare il valore attuale della barra di progressione e tipicamente richie-
de il ridisegno del componente.

public void setValue(int n) {


// updates the underlying model
BoundedRangeModel brm = getModelf);
Int oldValue = brm.getValue();
brm.setValue(n);

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>

" @param n e w L l m i t The new l l m l l value: m u s t be non-negative


and no larger than this buffer's capacity
* ©return This buller
" ©throws NlegalArgumentException If the p r e c o n d i t i o n s on
< t t > n e w L l m i t < / t t > do not hold
* /

public final Buffer limit(int newLlmit) I


if ((newLimit > capacity) || (newLimit < 0))
throw n e w NlegalArgumentException();

limit = newLimit;
if (position > limit)
position = limit;
if (mark > limit)
mark = -1;
return this;

2.5.4 Implementare metodi accessori/modificatori multipli per collezioni


Il ricorso all'utilizzo dei metodi accessori/modificatori, come illustrato precedentemente, pre-
senta una serie di importanti vantaggi, come aumento del livello di incapsulamento, maggiore
leggibilità del codice, semplificazione della manutenibilità, e così via.
Tuttavia, nel caso di collezioni, quali array, array dinamici, vettori, tavole hash etc., il ricorso
a questi metodi richiede l'implementazione di un insieme più articolato di metodi accessori/
modificatori.
Da tener presente che metodi di questo tipo sono inclusi in classi diverse da quelle degli
elementi che rappresentano. Per esempio il metodo di addOrderLine, atto ad inserire istanze di
tipo OrderLine nella classe Order, è, ovviamente, un metodo di quest'ultima. Pertanto nel nome
dei metodi get/set è necessario ripetere il soggetto della collezione.
Come esempio, si consideri il caso del pattern Observer. In particolare, si consideri un ogget-
to "osservato" che accetti una lista di oggetti di tipo Listener a cui inviare le segnalazioni di
cambiamento del proprio stato.
Vediamo ora una serie di pattern per l'implementazione dei metodi accessori/modificatori di
collezioni: al nome del metodo seguono alcuni esempi e una descrizione dello stesso.

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()

Compito di questo metodo è rimuovere tutti gli elementi della Collection.

2.6 Utilizzare con oculatezza la classe


java.lang.Runtime
Ogni applicazione Java è fornita di una sola istanza di questa classe i cui metodi le permettono di
accedere all'ambiente di esecuzione dell'applicazione. Per quanto questi metodi possano fornire
una serie di utili servizi, il relativo utilizzo pone seri problemi di portabilità.

2.6.1 Valutare attentamente l'utilizzo dell'istruzione Runtime.exec


L'istruzione Runtime.exec permette di richiedere al sistema operativo di eseguire in un apposito
processo il commando specificato nell'argomento. Sebbene si tratti di un'istruzione indubbia-
mente potente, il suo utilizzo può porre una serie di problemi di portabilità. Per esempio, si
consideri il caso in cui la si utilizzi per eseguire un'applicazione specifica Windows, come per
esempio Cale, non disponibile in altri ambienti. Tale utilizzo, chiaramente, limita la portabilità
dell'applicazione in ambienti come Unix. Le direttive Sun raccomandano l'utilizzo di Runtime.exec
solo qualora i seguenti criteri siano soddisfatti:

• l'invocazione deve essere un risultato diretto di un'azione specifica dell'utente e pertan-


to deve essere a conoscenza del fatto che si sta eseguendo un programma diverso, come
per esempio un browser Internet;
• l'utente deve essere messo in grado di selezionare, a tempo di esecuzione o direttamente
nel processo di invocazione dell'istruzione, il programma da eseguire;
• eventuali problemi nell'esecuzione dell'istruzione devono essere gestiti chiaramente e
limpidamente, specialmente se dovuti all'assenza dell'applicazione di cui si è tentato di
invocare l'esecuzione;
• l'utilizzo dell'istruzione è indipendente dalla piattaforma e perciò l'applicazione invocata
è disponibile per le diverse piattaforme, come l'invocazione del compilatore Java: javac.

2.7 Implementare i metodi Object


La classe java.lang.Object, antenata di tutte le classi Java, definisce una serie di metodi di servizio
(per esempio toString, equals, hasheode, clone, etc.) che, per quanto sia noioso, in molti contesti è
necessario implementare al fine di assicurare il corretto funzionamento delle API Java. Per esem-
pio, il metodo equals è utilizzato dalle collezioni Java per verificare l'uguaglianza di due oggetti,
per rimuovere oggetti dalla collezione, etc. Pertanto, qualora si vogliano utilizzare propriamente
le collezioni Java è necessario ridefinire il comportamento definito da questo metodo.
2.7.1 Implementare il metodo toString
Ogni classe dovrebbe implementare la propria versione del metodo toString (effettuarne
l'overriding). Questo è definito nella classe java.lang.Object e quindi è ereditato da tutte le classi
Java. Il suo compito è di riprodurre in una stringa lo stato dell'oggetto. Questa stringa dovreb-
be essere sintetica ma descrittiva.
L'implementazione di base del metodo toString è riportata nel frammento di codice del listato
seguente.

public String toString() I


return getClass().getName() + + Integer.toHexString(hashCodeO);
I

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.

2.7.2 Implementare il metodo equals pet le classi "dati"


Analogamente al metodo toString, anche la versione embrionale del metodo equals è definita
nella classe java.lang.Object. Il suo compito è quello di verificare se l'oggetto in questione è, o
meno, uguale a quello fornito.

public boolean equals(Object obj) I


return (this == obj);

L'implementazione del metodo deve soddisfare le seguenti proprietà:

• riflessiva: x.equals(x) = true;


• simmetrica: x.equals(y) = true <=> y.equals(x) = true;
• transitiva: x.equals(y) = true and y.equals(z) = true => x.equals(z) = true;
• di consistenza: se X.equals(y) = true allora questo deve essere vero per un numero qualsivoglia
di invocazioni di tale metodo (a condizione che le due istanze non siano soggette a modifiche).

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.

public static boolean equals(long[] a, long[] a2) {


if (a == a2)
return true;

if ( a == nuli || a2 == nuli)
return false;

int length = a.length;


if (a2.length != length)
return false;

for (int i=0; klength; i++)


if (a[i) != a2[i])
return false;

return true;

2.7.3 Implementare il metodo hashCode per le classi "dati"


La piena comprensione del metodo hashCode e della sua implementazione richiede la conoscenza
di alcune nozioni base della teoria dell'hashing. Pertanto, si consiglia di leggere l'Appendice (. ad
esso dedicata al fine di approfondire quanto riportato di seguito.

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.

public V get(Object key) I

Object k = maskNull(key);
ini hash = hash(k);

int i = indexForfhash, table.length);


Entry<K,V> e = table[i];

while (true) I
il (e == null)
return null;
il (e.hash == hash & & eq(k, e.key))
return e.value;
e = e.next;
I

static int indexFor(int h, int length) I


return h & (length-1);
]

static int hash(Object x) I


int h = x.hashCode();

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.

Le proprietà dell'implementazione del metodo hashCode sono:

• 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!

2.7.4 Implementare il metodo clone


Il metodo clone è utilizzato per restituire una copia (un clone appunto) dell'oggetto in cui è
definito. L'implementazione di questo metodo è più complessa di quanto possa sembrare. In-
fatti, se l'oggetto ha attributi che sono oggetti a loro volta, anche questi dovrebbero essere
clonati (in questo caso si parla di "copia profonda", deep copy). Questa regola, ovviamente,
non è valida per gli oggetti collezione, di cui si accetta la generazione di una copia dell'oggetto
collezione che però si riferisca alle medesime istanze degli oggetti memorizzati nella collezione
di partenza i quali, quindi, non vengono copiati (in questo caso si parla di "copia superficiale",
shallow copy). Ecco l'implementazione del metodo clone nella classe java.util.ArrayList.

public Object clone() (


try<
ArrayList<E> v = (ArrayList<E>) super.clone();

v.elementData = (E[])new Object[size];


System.arraycopy(elementData, 0, v.elementData, 0, size);

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

2.8 Porre attenzione alla chiusura degli stream


La libreria Java per l'Input e Output (I/O, java.io) è basata sul concetto di flussi {¡(reami. Si tratta
di un'astrazione utilissima che permette di acquisire e di scrìvere informazioni indipendentemente
dalla sorgente dati, che può essere un file, una connessione remota, la console, e così via. Inoltre,
gli stream possono essere tutilizzati congiuntamente a servizi aggiuntivi, come la compressione, la
crittografazione, la traslazione, e così via.

2.8.1 Chiudere sempre gli stream


Gli stream rappresentano risorse la cui corretta cessazione richiede l'esplicita chiusura invo-
cando il corrispondente metodo dose. Nelle classi di output l'esecuzione di questa funzione
include anche l'esecuzione del metodo di flush.
Se un determinato codice usa diversi oggetti stream (p.e.: uno di input, uno di output e uno per
la notifica di particolari scenari di errore), è necessario che la sezione dedicata alla chiusura gesti-
sca la cessazione dei tre gestendo opportunamente possibili eccezioni. I listati seguenti mostrano
due versioni di codice per la corretta chiusura degli stream. La prima è la versione classica mentre
la seconda è possibile dalla versione Java 5.0 grazie all'introduzione dell'interfaccia Closeable, che
definisce un solo metodo: close, la cui semantica prevede la chiusura dello stream e il rilascio di
tutte le risorse ad esso associate; se lo stream è già chiuso, l'invocazione del metodo non sortisce
alcun effetto. Ecco un esempio di chiusura di stream classica con codice delle versioni precedenti.

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) ;

' close the given s t r e a m


' If the given s t r e a m is already closed than it does not do a n y t h i n g
' © p a r a m s t r e a m s t r e a m to close
' * /

private static void closeAndLogException(Closeable stream) I


if (stream 1= null) I
try!
stream.close();
) calch (lOException ioe) I
LOGGER.warning(ioe);
I
I
I

2.8.2 Non fare affidamento sul metodo finalize


Ogni classe Java eredita il metodo finalize dalla classe antenata di tutte: java.lang.ObjeCt. Tale
metodo è usato a volte erroneamente per rilasciare le risorse utilizzate dall'oggetto. Secondo le
specifiche Sun, il metodo finalize di un oggetto è invocato dal garbagc collcctor nel momento in
cui libera la memoria occupata. Pertanto non vi è alcuna garanzia di quando e in che ordine il
metodo finalize sarà invocato. Inoltre, questo potrebbe non essere mai invocato fino alla chiusura
dell'intera applicazione, evitando quindi che altri oggetti possano accedere alle risorse bloccate
da un oggetto dereferenziato. Infine, affidarsi all'esecuzione del metodo finalize potrebbe creare
problemi di portabilità. In effetti, la politica utilizzata dal garbagc collcctor dipende dal particolare
algoritmo adottato per la relativa implementazione, che quindi dipendende, in ultima analisi,
dalla specifica implementazione J V M in cui l'applicazione è in esecuzione.

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.

2.8.3 Valutare l'utilizzo di oggetti Buffer


Per ogni tipologia di flusso (stream), le API Java offrono la possibilità di utilizzare una versione
dotata di buffer. Alcuni esempi di questa tipologia di classi sono: BufferedlnputStream,
BufferedOutputStream, BufferedReader, BufferedWriter e StringBufferlnputStream. P e r t a n t o , ogniqualvolta
si abbia la necessità di lavorare con gli stream, soprattutto per trasferimenti di dati con file, reti
etc., è consigliabile ricorrere all'utilizzo delle versioni dotate di buffer al fine di migliorare le
performance.
Da notare che con la versione JDK 1.4, Java è stato dotato di un nuovo package di I/O denomi-
nato NIO (New Input Output). In particolare, questo package fornisce una serie di classi per la
gestione delle operazione di I/O basate sul concetto di buffer modellato dalla classe astratta
java.nio.Buffer e specializzato in diverse forme. In base alle specifiche Sun, si tratta di
un'implementazione più efficiente, più scalabile e in grado di gestire l'intero set di caratteri. Per-
tanto, qualora sia necessario utilizzare oggetti buffer, è consigliato valutare il ricorso alla API NIO.

2.9 Tipi numerici


Tipi numerici: tutti i programmi ne fanno uso e tutti sembrano avere le idee piuttosto chiare...
Eppure, eppure analizzando applicazioni in campo finanziario, è possibile individuare una
serie di problemi ricorrenti legati al trattamento di informazioni numeriche. Il problema di
fondo, spesso ignorato, è relativo al fatto che la rappresentazione in virgola mobile è intrinseca-
mente imprecisa. Pertanto, qualora sia necessaria una certa precisione e/o quando anche una
piccola imprecisione possa generare grosse inconsistenze passando attraverso una serie di for-
mule (si immagini il valore di un'azione moltiplicata per milioni), allora è necessario ricorrere a
rappresentazioni assolutamente più precise.
Come se non bastasse, il problema dei campi numerici e della precisione è abbastanza sub-
dolo: l'errore potrebbe non verificarsi per lungo tempo fino poi a presentarsi al momento in cui
sia necessario dover spiegare dei risultati numerici inattesi, magari una riconciliazione fallita
con un cliente istituzionale che abbia investito miliardi di dollari. In questo caso, l'esperienza
insegna, che solo dopo aver investito moltissimo tempo in investigazioni, analisi e studi, si
riesce a capire che il problema è proprio intrinseco alla rappresentazione in virgola mobile.
Si tratta di problemi relativi ai numeri reali presenti in quasi la totalità dei linguaggi di pro-
grammazione moderni (più precisamente in tutti i linguaggi che implementano lo standard
IEEE754). Ma vediamo qual è la questione e come si riflette sul linguaggio Java. Java dispone
di due tipi base per il trattamento dei tipi reali: float e doublé (con le relative classi di wrapping:
java.lang.Float e java.lang.Doublé), la cui semantica è sancita dallo standard IEEE 754 - Binary
Floating-Point Arithmetic (Aritmetica binaria in virgola mobile). La tabella 2.1 mostra le diffe-
renze principali tra questi due tipi.
La parte dedicata alla mantissa è tipicamente indicata come precisione: questo per il sempli-
ce motivo che, disponendo di un numero finito di cifre nella mantissa, è possibile rappresenta-
re esattamente solo un numero finito di valori frazionari.
Float Double
Dimensione in byte 4 8
Bit segno 1 1
Bit esponente 8 11
Bit mantissa 32 52
Max numero positivo 3.40282347e+38 1.79769313486231570e+308
Min numero positivo 1.40239846e-45 4.94065645841246544e-324

Tabella 2.1 - Le differenze principali tra il tipi base float e il doublé.

2.9.1 Valutare attentamente la precisione richiesta


Come visto, la rappresentazione in virgola mobile standard dispone di un numero finito di
cifre, organizzate secondo una ben definita struttura (segno, mantissa ed esponente normaliz-
zato): ciò fa sì che non tutti i numeri reali decimali siano rappresentabili esattamente con la
notazione della virgola mobile binaria. Spesso diversi sviluppatori pensano erroneamente che
questa insidia si manifesti solo per numeri particolarmente grandi e/o con molte cifre decimali.
Per comprendere quanto ciò sia errato, basti considerare il listato riportato poco sotto. Dalla
relativa analisi ci si aspetterebbe che l'esecuzione generi in uscita il valore 1.0; in effetti il codice
non fa altro che sommare 10 volte il valore 0.1. Purtroppo non va a finire così: il risultato
restituito è 1.0000001. Si noti che qualora realNum sia dichiarato doublé, la situazione migliore-
rebbe notevolmente, si passerebbe da un errore dell'ordine IO7 ad uno dell'ordine di 10 16;
tuttavia ciò non risolverebbe il problema, e infatti il risultato prodotto sarebbe
0.9999999999999999. Quindi, sebbene il tipo doublé offra un'approssimazione decisamente mi-
gliore, presenta comunque dei margini di imprecisione. Ecco un semplice frammento di codice
atto a mostrare le insidie del tipo float. Infatti, il risultato prodotto è 1.0000001.

float realNum = 0.0F;

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


realNum += 0.1;
)

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.

BigDecimal realNum = new BigDecimalf'O");

for (ini i=1; i <= 10; i++) (


realNum = realNum.add(new BigDecimal("0.1 "));

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++.

2.9.2 Valutare attentamente il ricorso a strategie diverse


Una strategia spesso utilizzata per aggirare il problema della limitata precisione dei tipi float e
doublé consiste nell'utilizzare tipi long, spostando accuratamente la virgola. L'espediente consiste
nel moltiplicare per 10 elevato al numero di posti decimali necessari durante la memorizzazione
del numero per poi dividere per lo stesso fattore durante il reperimento. Così per esempio il
numero 875000.45, moltiplicato per IO2 verrebbe rappresentato come il long 87500045. Per
quanto questo espediente offra diversi vantaggi in alcuni scenari, crea seri problemi fino a giun-
gere, in casi estremi, a porre seri limiti all'espandibilità/correttezza del sistema. Si consideri il
caso in cui sia necessario considerare diverse cifre decimali. In questi casi sarebbe necessario
implementare movimenti di 6, 7 o anche più posti decimali. Ora considerando che il tipo long è
in grado di rappresentare numeri molto grandi (8 byte con segno che permette di rappresentare
numeri da -9,223,372,036,854,775,808 a +9,223,372,036,854,775,807), una precisione di 7 ci-
fre decimali, ridurrebbe la capacità del tipo long al seguente intervallo: da circa -922,337,203,685
a +922,337,203,685. Sebbene ciò a prima vista, potrebbe sembrare ancora conveniente, in alcu-
ni ambienti (come per esempio applicazioni finanziarie) in cui sia necessario manipolare diverse
monete e grandi numeri, potrebbe esserlo molto meno. Basti prendere in considerazione il cam-
bio Euro e Yen (nel momento in cui viene redatto questo paragrafo): 100 EUR = 15,570.60 JPY.
Il problema in questi casi è che l'errore potrebbe emergerebbe dopo alcuni mesi/anni del siste-
ma in esercizio e dopo aver memorizzato molti dati in database, fogli di calcolo, e così via.

2.9.3 Considerare la presenza di numeri speciali in virgola mobile


Lo standard IEEE 754 oltre a stabilire la struttura e semantica dei numeri a virgola mobile, si
occupa di definire delle particolari entità, come per esempio: NaN (Not a Number), -0, +infinity
(infinito positivo) e -infinity (infinito negativo). Questi elementi, la cui rappresentazione è affi-
data a configurazioni riservate, presentano delle peculiarità, alcune delle quali sono illustrate di
seguito. Nella tabella 2.2 sono riportate alcune proprietà di questi speciali numeri in virgola
mobile.
La situazione è complicata dal fatto che alcune regole cambiano a seconda se si utilizzi i tipi
base o i corrispondenti oggetti di wrapping. Per esempio, qualora si utilizzino due float, allora
Espressione Risultato
Math.sqrt(-I.O) NaN
0.0 / 0.0 NaN
1.0 / 0.0 Infinity
-1.0 / 0.0 -Infinity
NaN + 1.0 NaN
Infinity + 1.0 Infinity
Infinity + Infinity Infinity
NaN > 1.0 false
NaN == 1.0 false
NaN < 1.0 false
NaN == NaN false

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

2.9.4 Porre attenzione alle comparazioni con i numeri reali


Dopo avere esaminato il comportamento dell'entità NaN nelle comparazioni, i vari
arrotondamenti e gli errori di precisione, dovrebbe essere ormai chiaro che eseguire la compa-
razione tra numeri reali nasconde diverse insidie, tanto che alcuni autori giungono a suggerire
di evitare completamente le comparazioni tra numeri reali! Questo chiaramente è un suggeri-
mento un po' estremo e non sempre possibile.
Tuttavia, date le limitazioni delle comparazioni dei numeri in virgola mobile, è possibile
ottenere implementazioni robuste solo ricorrendo a opportune strategie. Per esempio, una buona
tecnica per valutare se due numeri reali siano uguali o meno consiste nel verificare che la loro
distanza sia prossima allo zero, ossia che la differenza appartenga a un intervallo molto piccolo.
Un'altra strategia necessaria per eseguire il confronto di numeri reali, dovuta soprattutto per
via del fatto che la configurazione NaN non ha proprietà di ordinamento, consiste nell'assicurar-
si che i valori appartengano all'insieme di validità desiderato, piuttosto che cercare di escludere
valori non validi (il frammento successivo mostra un esempio da evitare e l'equivalente consi-
gliato). Come di consueto, ecco su due colonne, degli esempi di codice atti ad evitare
l'impostazione di valori reali indesiderati.
Codice sconsigliato Codice consigliato

public void setDepth(float aDepth) {


public void setDepth(lloat aDepth) I It ( (aDepth >= 0) && (aDepth <= MAX_VAL) )
if (aDepth < 0) depth = aDepth;
throw new IHegalArgumentException(...); else
depth = aDepth; throw new HlegalArgumentException(...);

2.9.5 Non utilizzare float e doublé con BigDecimal


Come visto nei paragrafi precedenti, le rappresentazioni float e doublé sono intrinsecamente im-
precise. Pertanto qualora sia necessario disporre di un'elevata precisione è necessario ricorrere a
classi quali BigDecimal. Ciò rappresenta una condizione necessaria ma non sufficiente. In effetti,
qualora si ricorra a queste classi è importante evitare ogni passaggio per i tipi base float e doublé,
incluso la restituzione dei valori finali: si correrebbe il rischio di non risolvere il problema. Per
esempio utilizzando i tipi float e doublé nei costruttori degli oggetti BigDecimal (BigDecimal(Biglnteger
vai), BigDecimal(double vai), BigDecimal(String vai), etc) si correrebbe il rischio di introdurre un vizio
di precisione all'inizio della computazione (cfr. listato seguente), mentre utilizzando tali tipi a
risultato generato, si correrebbe il rischio di invalidare gran parte delle cautele adottate. Ecco un
frammento di codice atto ad illustrare la necessità di non passare per i tipi float e doublé. Infatti
l'output di questo frammento di codice è: 0.01 e 0.00999999977648258209228515625.

BigDecimal realNuml = new BigDecimal("0.01");


BigDecimal realNum2 = new BigDecimal(0.01F);

System.ouf.printlnfResult : "+realNum1);
System.ouf.prinllnfResult : "+realNum2);

2.9.6 Evitare l'utilizzo del metodo BigDecimal#equals


Qualora si decida di utilizzare le rappresentazioni BigDecimal, bisogna porre attenzione all'uti-
lizzo del metodo equals. In effetti, poiché questi oggetti utilizzano una rappresentazione simile
a quella delle stringhe, i numeri 10.00 e 10 finiscono per essere interpretati come diversi dal
metodo equals. Il problema è facilmente risolto utilizzando il metodo compareTo.
Quindi, il suggerimento consiste nel ricorrere al metodo compareTo per comparazioni aritme-
tiche. Di seguito, le insidie del metodo BigDecimal equals. Questo frammento restituisce i valori
false e 0.

BigDecimal realNuml = new BigDecimal("10.00");


BigDecimal realNum2 = new BigDecimal("10");

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 è:

BigDecimal total = BigDecimal.ZERO,


total = total.add(base);
total = total.add(subTotal);
total = total.add(taxAmount);

2.9.8 Verificare attentamente la possibilità di overflow


Java rappresenta il tipo long per mezzo di 8 byte permettendo di rappresentare un intervallo
numerico molo ampio (-9,223,372,036,854,775,808 a +9,223,372,036,854,775,807). Tuttavia, spesso
alcuni utilizzi tendono ad accedere questa capacità senza che ciò sia immediatamente chiaro.
Spesso problemi di overflow tendono a generare problemi difficilmente scovabili.
Inoltre, alcune convenzioni Java non sempre aiutano. Si consideri la seguente dichiarazione:

public linai long TM_MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;

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:

public final long TM_MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;

2.9.10 Verificare attentamente le assegnazioni composte


Le assegnazioni composte sono espressioni del tipo a += b. Per molti, espressioni di questo tipo
equivalgono alla seguente versione espansa: a = a + b. Ciò non è del tutto corretto e in effetti la
grammatica relativa prevede il cast automatico al tipo oggetto dell'assegnamento. Pertanto, qua-
lora gli elementi partecipanti siano dello stesso tipo, il cast praticamente non sortisce alcun
effetto, mentre nei casi in cui questi siano di tipo a maggiore precisione, allora si corre il rischio
di generare risultati imprevisti senza che il compilatore emetta alcuna segnalazione (listato se-
guente). Cosa che invece avverrebbe senza assegnazione composta. Questo frammento di codi-
ce invece di stampare il valore 100001 stampa -31071, per via del cast implicito. Da notare che
l'istruzione count =count + ine avrebbe generato un errore di compilazione senza cast esplicito.

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.

2.10.1 Valutare l'utilizzo delle classi ArrayList e HashTable


al posto di Vector e Hashtable quando possibile
Le iniziali classi per la gestione delle collezioni come Vector e Hashtable sono state disegnate e
implementate includendo la gestione del multi-threading.
Pertanto, questa caratteristica (thread-safety) finisce per degradare le performance in tutti i
casi in cui questa caratteristica non sia necessaria. Pertanto, si consiglia sempre di ricorrere
all'utilizzo delle corrispondenti classi introdotte con la versione Java 2 (ArrayList, HashMap e HashSet)
in cui la gestione del multi-threading è opzionale. In questi casi, qualora, sia richiesta la sincro-
nizzazione dei metodi che modificano le collezioni, è possibile ricorrere a due alternative:

1. sincronizzare i metodi delle classi che incapsulano le collezioni;


2. utilizzare le classi standard disegnate per incapsulare le collezioni dotandole di sincro-
nizzazione. Per esempio, per ArrayList è necessario utilizzare il costrutto List list =
Collections.synchronizedList(new ArrayList(...));

2.10.2 Scegliere accuratamente la classe da utilizzare


La tabella 2.3 fornisce una sintesi atta a semplificare la scelta della struttura da utilizzare per
manipolare una collezione di oggetti secondo i propri requisiti.
Da notare che in tutte le strutture hash l'ordine fa riferimento alla sequenza con cui gli
elementi sono accessibili nel corrispondente oggetto Iterator.

2.10.3 Valutare il ricorso alle nuovi collezioni


Nell'implementazione di applicazioni multi-threading è opportuno considerare le collezioni
introdotte con il nuovo package della concorrenza (cfr. Appendice D). Queste sono state appo-
sitamente disegnate per funzionare in ambienti fortemente concorrenti e quindi tendono a
presentare performance decisamente migliori in applicazioni multi-threading.

2.10.4 Valutare l'utilizzo di StringBuffer o StringBuilder


In alcuni contesti in cui è necessario costruire opportune stringhe in diverse fasi dell'esecuzione di
un metodo, invece di utilizzare un oggetto stringa, è consigliabile utilizzare la classe java.util .StringBuffer
Interfaccia D u p l i c a t i Classi JDK
1.0
LinkedHashSet TreeSet
Set No HashSet (ordinata - (ordinata

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)

Tabella 2.3 - Corrispondenza tra le varie strutture dati.

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.

public static String toString(long[] a) I


if (a == nuli)
return "nuli";
it (a.length == 0
return "[]";

StringBuilder buf = new StringBuilder();


buf.append(T);
buf.append(a[0]);

tor (int i = 1; i < a.length; i++) {


buf.appendf, ");
buf.append(a[i]);

buf.appendf]");

return buf.toString();
)

2.11 Lavorare con le date


Esaminando la classa java.util.Date si nota subito che la quasi totalità dei metodi è stata, giusta-
mente, deprecata (di 28 metodi solo 8 sono ancora disponibili e 5 sono la ridefinizione dei
metodi della classe Object) a tal punto che verrebbe quasi da chiedersi se essa abbia ancora
senso. Chiaramente la risposta è affermativa in quanto, come minimo, rappresenta un conteni-
tore, un wrapper di campi data. Quindi ha lo stesso ruolo che classi come Integer, Long, etc.
hanno nei confronti dei corrispondenti tipi base: int, long, etc.
In ogni modo, la manipolazione delle informazioni di tipo data tende a generare non pochi
problemi e pertanto è consigliato seguire le semplici regole riportate di seguito.

2.11.1 Consultare la documentazione...


Questa area delle API Java verosimilmente non è una delle migliori in assoluto, sia per gli
iniziali errori di disegno della classe Date, sia per alcune strane decisioni, come si vedrà di segui-
to. Per risolvere alcuni grattacapi, già dalla versione Java 1.1 sono state introdotte le classi dei
calendari. Queste permettono di risolvere una serie di problemi della classe Date, come la man-
canza di supporto per le diverse rappresentazioni (internationalization), il disegno non basato
su istanze immutabili, etc.
Per questi motivi la manipolazione delle informazioni di tipo data può presentare alcuni
problemi, spesso abbastanza subdoli. Pertanto, il consiglio non può che essere quello di con-
sultare sempre attentamente la documentazione delle API qualora sia necessario manipolare
variabili di tipo data.

2.11.2 Creare correttamente oggetti di tipo Calendar


Come visto, la quasi totalità dei metodi della classe Date sono deprecati, pertanto il trattamento
delle informazioni di tipo data deve passare attraverso l'utilizzo degli oggetti di tipo Calendar. In
particolare, Java fornisce una sola specializzazione di questa classe astratta: la classe
GregorianCalendar. Il nome di questa classe ricorda il calendario in uso in gran parte del mondo
ed è un omaggio al Papa Gregorio Vili che lo introdusse nel 1582 con apposita bolla papale
{Inter Gravissima.s). L'idea moderna che ne è derivata è che ogni istante di tempo possa essere
formulato come un valore espresso in millisecondi che definisce la distanza dal 1 Gennaio 1970
a mezzanotte precisa (00:00:00.000 GMT).
Per quanto sia possibile creare direttamente un'istanza di tipo GregorianCalendar
(GregorianCalendar now = new GregorianCalendar() ), questa non è la procedure più corretta. Le
direttive Java suggeriscono di utilizzare la seguente procedura: Calendar now = Calendar.getlnstance().

2.11.3 Porre attenzione alla rappresentazione dei mesi


L'utilizzo dei mesi in formato numerico della classe Calendar presenta alcune peculiarità a cui è
necessario porre attenzione. Si consideri il seguente frammento di codice, con un esempio di
problemi con il trattamento del campo mese:

Calendar endCentury = Calendar.gef/nsfance();

endCentury.set(1999, 12, 31);

System.ouf.println( "Date ->"+ DateFormat.geiDa/er/me//?sfa/7ce().format(endCentury.getTime()));

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!

Pertanto... come sempre, occorre porre la massima attenzione!

2.11.4 Utilizzare DateFormat e SimpleDateFormat


La classe astratta java.text.DateFormat e la sua unica specializzazione java.text.SimpleDateFormat, for-
niscono una serie di metodi standard per la conversione di date e time incapsulate in oggetti di
tipo Date. Pertanto, invece di implementare proprie utility di conversione è molto più facile e
immediato utilizzare le librerie predefinite. Nella tabella 2.4 sono mostrati alcuni metodi utili.
Qualora, questi formati non siano sufficienti, è possibile definirsene propri customizzati uti-
lizzando la classe java.text.SimpleDateFormat. La tabella 2.5 ne descrive la convenzione.

2.11.5 Attenzione all'utilizzo di SimpleDateFormat


Questo è un altro esempio di quanto sia valida la regola sulla lettura approfondita della docu-
mentazione. Infatti, spesso nell'utilizzare questa classe sfuggono alcuni interessanti informa-
zioni che possono portare alla generazione di errori difficili da individuare. In particolare, il
metodo parse, per default, non genera eccezioni. Inoltre, non è thread-safe.
Si inizi con il considerare il seguente frammento di codice: attenzione al fatto che il metodo
parse di SimpleDateFormat, di default, non lancia eccezioni:

String fmt = "yyyyMMdd";

String testDate = "20080231";

Date dt = nuli;
tryl
dt = (new SimpleDateFormat(fmt)).parse(testDate);
] catch (ParseException e) {

Metodo Tipo di output


DateFormat.getlnstance().format(now) 23/02/08 16:17
DateFormat.getTimelnstance().format(now) 16:17:56
DateFormat.getDateTimelnstance().lormat(now) 23-Feb-200816:17:56
DateFormat.getTimelnslance(DaleFormat.SHORT).format(now) 16:17
DateFormat. getTimelnstance(DateFormat.MEDIUM).format(now) 16:17:56
DateFormat.getTimelnstance(DateFormat.LONG).format(now) 16:17:56 GMT
DateFormat.getDateTimelnslancef
23/02/08 16:17
DateFormat.SHORT, DateFormat.SHORT).format(now)
DaleFormat.getDateTimelnstancef
23-Feb-2008 16:17
DaleFormal.MEDIUM, DateFormat.SHORT).format(now)
DateFormat.getDateTimelnslancef
23 February 2008 16:17:56 GMT
DateFormat.LONG, DateFormat.LONG).lormat(now)

Tabella 2.4. Metodi standard del DateFormat.


Simbolo Significato Tipo Esempio Risultato
G Era Testo "GG" "AD"

"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"

Tabella 2.5. Grammatica del tipo SimpleDateFormat.


System.ouf.println(e);
I

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.

2.11.6 Porre attenzione all'informazione tempo dei campi data


Si consideri il frammento di codice mostrato di seguito. Si tratta di un semplice frammento atto
a impostare una data. Tuttavia, è importante notare che, volenti o nolenti, in questi casi il
campo data memorizza anche il contenuto temporale dell'informazione. Infatti, eseguendo questo
frammento si produce il seguente output: Date ->23-Feb-2008 18:17:08. Nella maggior parte dei
casi ciò dovrebbe essere innocuo, tuttavia vi sono alcuni scenari, come per esempio qualora la
data debba poi essere memorizzata nella base di dati, in cui ciò potrebbe creare dei problemi.
Basti immaginare date inizializzate a cavallo del momento in cui avviene il cambio di orario
dovuto al passaggio da ora legale estiva a ora solare invernale o viceversa. Qualora non si voglia
correre rischi, è opportuno ricorrere al seguente metodo di set: cal.set(2008, 1, 23, 0,0,0).

Calendar cai = Calendar ,getlnstance()\

cal.set(2008,1, 23);

System.ouf.println( "Date ->"+ DateFormat.gefDafer/me//7sfance().format(cal.getTime()));

2.11.7 Utilizzare UTC internamente


Quale formato e fuso utilizzare per i campi data? Si tratta di un interrogativo che pur molto
semplice, risulta particolarmente ricorrente tanto che tutti i programmatori prima o poi si tro-
vano a doverlo affrontare. Tipicamente la prima occasione è fornita dal primo sistema fruibile
a utenti dislocati in diverse località geografiche. Il problema è dovuto al fatto che le necessità
tipiche degli utenti si scontrano con quelle dei sistemi. Da un lato vi sono i primi, i quali legit-
timamente desiderano vedere le date conformate alla nazione di appartenenza, sia in termini di
fuso orario, sia di formattazione. Dall'altra vi è il sistema che, per una serie di motivi, per
esempio possibilità di confrontare date relative a dati inseriti da utenti in diverse parti del
mondo, ordinamenti consistenti sul database, etc. necessita di trattare tutte le date nella stessa
maniera, evitando, possibilmente, il problema del cambiamento di orario. La soluzione standard
a questo quesito consiste nel distinguere problemi di memorizzazione e trattamento delle date
dai requisiti relativi alla relativa presentazione. In particolare, è sempre opportuno far in modo
che il sistema tratti internamente le date come oggetti i cui valori siano espressi in UTC. Ciò
include la memorizzazione sul database e lo scambio di messaggi. Questi campi data, tuttavia,
vanno mostrati all'utente nel formato più desiderato. Ciò è ottenibile utilizzando diverse strate-
gie, per esempio gestire un profilo utente in cui, tra le varie informazioni, memorizzare il fuso
orario e il formato delle date gradito dall'utente, oppure prevedere in ogni interfaccia grafica
un meccanismo (una form di impostazione) atta ad impostare questi valori, oppure una solu-
zione ibrida che includa entrambe, etc. In ogni modo è fondamentale riportare a fianco di
ciascuna data il fuso orario di riferimento.
E consigliabile seguire questo consiglio anche qualora si realizzino applicazioni pensate ini-
zialmente per funzionare in un'unica specifica nazione. Ciò per tutta una serie di motivi: è più
facile implementare il sistema, è possibile implementare e riutilizzare proprie librerie, perché
requisiti di questo tipo tendono inevitabilmente a emergere, e così via.

2.12 Problemi con il riutilizzo dei nomi


Riutilizzare i nomi è una pratica spesso azzardata che per qualche motivo sembra popolare
presso molti sviluppatori e non solo. Per esempio, anche i disegnatori Java probabilmente han-
no commesso qualche leggerezza in questo ambito, come si vedrà di seguito.
Nello scrivere parti di codice, è opportuno valutare attentamente il riutilizzo dei nomi al fine
di evitare problemi del tipo: hiding (nascondimento), shadowing (porre in ombra) e obscuring
(oscurare), descritti di seguito.

2.12.1 Non nascondere gli attributi


Ogni qualvolta in una classe figlia si dichiara un attributo istanza con lo stesso nome (anche di
tipo diverso) di quello della classe genitore, si genera quello che tecnicamente viene chiamato
hiding (nascondimento) dell'attributo. Si noti che questo non ha nulla a che vedere con il
validissimo principio dell'information hiding (incapsulamento).
Per quanto il meccanismo delhiding degli attributi sia presente in Java, il suo utilizzo è
fortemente sconsigliato soprattutto perché si finisce col rendere il codice oscuro e se ne riduce
il controllo. Per esempio, è possibile effettuare un hiding variando il tipo dell'attributo, nel
listato seguente, ciò si otterrebbe dichiarando private int classLevel = 2 nella classe TestB, finendo
per diminuire il livello di comprensione del codice e creando problemi ad altre classi che po-
trebbero utilizzare queste.

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.

public class TestA I


public String classLevel = "Parent";

public class TestB extends TestA I


private String classLevel = "Child";
I

public class TestC I

public static void main(String[] args) {

TestA testClassA = new TestA();


System.out.println( testClassA.classLevel );

TestA testClassB = new TestB();


System.out.println( testClassB.classLevel );

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.

2.12.2 Evitare l'oscuramento dei tipi


Utilizzando correttamente la convenzione Java relativa al codice (cfr. [SJAVCC]), e in partico-
lare le regole relative al nome degli attributi, questo problema non si verifica mai. Pertanto è
possibile che molti sviluppatori anche esperti non abbiamo mai assistito a questa strana mani-
festazione. Tuttavia è opportuno esserne a conoscenza.
Il fenomeno dell'oscuramento si genera qualora si dichiari una variabile con lo stesso nome
di un tipo nello stesso ambito (scope). In questo scenario, l'utilizzo del nome nello spazio in cui
sia la variabile sia il tipo siano utilizzabili, fa sì che la variabile prenda la precedenza [cfr. seguen-
te listato) e che quindi il tipo non sia utilizzabile. Da tener presente che l'oscuramento può
avere effetto anche sui package (N.B. Il codice non compila!).

public class TestObscuring I

private String System;

public static void main(String[] args) I

// System siring obscures System class


/'/ therefore it does not compile!
System.out.println("Compile error!");

I
I

2.12.3 Porre attenzione allo shadowing


Il fenomeno dello shadowing (porre in ombra) avviene quando una variabile o un metodo o
una classe condividano lo stesso nome di corrispondenti elementi nello stesso ambito d'azione.
Quando ciò avviene, ne segue che l'elemento posto in ombra non è più accessibile con lo stesso
nome e, a volte, non è accessibile del tutto. Quando si scrivono i programmi Java è opportuno
evitare questo fenomeno. L'unica eccezione è rappresentata dall'implementazione dei metodi
setXXX. In questo caso è prassi dichiarare i parametri con lo stesso nome degli attributi istanza
da assegnare. In questo contesto, la confusione viene eliminata attraverso l'utilizzo della parola
chiave this.

public class TestShadowing I

static String comment^ "This is a shadowing test!";

public static void main(String[] args) I

String comment = "This is the shadowing";

System.ouf.println(comment);

2.12.4 Fare attenzione al modificatore final


Non è infrequente il caso in cui programmatori Java fraintendano la funzione del modificatore
final. Questo perché il suo comportamento varia a seconda che venga utilizzato con attributi o
con metodi, tanto che si può parlare di tentativo di riutilizzo di una parola chiave.
Per gli attributi, questo significa semplicemente che il valore dell'attributo può essere asse-
gnato una sola volta, e quindi di fatto trasforma l'attributo in una constante. Mentre nel caso
dei metodi significa che questo non può essere né ridefinito (caso dei metodi istanza) né nasco-
sto (metodi statici). Si consideri il seguente frammento di codice relativo al modificatore final:

public class TestA 1


public static final String BASIC_VALUE = "one mlllon";

public class TestB extends TestA I

public static final String BASIC_VALUE = "1 thousand";

public static void main(String[] args) I

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:

ArrayList<IOException> exceptionList = new ArrayList<IOException>();


ArrayList<lnteger> integerList = new ArrayList<lnteger>();

System.ouf.printlnf'Exception list type: \t"+ exceptionList.getClass().getName()):

System, ouf.pnntlnflnteger list type: \t"+ integerList.getClass().getName());

L'output prodotto dalla sua esecuzione è:

Exception list type: java.util.ArrayList


Integer list type: java.util.ArrayList

Come si può notare, il compilatore si occupa di "ridurre" i tipi ArrayLÌSt<IOException> e


ArrayList<Integer> a un semplice ArrayList. Non ci sono dubbi che l'introduzione dei Generics
abbia prodotto un significativo miglioramento della qualità del codice scritto Java e un signifi-
cativo incremento di produttività; tuttavia, i Generics nascondono più di qualche insidia, come
mostrato successivamente. Il problema di fondo è che il disegno dei Generics rappresenta un
buon compromesso tra l'esigenza di far evolvere il linguaggio Java e la necessità di mantenere la
compatibilità con le librerie esistenti. Comunque di compromesso si tratta e, come tale, finisce
per presentare diversi lati deboli. Sebbene l'introduzione dei Generics abbia richiesto di riscrivere
la quasi totalità della libreria del JDK1.4, che è stata estesa appunto per usare questa nuova
feature in JDK5, il codice Java compilato (bytecode) per la versione 1.4 può girare senza pro-
blemi e senza dover essere ricompilato anche su 1.5. Il che indubbiamente è un grandissimo
successo tecnico. A fronte di questo vantaggio, c'è però il limite che i Generics , presentano
alcuni lati oscuri, e non sono così eleganti, efficienti e lineari come i corrispondenti template del
C++. Per ulteriori dettagli sui Generics si consiglia di leggere il testo [JAVAGEN].

3.1.1 Utilizzare i Generics


Viste le loro caratteristiche, si consiglia di utilizzare i Generics. I principali vantaggi sono:

• 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.

import java.util.ArrayList; import java.util.ArrayList;


import java.util.Collection; import java.util.Collection;
import java.util.lterator; import java.util.Iterator;
import java.util.List; import java.util.List;

public class Test I public class Test {

private void testCollection() I private void testCollection() {


List list = new ArrayList();
List<String> list = new ArrayList<String>();
list.add(new StringfJava Best Practise"));
list.add(new String("Luca V.T.")); list.addfJava Best Practise");
Iist.add(new String("2008")); list.addf'Luca V.T.");
Iist.add("2008");
printCollection(list);
printCollection(list);

private void printCollection(Collection c) I


private void printCollection(Collection c) I
Iterator i = c.iterator();
lterator<String> items = c.iteratorQ;
while (i.hasNext()) I
String item = (String) i.next(); while (items.hasNext()) I
System.ouf.println(item);
System. ouf.println(items.next());

public static void main(String argv[]) (


Test myTest = new TestQ; public static void main(String argv[]) {
myTest.testCollection(); Test myTest = new Test();
myTest.testCollection();
Come si può notare, la versione con i Generics è più concisa, elegante: elimina la necessità di
eseguire una serie di down-cast (per esempio String item = (String) i.next()) e cosa più importante
fa sì che molti controlli di tipo avvengano al tempo di compilazione. Per comprendere l'impor-
tanza di ciò è sufficiente aggiungere la seguente linea nel metodo testCollection: list.add(new
lnteger(400)). Nel primo caso, versione pre-Generics, il codice compila "regolarmente" e genera
l'eccezione di ClassCastException (dovuta al tentativo di effettuare il cast a String) solo a tempo di
esecuzione, e più precisante al momento di stampare i valori. Nel caso della versione con i
Generics invece, poiché il controllo avviene a tempo di compilazione, ne segue che il codice
semplicemente non compila.
Da notare, che nel listato con i Generics si è utilizzata il meccanismo, sempre introdotto in
Java 5, dell'auto-boxing (list.add("Java Best Practise"))

3.1.2 Evitare mix tra Generics e tipi grezzi


Le motivazioni alla base dell'utilizzo dei Generics dovrebbero essere ormai chiare. Tuttavia
alcuni programmatori potrebbero essere portati a scrivere del codice contenente sia i Generics
sia i tipi raw. Un esempio tipico si ha con l'utilizzo di oggetti di tipo Iterator non tipizzati. Si
tratta di una pessima scelta implementativa. Ciò sia perché si generano gli stessi inconvenienti
dovuti al non utilizzo dei Generics (codice meno chiaro, necessità di down-cast, etc.), sia perché
si dà luogo ad uno stile decisamente poco professionale.
Inoltre, un utilizzo non accorto di entrambi i tipi può dar luogo ad un problema noto con il
nome di heap pollution (inquinamento dell'heap). Con questo nome ci si riferisce al caso in cui
una variabile di un tipo parametrizzato si riferisca a un oggetto che non è di quel tipo. Questo
scenario si ottiene mischiando i tipi grezzi (raw) con quelli parametrizzati, e con l'aggiunta di
cast non opportuni. In queste situazioni il compilatore emette opportuni warmng, ma il codice
viene regolarmente compilato.
Si consideri il listato riportato poco sotto che dimostra una heap pollution. La relativa esecu-
zione genera il seguente output: "->[10, Luca, Vetti Tagliati]". Come si può notare, per via del principio
della cancellazione si riesce "tranquillamente" ad utilizzare un ArrayLiSt<Number> come un LiSt<String>.
Ciò perché il compilatore esegue il mapping di ArrayList<Number> a ArrayList e di List<String> a
List, e quindi si finisce per generare la paradossale situazione in cui sia assolutamente legittimo
utilizzare un ArrayList come un oggetto di tipo List.
Chiaramente si tratta di un codice assolutamente sconsigliato, in cui il compilatore si limita
ad emettere un warning relativo all'istruzione che genera l'inquinamento dell'heap.

public static void test(ArrayList<Number> listi) {

List<String> list = nuli;

list = genericListl; / / h e a p pollution!!!


list.addfLuca");
list.addf'Vetti Tagliati");

System.out.prlntln("->"+llst);
public static void main(String args[]) {
ArrayList<Number> al = new ArrayList<l\lumber>();
al.add(10);
Test.test(al);
I

3.1.3 I Generics non implementano la proprietà della covarianza


Un'altra caratteristica a cui bisogna porre attenzione con i Generics è dovuta al fatto che questi
non implementano un'importante caratteristica chiamata tecnicamente "covarianza". Per esem-
pio, si immagini di realizzare un oggetto Java "cestino della frutta" e di volerlo utilizzare come
"cestino delle arance", dove, come lecito attendersi, la classe "arancia" estende quella "frutta".
Ora, se si implementasse ciò attraverso array, tutto funzionerebbe correttamente (gli array im-
plementano la proprietà della covarianza), mentre ciò non è possibile con i Generics. Badare
bene però a non far confusione: con i Generics è tuttavia consentito inserire un'arancia nel
cestino della frutta! Per comprendere quanto riportato si analizza il tutto in termini di codice.
Si assuma di aver implementato un metodo, f i n d M i n i m u m ( l \ l u m b e r [ ] n u m b e r s ) , in grado di trova-
re il numero più piccolo di un dato insieme. Questo potrebbe essere correttamente invocato
come segue:

lnteger[] intNumbers = 12, 4 5 , 1 , 1 0 , 45, 56, 34, 22, 3 3 , 1 2 , 78


Integer min = findMinimum(intNumbers)

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:

public int findMinimum(List<Number> numbers) {

con il seguente oggetto

List<lnteger> intNumber = new ArrayList<lnteger>()

! i some initialisation

findMinimum(intNumbers)

genera un errore di compilazione, mentre la seguente istruzione è assolutamente valida:


List<Number> numbers = new ArrayList<Number>()
number.addfnew lnteger(3));

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:

HashMap<String,String>[] intPairArr = new HashMap<String,String>[10]; // ERRORI

3.1.4 Non tutti i tipi possono essere parametrizzati


Quando si disegnano classi parametrizzate è necessario considerare il fatto che sebbene la
quasi totalità dei tipi Java possano essere parametrizzati attraverso il meccanismo dei Generics,
esistono le seguenti eccezioni:

• 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();

I catch (IHegalArgumentException<String> iaeS) { // ERRORI

/ / . . . do something

I catch (NlegalArgumentException<Long> iaeL) I // ERRORI

/ / . . . do something
I
)

3.1.5 Prendere spunto dalle classi della libreria Java


Molto spesso durante l'implementazione di codice basato sui Generics, soprattutto in utilizzi
avanzati, è possibile trovarsi nella condizione di non riuscire a individuare la strategia più effi-
cace, o di essere perplessi circa alcune costrizioni imposte dal compilatore. In questi casi, oltre
al sempre valido suggerimento di ricercare nel mondo della conoscenza su Internet, è
consigliabile dare un'occhiata ai sorgenti delle classi della libreria standard Java. Alcune parti-
colarmente interessanti, da questo punto di vista, sono quelle del package java.util.
3.1.6 Porre attenzione agli overloading e overriding
La tecnica della cancellazione genera una serie di conseguenze anche significative a diversi mec-
canismi della programmazione Java. In particolare, poiché i tipi Generics a run-time perdono
l'informazione circa il tipo, automaticamente si finisce con l'influenzare il comportamento di
gran parte dei meccanismi sensibili al tipo. Tra questi figurano anche Voverloading e l'overriding.
Si consideri il listato riportato poco sotto con alcune peculiarità dell'overloading con tipi Generics:
tale listato stampa Collection<?>. A prima vista si sarebbe tenuti a pensare che l'esecuzione possa
stampare la seguente stringa ArrayLiSt<lnteger>, ma l'esecuzione invece mostra questo output:
Collection<?>. Un bug? No di certo. Questo "strano" comportamento è dovuto al fatto che il
compilatore durante la prima passata del codice riduce i tipi generici ai relativi tipi raw (even-
tualmente ai tipi object) e poi applica Voverloading. Quindi l'associazione tra il metodo overloaded
e il corrispondente printType è eseguita a tempo di compilazione e non a run-time.

public class MyClass<T> {

private void printType(Coilection<?> collection) I


System. ouf.println("Collection<?>");

private void printType(List<Number> list) {


System. ouf.println("List<Number>");

private void printType(ArrayList<lnteger> list) I


System, oui. pri ntl n ("Array List<l nteger>") ;

private void overloaded(List<T> t) I


printType(t);
I

public static void main(String[] args) I


MyClass<lnteger> test = new MyClass<lnteger>();
test.overloadedfnew ArrayList<lnteger>());

Chiaramente, situazioni analoghe si generano anche con 1 'overriding.

3.1.7 Porre attenzione agli attributi statici nei tipi parametrizzati


La presenza di attributi statici in tipi parametrizzati può essere oggetto di confusione. Si consi-
deri questa porzione di codice:

public class MyClass<T> I

public static int numCalls = 0;

public static void main(String[] args) I


MyClass<lnteger> myClassI = new MyClass<lnteger>();
MyClass<String> myClass2 = new MyClass<String>();
myClassI ,numCalls++\ // Non recommended
myClass2 .numCalls++\ // Non recommended
MyClass. numCalls++\ // Recommended

System. ouf.printlnfNum calls myClassI : \t"+myClass1. numCallsy,


System.ouf.pnntlnfNum calls myClass2: \t"+myClass2.numCalls);

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:

l\lum calls myClassI : 3


Num calls myClass2: 3

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

public class MyClass<T> I

public static T genericField; // ERRORI

public static void incorrectMethod() {


T temp = nuli; //ERRORI

// do something
I
I

Da tener presente che, sebbene la dichiarazione statica non sia consentita, quella final invece
non crea problemi.

3.1.8 Cercare sempre di utilizzare il carattere jolly (wildcard)


pur con i limiti appropriati
Il carattere jolly nel contesto dei Generics è rappresentato dal punto interrogativo: ?, ed è
utilizzato per rappresentare famiglie di tipi.
In particolare può essere utilizzato per rappresentare i seguenti tre concetti:

1. tipo illimitato (unbounded). ?. Serve per descrivere tutti i tipi.


2. tipo limitato superiormente. ? extends Type. In questo caso descrive la famiglia di tutti i
tipi sottotipo di Type incluso lo stesso Type.
3. tipo limitato inferiormente. ? super Type. Dichiara la famiglia di tutti i tipi che sono
supertipo di Type dove, questa volta, Type è escluso.

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>

" This m e t h o d runs In linear time.

* @param list the list to be filled w i t h the specified element.


' @param obj The element w i t h w h i c h to fill the specified list.
* @throws U n s u p p o r t e d O p e r a t l o n E x c e p t i o n if the specified list or its
list-Iterator does not s u p p o r t the <tt>set</tt> operation.

public static <T> void fill(List<? super T> list, T obj) f


int size = list.sizef);

if (size < FILL_THRESHOLD || list instanceol RandomAccess) I


for (int i=0; ksize; i++) I
list.set(i, obj);
I
I else <
Ustlterator<? super T> itr = list.listlterator();
for (int i=0; ksize; i++) I
itr.next();
itr.set(obj);
I
(
I

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 ).

public static List<?> alterList(List<?> aList ) I


// does something

return aList;
I

public static void main(String args[]){


Llst<String> names = new ArrayList<Strlng>();

names.addfLuca");
names. addf'Vera");
names. addf'Francesco");
names. addf'Natalya");

List<?> newNames = alterList(names);


// cannot add a string without a cast
// cannot invoke methods like newNames.add("Cinza"):

names = (List<String>) alterList(names);

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:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader)


throws ClassNotFoundException

Bisognerebbe sempre cercare di evitare il carattere jolly nel parametro di ritorno di un metodo.

3.1.10 Controllare l'implementazione dei metodi che prevedono


un carattere jolly nei parametri
Qualora si dovesse implementare un metodo che preveda il carattere jolly come tipo di uno o
più parametri, sarà necessaria più di qualche accortezza. Questo perché la presenza di tale
carattere limita l'accesso ai metodi del tipo. In questo caso è possibile ricorrere a due strategie
come la cattura del jolly (wildcard captare), l'utilizzo di metodi di help, etc. Un'altra buona
strategia consiste nel cercare di utilizzare i metodi forniti dalla libreria Java, come quelli della
classe java.collections.
Si consideri il seguente listato. Come si può notare, per via della presenza del carattere jolly,
non è possibile eseguire una serie di operazioni, come per esempio inizializzare una copia della
lista e utilizzare il metodo set.

public static void alterList(List<?> aList ) I

List<?> tmp = new ArrayList<?>(aList); // ERRORI!!

for (int i=0; i < aList.size(); i++) (


int j = 0;
// do something
tmp.set(i, alJst.get(j)); //ERROR!!!
I

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.

public stalic void alterList(List<?> aList ) <


a/ie/^aList);

public static <T> void alter(List<T> aList) I

List<T> tmp = new ArrayList<T>(aList);

lor (int i = 0; i < aList.sizeQ; i++) I


int j = 0;
// do something
tmp.set(i, aList.get(j));
I

3.1.11 Non confondere collezioni eterogenee con collezioni omogenee


di tipi non conosciuti
Non è infrequente il caso in cui alcuni sviluppatori debbano scrivere del codice generico che
manipoli oggetti di cui non si conosca a priori il tipo e finiscano così con l'utilizzare i Generics
in maniera errata. In particolare bisogna sempre fare attenzione a distinguere correttamente i
due casi:

1. Collection<Object> rappresenta una collezione di elementi eterogenei. Infatti può essere


popolata con tutti gli oggetti sottotipi di Object: ossia tutte le classi Java.
2. Collection<?> rappresenta una collezione di elementi omogenei di cui però non si conosce
il tipo. Una volta definito il parametro della collezione, solo elementi di quel tipo speci-
fico potranno essere manipolati dalla collezione.

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.

3.1.12 Porre attenzione all'implementazione dei metodi "infrastnitturali"


Come lecito attendersi, la presenza dei Generics fa sì che l'implementazione dei classici metodi
infrastrutturali (equals, hash, clone, etc.) richieda qualche accortezza in più.
Nella parte seguente sono mostrati alcune implementazioni tipo di questi metod, in partico-
lare l'implementazione del metodo equals nella classe java.util.AbstractMap. Classe genitore delle
più popolari: HashMap, IdentityHashMap, TreeMap, WeakHashMap.

public boolean equals(0bject o) I


il (o == this)
return true;

if (!(o instanceof Map))


return false;

MapcK, V> t = (MapcK, V>) o;

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

} catch (ClassCastException unused) I


return false;
I catch (NulIPointerException unused) I
return false;
I

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>.

public Object clone() {


try [

ArrayList<E> v = (ArrayList<E>) super.clone();


v.elementData = (E[j) new Object[size];
System.araycopy(elementData, 0, v.elementData, 0, size);
v.modCount = 0;

I catch (CloneNotSupportedException e) I
// this shouldn't happen, since we are Cloneable
throw new lnternalError();
I

return v;

3.2 Static import


Lo static import rappresenta una delle novità introdotte con la versione Java5 di cui, questa
volta, molto probabilmente si sarebbe potuto fare tranquillamente a meno.
L'idea di base consiste nel disporre di speciali versioni del costrutto di import ( i m p o r t introdot-
to dalla parola chiave Static) che permettano di utilizzare i membri delle classi specificate, a
meno della relativa qualificazione.
Si considerino le due versione del listato riportato di seguito su due colonne. Come si può
notare, la presenza del costrutto static import permette di evitare la ripetizione di una serie di
elementi (come per esempio il nome della classe Math) e quindi rendere la produzione del
codice più veloce. Inoltre ciò dovrebbe eliminare la tentazione per alcuni programmatori di
ricorrere a trucchi poco raccomandabili per evitare la ripetizione dei nomi delle classi. A sini-
stra l'import standard, a destra l'import statico
public class Test I import static java.lang.Math. *;
import static java.lang.System.out,
public static void printValues(double val) I
it ((val >=0) && (val <=360)) I public class Test I
System.ouf.printlnfValue :"+val);
System. ouf.println("Math.sin("+val+")="+ public static void printValues(doubie val) {
Math.s/'n(val) ); if ( (vai >=0) && (val <=360)) {
System. oi/f.println("Math.cos("+val+")="+ oi/f.printlnfValue :"+val );
Math.cos(val) ); oui.println("Math.sin("+val+")= " + s/n(val));
System. ouf.println("Math.tan("+val+")="+ ouf.println("Math.cos("+val+")= " + cos(val));
Math.tìn(val) ); ouf.println("Math.cos("+val+")= " + fan(val));
I else ( I else {
System. ouf.printlnfAllowed value range: ouf.printlnfAllowed value range: 0 - 360"):
0 - 360");

3.2.1 Utilizzare con cautela le importazioni statiche


Questa regola è assolutamente atipica rispetto a quanto riportato in questo testo: dopo aver
introdotto una nuova feature, ne viene scoraggiato l'utilizzo. La ragione c'è, però. Sebbene
questo meccanismo permetta di ridurre la necessità di ripetere continuamente parti di codice,
e quindi permetta di rendere il codice più succinto, presenta anche una serie di importanti
svantaggi tali da non renderlo sempre raccomandabile. Il più importante è relativo al fatto che
il codice diviene, paradossalmente, meno chiaro. Infatti, a meno di casi evidentissimi come
quelli mostrati poco sopra, la lettura del codice richiede di dover verificare a quale elemento
(classe o interfaccia) appartengano i vari elementi utilizzati.
Pertanto, è consigliabile utilizzare questo meccanismo con parsimonia e, qualora si decida di
utilizzarlo, è opportuno limitare l'importazione statica a uno o due elementi.

3.1.2 Valutare l'utilizzo delle importazioni statiche con le costanti


La strategia di utilizzare apposite interfacce per incapsulare valori costanti è oggetto di una
lunga diatriba. Da un lato vi sono coloro che considerano questa soluzione come un vero e
proprio antipattern, dall'altra invece ci sono coloro che considerano tale strategia assolutamen-
te legittima. Come al solito, entrambe le posizioni sono fondate su alcuni validi concetti.
Nel primo caso si obietta legittimamente che le interfacce dovrebbero essere utilizzare per
dichiarare del comportamento, quindi una sorta di contratto tra le classi clienti e quelle che
implementano l'interfaccia: si tratterebbe di un utilizzo snaturato.
Dall'altra parte dello schieramento invece si argomenta che non ci sia nulla di particolarmen-
te sbagliato nell'incapsulare i valori costanti qualora questo sia l'unico modo per far sì che
diverse classi utilizzino gli stessi valori e questi non prevedano particolare e comportamento.
Chiaramente, il problema dell'abuso è sempre in agguato, e come tale si tratta effettivamente
un antipattern. In ogni modo, è consigliato l'utilizzo del meccanismo delle importazioni statiche
in presenza di costanti, come mostrato di seguito.
import static java.awt.Color.*;

public class ImportExample I


public static void main(String args[]) I
new Form(RED);
I

3.3 Auto-Boxing / Unboxing


L'auto-boxing / auto-unboxing è un altro interessante meccanismo introdotto con la versione
JDK 5, che permette di eseguire la trasformazione automatica dei tipi base nei corrispondenti
oggetti di wrapping (auto-boxing) e vice versa [auto-unboxing).
Da notare che questo automatismo non si limita al caso delle collezioni, ma funziona anche
con le normali operazioni aritmetiche, come mostrato nel codice riportato sotto, la cui esecu-
zione stampa correttamente il valore 21. Come lecito attendersi, la somma avviene tra tipi base...
Purtroppo Xoverloading degli operatori non è ancora stato introdotto in Java.

Integer numi =1: //auto-boxing


int num2 = 20;

System.ou/.println(num1 + num2); //auto-unboxing

Si tratta di un meccanismo molto apprezzato dagli sviluppatori in quanto permette di realiz-


zare codici più eleganti e puliti e, in generale, aiuta a risparmiare tempo.

3.3.1 Utilizzare il meccanismo dei boxing


Come standard, il primo suggerimento consiste nell'awalersi di questo meccanismo, ma con
intelligenza. Come visto, rende il codice più elegante, pulito, quindi più leggibile e porta a un
generale risparmio di tempo. Tuttavia, come mostrato successivamente, in alcuni contesti può
generare significative perdite di prestazioni.

3.3.2 Ricordarsi la compatibilità con il passato


In alcune situazioni, le priorità del meccanismo del boxing avvengono in un modo che non
sempre risulta chiaro in prima analisi. Si consideri il seguente il codice.

public class Test I

private void printValue(lnteger i) I


System. ouf.println("lnteger:"+i);
I

private void printValue(long i) {


System. ouf.println("long:"+i);
public static void main(String argv[|) |

Integer ¡1 = 1 ;
long 11 = 10L;
int i2 = 5;

Test myTest = new Test();

myTest.printValue(il);
myTest.printValue(M);
myTest.printValue(i2);

L'esecuzione del frammento di codice produce il seguente output:

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.

3.3.3 Fare attenzione alle performance


Il meccanismo di boxing in determinati contesti non fornisce le stesse prestazioni dei tipi
base. Questo risultato è abbastanza ovvio: il boxing si ottiene creando oggetti immutabili di
wrapping, ma è opportuno tenerlo presente soprattutto in presenza di loop estesi e in applica-
zioni real-time.

private static void printPeriod(String comment, long startTime) I

long endTime = System.na/7o7ime();


System. oo/.println( comment+" \t"+
(endTime-startTime)+"ns "+"\t"+
((endTime-startTime)/1000000)+"ms");
I

public static void main(String argv[]) I

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);

// boxing: values retrieval and calculations


startTime = System. nanoTimeQ,
lor(int i=0; ¡<500000; i++)(
listValues.set(i,listValues.get(i)*7+4);
I
printPeriod("Boxmg calculation: \t", startTime);

// natural type: values retrieval and calculations


startTime = System. nanoTimeQ-,
for(int i=0; ¡<500000; i++)|
arrValues[l] = arrValues[i]*7+4;
I
printPeriod("\ialura\ type calculation:", startTime);

L'esecuzione di tale listato genera risultati del tipo:

ArrayList declaration: 32127ns 0ms


Array declaration: 8885207ns 8ms
Initialisation ArrayList: 249998533ns 249ms
Initialisation ArrayList: 1501028ns 1ms
Boxing calculation: 157989633ns 157ms
Natural type calculation: 2819073ns 2ms
Sebbene si tratti di misurazioni piuttosto grossolane, che includono anche l'invocazione del
metodo di calcolo e stampa della differenza, forniscono chiare indicazioni circa i diversi ordini
di grandezza. Pertanto nella scrittura di programmi in cui le performance giocano un ruolo
centrale è opportuno fare attenzione all'utilizzo del boxing, soprattutto in loop estesi, al fine di
evitare che le continue conversioni finiscano per degradare le performance.

3.3.4 Cercare di evitare gli oggetti di wrapping nei calcoli


Dalla lettura di quanto riportato nella regola precedente, questa dovrebbe essere ridondante.
In complessi e/o lunghi calcoli matematici, il ricorso agli oggetti di wrapping e agli automati-
smi di auto-boxing e auto-unboxing dovrebbero essere, per quanto possibile, minimizzati al fine
di evitare impatti significativi sulle prestazioni.

3.3.5 Fare attenzione all'operatore di uguaglianza


Un elemento a cui bisogna assolutamente porre attenzione con i meccanismi di boxing è l'ope-
ratore di uguaglianza che potrebbe presentare qualche sorpresa. Si consideri il frammento di
codice riportato, che mostra problemi con l'operatore ==.

public static void main(String args[J) I

Integer ¡1 = new lnteger(10);


Integer ¡2 = 10;
System.oui.println("1. Equals : " + (¡1 == i2));

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));
)

La sua esecuzione genera il seguente "magico" risultato:

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.

3.4 Varargs: argomenti variabili


Varargs (variable arguments, "argomenti variabili") rappresenta un'altra feature introdotta
con Java5.
Come suggerisce il nome, si tratta di un meccanismo che permette di implementare metodi
in grado di prendere zero o un numero variabile di elementi del tipo definito.
Anche questa feature ha finito per generare una serie di polemiche, spesso legittime. Come
nel caso dei Generics, si tratta di sintassi non supportate dalla Java Virtual Machine per via dei
soliti motivi di compatibilità con le precedenti versioni Java. Queste feature sono pertanto
risolte dal compilatore attraverso appositi mapping ai costrutti esistenti, in questo caso array.
Ciò fa sì che i varargs presentino a volte dei comportamenti inaspettati, come mostrato succes-
sivamente. Prima di proseguire oltre, si consideri il codice riportato di seguito. Come si può
notare il metodo minValue può essere invocato con un numero qualsivoglia di valori, o addirittu-
ra con nessuno.

public static ini minValue(int... vallnt) I


int min = lnteger.MAX_VALUE;

if ( vallnt.length = = 0 ) 1
throw new HlegalArgumentExceptionfNo paraemter provided");
I

for (int el : vallnt) I


il (el < min) I
min = el;
I
I

return min;
)

public static void main(String args[]) I


System.out.println(min\/alue(25, 3 , 1 0 0 , 41));
System.out.println(minValue(123, 2));
System.out.println(minValue());
I

3.4.1 Utilizzare il meccanismo dei varargs con parsimonia


Anche in questo caso, si consiglia vivamente di utilizzare questa feature con parsimonia. Molto
probabilmente rientra nella categorie delle feature di cui non si sentiva grandissimo bisogno.
In effetti, tutto quello che è possibile fare con i varargs si può fare con i corrispondenti array.
Probabilmente, questo meccanismo è particolarmente utile ai programmatori meno esperti
con incertezze circa l'utilizzo degli array...
L'utilizzo dei varargs in genere non è fortemente consigliato perché può portare
all'implementazione di metodi meno robusti. Per esempio, si tende a rilassare i controlli su
parametri tipici del non utilizzo dei varargs, si possono avere più problemi nell'implementare
metodi con un numero fisso di parametri, etc.
Si consideri il codice riportato di seguito, che mostra qualche stranezza nell'utilizzo dei tipi
varargs. Qualora eseguito così com'è, genera l'output atteso (0, 0 e 3). Tuttavia il compilatore
segnala un warning nella prima esecuzione, chiedendo di eseguire il cast del valore nuli al tipo
Object. Seguendo tale suggerimento, e quindi sostituendo la prima invocazione con il seguente
codice: System.out.printlnfelements : " + elementCount( (Object)null) ); e quindi eseguendo si ottiene
il seguente inaspettato output: 1, 0 e 3. Ciò perché il compilatore sostituisce (Object)null con un
array di un solo elemento posto a nuli.

private static int elementCount(Object... elements) {


return elements == null ? 0 : elements.length;
I

public static void main(String... args) {


System.out.println("elements : " + elementCount(null));
System.out.println("elements : " + elementCountf));
System.out.printlnfelements : " + elementCountfLuca", "Antonio", "Roberto"));
)

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.

private static int elementCount(String... elements) I


return elements == null ? 0 : elements.length;

private static inl elementCount(lnteger... elements) I


return elements == null ? 0 : elements.length;

public static void main(String... args) I


System.oi//.println("elements : " + elementCount(\, 2, 3, 4, 5, 6, 7, 8, 9));
System.ouf.printlnfelements:" + elementCount("Luca", "Antonio", "Roberto"));
System.ouf.println("elements : " + elementCount()); // ERROR

3.5 Applicazioni multi-threaded tradizionali


La programmazione multi-threading (MT) rappresenta indubbiamente uno degli argomenti
più complessi della programmazione. Pertanto, al fine di non appesantire la trattazione e, al
tempo stesso, di fornire i lettori meno esperti dell'argomento le nozioni fondamentali, si è
deciso di dedicare al fondamentale argomento una specifica appendice (Appendice D) a cui si
rimanda per spiegazioni di maggiore dettaglio.
Cominciamo a vedere il MT tradizionale, ossia i meccanismi presenti prima dell'introduzio-
ne del nuovo package della concorrenza. Esaurite le direttive relative al multithreading tradi-
zionale, ci concentreremo sul nuovo package java.util.concurrent di Doug Lea. Il tutto viene con-
cluso con una serie di consigli indipendenti dalle particolari classi utilizzate.
3.5.1 Preferire l'implementazione dell'interfaccia Runnable
Java offre due modi per definire classi le cui istanze dovranno essere eseguite da appositi thread:

1. implementare l'interfaccia java.lang.Runnable;


2. estendere la classe java.lang.Thread.

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.

class simpleThread extends Thread I

// Questo m e t o d o è invocato q u a n d o il thread è in esecuzione


public void run() I
I

// Per creare il thread è necessario eseguire le seguenti istruzioni


Thread thread = new SimpleThread));
thread. start();

Vediamo la creazione di un thread tramite implementazione dell'interfaccia java.lang.Runanble.

class SimpleThread i m p l e m e n t s Runnable I

// Questo m e t o d o è invocato q u a n d o il thread è in esecuzione


public void run() I
I

/ / Per creare il thread è necessario eseguire le seguenti istruzioni

// 1. Creare un oggetto di tipo runnable


SimpleThread myRunnable = new SimpleThreadf);

/ / 2. Creare un thread per eseguire l'oggetto runnable


Thread myThread = n e w Thread(myRunnable);

il invocare i\ metodo start


myThread.startO;

3.5.2 Implementare classi thread in modo che terminino


programmaticamente
Analizzando programmi concorrenti scritti in Java non è infrequente imbattersi in codici che
utilizzano i metodi deprecati stop e suspend della classe thread. Si tratta di una prassi pericolosa
e quindi sconsigliata. In particolare, interrompendo bruscamente l'esecuzione di un thread
(invocandone il metodo Stop) lo si forza a rilasciare tutti i monitor precedentemente acquisiti
senza permettergli di terminare il task in esecuzione. Tale brusca terminazione avviene per
mezzo della propagazione dell'eccezione ThreadDeath. Se un oggetto precedentemente bloccato
dal thread in questione si trova in uno stato inconsistente durante la terminazione del thread,
altri thread potrebbero utilizzarlo senza aver alcuna opportunità di essere informati circa la
relativa inconsistenza (si dice che è "danneggiato", damaged). Questa situazione ha le potenzialità
di generare comportamenti randomici difficilmente diagnosticabili. A peggiorare la situazione
interviene il fatto che l'eccezione ThreadDeath termina i thread in maniera "silenziosa" senza
fornire all'utente alcun avvertimento del fatto che il programma potrebbe trovarsi in uno stato
inconsistente. Situazioni del genere, inoltre, potrebbero generare strani comportamenti solo
dopo ore o giorni, rendendone l'individuazione e la diagnosi ancora più problematica, specie in
applicazioni mission criticai quali quelle solitamente realizzate in Java (sistemi bancari, gestione
di prenotazioni etc.).
L'implementazione corretta della terminazione di un thread dovrebbe utilizzare un codice
simile a quello riportato di seguito, che non va utilizzato in caso di operazioni bloccanti.

public class EsampleThread implements Runnable {

boolean volatile endOfExecution = false;

/**

* thread run method


7
public void run() {
boolean endOfWork = false;

while ( (¡endOfExecution) && (lendOfWork) ) I

//perform the work


endOfWork = someExecutionf);

I
I

/ "

* Request the thread termination.


7
public void requestStop(){
endOfExecution = true;
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.

public class EsampleThread extends Thread I

/**

" thread run method


7

public void run() {

try I
boolean endOfWork = false;

while ( (lislnterruptedO) && (lendOfWork) ) I

// perform blocking work


endOfWork = someExecutionQ;

I
I catch (InterruptedException ie) {
// clean-up and thread-exit
I

/**

" Request the thread termination


*/
public void requestStop()|
interruptf);
I
I

3.5.3 Utilizzare volatile


Le variabili volatile rappresentano un meccanismo Java per implementare la sincronizzazione,
tanto che spesso sono definite come la sincronizzazione leggera, soprattutto dal punto di vista
della notazione.
I meccanismi di sincronizzazione in Java hanno principalmente due importanti obiettivi:
evitare che diversi thread accedano concorrentemente ad aree di codice critiche, assicurare la
corretta visibilità degli oggetti condivisi. Per quanto riguarda quest'ultima, è una caratteristica
abbastanza complessa che interagisce con le politiche di gestione della memoria, le ottimizzazioni
eseguite dai compilatori, le cache gestite dai vari thread, etc. Comunque, in generale, la presen-
za del costrutto fa sì che gli aggiornamenti eseguiti da uno specifico thread prima di uscire
dall'area sincronizzata diventino visibili ad altri thread che si accingono ad entrare in tale por-
zione di codice.
Si consideri l'esempio di codice mostrato poco sopra. Qualora la variabile endOfExecution non
fosse dichiarata volatile, si correrebbe il rischio che ciascun thread ne gestisca una copia nella
propria memoria di cache senza aggiornarla mai con la copia master. Ciò potrebbe portare
all'ovvio risultato di avere thread che non si accorgano del relativo cambio di valore e che
quindi non terminano quando richiesto.
Da quanto detto è evidente che la parola chiave volatile fa sì che la JVM non renda possibile ai
thread di gestire copie delle variabili nella propria cache, e pertanto finisce per peggiorare le
prestazioni. Quindi, è necessario utilizzare questa parola chiave con oculatezza.
Le variabili volatile forniscono un modo elegante ed agevole per definire la sincronizzazione,
influenzando sia l'atomicità delle istruzioni, sia la relativa visibilità. Tuttavia, da sole offrono la
protezione richiesta quando la variabile è indipendente sia dal valore di altre variabili che dal
proprio valore. Come si può notare entrambe condizioni sono rispettate dal codice dell'esem-
pio. Da tener presente però che l'utilizzo delle variabili volatile può rendere il codice più fragile.

3.5.4 Non utilizzare il metodo Thread.suspend


Il metodo Thread.suspend non va utilizzato perché è rischioso e come tale è stato correttamente
deprecato. In questo caso la deprecazione è avvenuta perché il metodo è intrinsecamente sog-
getto alla generazione di situazioni di dead-lock. Il problema è relativo al fatto che, mentre un
thread è sospeso, nessun altro può accedere alle risorse che questo ha bloccato fintanto che la
relativa esecuzione viene ripresa (invocazione del metodo Thread.résumé). Ora, se il thread inca-
ricato di riawiare quello sospeso dovesse avere la necessità di accedere a un oggetto tra quelli
bloccati da quest'ultimo, ecco generata la situazione di dead-lock che si manifesta con la pre-
senza di processi congelati.
Si immagini l'impatto sul sistema di un thread bloccato che abbia acquisito una serie di lock
tra cui uno o più di importanza critica, come per esempio uno che veicola l'utilizzo di una
risorsa importante del sistema. Anche in questo caso la situazione può essere risolta in maniera
programmatica utilizzando i metodi wait e notify.

3.5.5 Evitare la manipolazione dei livelli di priorità dei thread


La manipolazione esplicita delle priorità dei thread può creare problemi legati alla dipendenza
di Java dallo OS (Operating System, sistema operativo). In teoria il linguaggio Java definisce
dieci diversi livelli di priorità: la classe java.lang.Thread definisce le seguenti costanti (attributi
statici):

public static final int MAX_PRIORITY = 10;


public static final int MIN_PRIORITY = 1;
public static final int N0RM_PRI0RITY = 5

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.

3.5.6 Considerare i diversi modelli di esecuzione dei thread


Un importante vincolo da tener presente quando si progettano programmi concorrenti in Java
è che, in questo contesto, il linguaggio non è completamente indipendente dalla piattaforma di
esecuzione. Questa caratteristica fondamentale è ancora ottenibile a spese però di un disegno
che contempli, in maniera ridondante, i diversi modelli di funzionamento delle varie piattafor-
me (il codice deve essere quindi platform aware). La questione nodale è che la programmazione
multi-threading presenta dipendenze strutturali dal sistema operativo, le quali possono essere
minimizzate ma non eliminate. Il problema per i programmatori risiede nel fatto che il linguag-
gio Java non prevede alcun modello multi-threading di riferimento; questo è "ereditato" dalla
piattaforma di esecuzione. Ciò fa si che se si desidera scrivere programmi concorrenti in grado
di essere eseguiti su qualsiasi piattaforma e quindi per qualsiasi modello multi-threading, que-
sti devono contenere i meccanismi di funzionamento per i vari modelli.
Il modello più comune, cosiddetto preventivo o a partizione di tempo (time- slice), prevede
che la CPU sia assegnata ai thread per intervalli di tempo (slices, "fettine") ben definiti, al
termine dei quali, il thread è forzato a rilasciare la CPU. In questo caso è il sistema operativo
che si fa interamente carico di gestire la concorrenza.
Nell'altro modello, denominato cooperativo, la concorrenza è (quasi) completamente
demandata all'applicazione, quindi al programmatore il quale dichiara esplicitamente quale
siano i momenti più opportuni in cui un thread debba cedere il controllo (invocazione del-
l'istruzione yield), inoltre, potenzialmente, il passaggio del controllo tra diversi thread, può av-
venire al livello di user-mode subroutine, evitando il coinvolgimento di servizi kernel del OS, i
quali possono richiedere fino a diverse centinaia di cicli macchina.
Pertanto, i programmi multi-threading Java devono essere scritti considerando sia il fatto che
il thread possa essere forzato a lasciare la CPU per via dell'esaurimento del tempo a disposizio-
ne, sia che 0 rilascio della CPU possa avvenire solo dietro esplicita richiesta da parte dello
stesso thread (operazione di yield).

3.5.7 Evitare dead-lock


Il dead-lock ("blocco mortale") è una situazione in cui due o più thread interferiscono tra di
loro in un modo tale che nessuno possa procedere nella propria esecuzione: un thread blocca
una serie di risorse, però per proseguire e rilasciarle ha bisogno di altre risorse, bloccate da uno
o più thread che, a loro volta, per poter proseguire, hanno bisogno delle risorse bloccate dal
primo thread. Il classico cane che si morde la coda... Sebbene esista una serie di tool di suppor-
to all'individuazione di dead-lock, spesso, si tratta di situazioni difficili da individuare. Ciò
nonostante è possibile illustrare una serie di strategie che favoriscono la prevenzione di dead-
lock. In particolare:
• implementare i thread aafinché la procedura per l'acquisizione di lock segua lo stesso
ordine: pertanto, qualora un thread trovi bloccata la prima risorsa, automaticamente,
non tenta di acquisire le altre;
• cercare di inglobare i vari lock in un altro: in questo caso l'acquisizione del lock globale
causerebbe l'acquisizione degli altri lock;
• fare in modo che un thread che abbia acquisito un certo numero di risorse rilasci tutte le
risorse precedentemente acquisite qualora fallisca ad acquisire le restanti dopo un paio
di tentativi.

Il codice seguente mostra un semplice esempio di deadlock causato da due thread che tenta-
no di acquisire due risorse in senso opposto.

public class SimpleDeadLock i m p l e m e n t s Runnable I

private Object resourcel;


private Object resource2;
private String name;

public SimpleDeadLock(String aName, Object aResourcel, Object aResource2) I

name = aName;
resourcel = aResourcel;
resource2 = aResource2;

public void run() {

System.out.printlnfThread: "+name+" running...");

synchronized (resourcel ) {

System.out.println("Thread; "+name+" locked resource:"+resource1);

try I

System.out.printlnfThread: "+name+" sleeping...");

// il thread viene m e s s o a d o r m i r e per 5 0 millisecondi


Thread.sleep(50);
I catch ( I nterrupted Exception ie) I
ie.printStackTrace();

System.out.printlnfThread: "+name+" awaking...");

// al risveglio il thread tenta di effettuare il lock della seconda risorsa


synchronized (resource2) (
System.out.println("Thread: "+name+" locked resource:"+resource2);
I

public static void main(String[] args) I

String resourceA = "RisorsaJ";


String resourceB = "Risorsa_2";

SimpleDeadLock runnablel = new SimpleDeadLock("T1", resourceA, resourceB);


SimpleDeadLock runnable2 = new SimpleDeadLock("T2", resourceB, resourceA);

Thread treadl = new Thread(runnablel);


Thread tread2 = new Thread(runnable2);

treadl ,start();
tread2.start();

L'esecuzione del precedente codice genera il seguente output:

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.

3.5.8 Porre attenzione alle inizializzazioni pigre (Lazy initialization)


La lazy initialization è una tecnica di programmazione molto utilizzata la cui idea alla base
consiste nel posticipare l'esecuzione di determinate attività, spesso l'inizializzazione di oggetti
"pesanti", fino a quando non ve ne sia l'effettiva necessità, in modo da diluire nel tempo ope-
razioni costose. L'applicazione di questa tecnica impone di implementare appositi controlli per
assicurarsi che questa inizializzazione avvenga una sola volta. Ciò fa sì che spesso si ricorra
all'implementazione diSingleton. Sebbene si tratti di un problema ricorrente, è sempre possi-
bile individuare codici errati. Alcuni esempi di codice errato sono mostrati di seguito.
Qui abbiamo un esempio errato di implementazione della lazy initialization. Si assume che il
metodo costruttore, dichiarato privato, esegua operazioni pesanti.

public class HeavyObject I

private final static HeavyObject instance = null;

public HeavyObject getlnstanceQ I


il (instance == null) {
instance = new HeavyObjectQ;
I

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:

1. T^ esegue il metodo getlnstanceQ;


2. T trova il valore nullo e quindi acquisisce il lock;
3. T b esegue getlnstance mentre l'esecuzione di T è interrotta;
4. T trova ancora il valore nullo (T è stato interrotto) però fallisce ad acquisire il lock e
quindi viene forzato a lasciare la CPU;
5. Ta acquisisce la CPU e quindi termina l'esecuzione del metodo getlnstance inizializzando
l'oggetto e rilascia 0 lock;
6. T b può finalmente acquisire il lock e quindi prosegue la sua esecuzione, inizializzando
nuovamente l'oggetto.

Ecco il pattern pericoloso del doppio check.

public class HeavyObject I

private final slatic HeavyObject instance = nuli;

public HeavyObject getlnstance() I


if (instance == null) {
synchronized(lhis) I
if (instance == null) I
instance = new HeavyObject();
I

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.

public class HeavyObject I

private final static HeavyObject instance = null;

public synchronized HeavyObject getlnstance() {


if (instance == null) I
instance = new HeavyObjectf);
I

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.

3.5.9 Utilizzare ThreadLocal per implementare confinamenti


all'interno dei singoli thread
Gli oggetti ThreadLocal permettono di mantenere una copia separata della variabile gestita per
ciascun thread che ne richieda l'utilizzo, permettendo di realizzare una sorta di pool preassegnato.
Ciò fa sì che ciascun thread veda esclusivamente i valori della propria copia senza doversi
preoccupare di eventuali modifiche o letture da parte di altri thread. Così facendo si riesce a
implementare brillantemente il confinamento delle variabili all'interno dei thread. La logica
conseguenza è che la classe ThreadLocal, permette di ottenere comportamenti assolutamente
thread-safe eliminando al contempo diversi colli di bottiglia, permettendo al codice di scalare
linearmente con il numero di thread. Un primo esempio di utilizzo di ThreadLocal lo si è visto
con i tipi date. In quel caso è stato suggerito di utilizzare questa classe per risolvere il problema
legato alla caratteristica di non thread-safe della classe SimpleDateFormat.
Un altro esempio utilizzo è relativo alla condivisione delle connessioni JDBC. Poiché queste
non sono necessariamente thread-safe, la loro gestione in ambiente MT richiede qualche prov-
vedimento, come appunto l'utilizzo della classe ThreadLocal, come riportato nel listato sequente:

privale static ThreadLocal<Connection> connectionCache =


new ThreadLocal<Connection>() I
public Connection initialization() I
return DriverManager.getConnection(DBMS_URL)

public static Connection getConnectionQ I


return connectionCache.getQ-,

3.5.10 Fare attenzione all'atomicità dei tipi long e doublé


La proprietà di atomicità applicata agli attributi di un programma Java garantisce che un thread,
in un qualsiasi intervallo di tempo, acceda o al valore iniziale o quello finale di uno specifico
attributo. In altre parole, assicura che non sia possibile da parte di un thread accedere a valori
aleatori generati da diversi thread intenti a modificare contemporaneamente il valore della
stessa variabile. Questa proprietà è garantita per tutti i tipi base ad eccezione di long e doublé
(non dichiarati volatile). Questo perché la relativa implementazione utilizza una dimensione di
64bit che spesso è trattata come due word da 32bit. Pertanto ciascuna operazione di lettura e
scrittura richiede una doppia operazione che, in determinate condizioni, può aver luogo con
una distanza temporale (relativamente) significativa. Ciò, pertanto, potrebbe portare al verifi-
carsi della situazione in cui due o più thread modifichino e/o leggano la stessa variabile all'uni-
sono ottenendo un valore errato dovuto all'intrecciarsi delle operazioni da 32bit.
Pertanto, l'utilizzo di variabili o attributi di tipo long e doublé (non dichiarati volatile), in un
ambiente multi-threading va reso esplicitamente atomico. L'atomicità è condizione necessaria
ma non sufficiente per la programmazione concorrente (per esempio non garantisce l'accesso al
valore più recente della variabile).
La JVM garantisce l'atomicità di attributo o variabile di tipo long e doublé dichiarato volatile.

3.5.11 L'incremento unitario non è thread-safe


Nella scrittura di programmi MT è necessario tenere a mente il fatto che l'operatore incremento
unitario non è thread-safe. La sintassi potrebbe essere ingannevole (si tratta di un'operazione) ma
in realtà l'incremento unitario è una contrazione delle tre classiche e distinte operazioni: lettura,
aggiornamento e memorizzazione del valore. Come tali non sono atomiche. A peggiorare la situa-
zione interviene il fatto che individuare problemi di race-condition generati da incrementi unitari
è molto difficile, in quanto tendono a manifestarsi molto raramente. Ecco un semplicissimo esempio
di un programma che non fa altro che eseguire l'incremento unitario di un contatore.

public class Counter I

private static int counter.


public static void main(String[] args) I
counter++;
I
)

Si consideri il semplicissimo programma mostrato nel listato precedente. Eseguendo l'utility


javap (javap -c Counter) è possibile ottenere il seguente frammento di byte code:

public static void main(java.lang.String[]);


Code:
0: getstatlc #18; //Field counter: I
3: ¡const_1
4: iadd
5: putstatic #18;//Field counter:l
8: return

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.

3.6 Applicazioni multi-threaded Java 5


In questa sezione sono presentate una serie di linee guida più specifiche per Java 5. Poiché
l'introduzione di questa libreria è relativamente recente, non è stato possibile collezionare/
analizzare un grandissimo numero di errori tipici e trabocchetti. Tuttavia, ce ne sono già diversi
e molto importanti.

3.6.1 Non reinventare la ruota


Prima di imbarcarsi nell'implementazione di una classe/libreria è sempre opportuno verificare
se esistano delle versioni già implementate che possano risolvere il problema e soprattutto chie-
dersi se sia veramente il caso di avventurarsi in nuove implementazioni. Questo è particolarmen-
te ricorrente in librerie per ambienti MT. Il caso più emblematico sono le implementazioni di
cache. Analizzando molti sistemi si osservano spesso implementazioni ad hoc, con tutti i limiti
del caso. Pertanto, prima di lanciarsi nell'implementazione di librerie/utility etc. si consiglia di
verificare la possibilità di utilizzare qualcosa di già esistente. Inoltre, qualora sia necessario im-
plementare componenti MT è opportuno studiare attentamente il package per la gestione della
concorrenza introdotto con Java 5. Questo presenta un'enorme ricchezza di classi appositamen-
te disegnate per offrire elevate prestazioni in ambienti fortemente concorrenti. Dall'analisi di
queste classi, spesso, è possibile individuare versioni corrispondenti di proprie classi e utility.
Tuttavia, è importante assegnare la priorità alle versioni Java, ciò per una serie di motivi:

• performance. Le nuove classi sono state a lungo studiate e implementate da un team


altamente specializzato. Inoltre traggono vantaggio da una serie di nuove istruzioni al
livello di bytecode. Pertanto tendono a presentare prestazioni difficilmente superabili.
• migliore supporto. Ci sono specialisti che rivedono e migliorano le varie implementazioni.
• documentazione/curva di apprendimento. Tali classi sono parte della libreria standard
Java, sono ben documentate, supportate da articoli e libri, e sono parte del bagaglio
culturale di ogni sviluppatore Java: non dovrebbe essere necessario ulteriore training.
• elevato livello di affidabilità. Il corretto funzionamento delle classi Java è stato verificato
negli ambienti più disparati, per le più diverse applicazioni, offrendo un'affidabilità
assolutamente unica.

3.6.2 Ricordare che gii oggetti atomici non estendono automaticamente


la relativa atomicità a operazioni composte.
Con l'introduzione di Java 5, la programmazione MT è diventata indubbiamente meno com-
plessa. In particolare, la presenza dei tipi di dati atomici (java.util.concurrent.atomic) ha risolto
diversi problemi, come visto in precedenza. Tuttavia, è necessario considerare che l'atomicità è
relativa al singolo oggetto. Ecco un esempio di utilizzo non thread-safe di oggetti atomici.

public class Counter I

private AtomicLong counter = new AtomicLong(O);


private AtomicLong aggregator = new AtomicLong(O);

public long addAndResult(long element) I


long result = 0;

aggregator.addAndGet(element);
counter. incrementAndGet();

return result;

public float getAvaragef) I


float result = OF;

if (counter.get() > 0) I
result = aggregator.get() / counter.get();
I

return result;
I

L'intera implementazione è assolutamente non thread-safe. Nel metodo addAndResult, sebbe-


ne ciascuno dei due oggetti, considerato singolarmente, sia incrementato atomicamente, i due
incrementi insieme non lo sono. Quindi un thread in esecuzione potrebbe essere interrotto
dopo aver incrementato il totale (aggregator) e prima dell'incremento del contatore (counter),
mentre un altro esegue il calcolo della media, con il risultato di produrre un risultato incorretto.

3.6.3 Valutare l'utilizzo delle collezioni concorrenti


Un'altra componente molto apprezzata di Java 5 sono le collezioni concorrenti
(java.util.concurrent): C o n c u r r e n t H a s h M a p , C o p y O n W r i t e A r r a y L i s t , e C o p y O n W r i t e A r r a y S e t , alle quali si
Interfaccia/classe tradizionali Collezioni concorrenti Versione Java
HashTable ConcurrentHashMap 1.5
ArraList CopyOnWriteArrayList 1.5
Set CopyOnWrlteArraySet 1.5
SortedMap (TreeMap) ConcurrentSkipListMap 1.6
SortedSet (TreeSet) ConcurrentSkipListSet 1.6

Tabella 3.1 - Le nuove collezioni per la concorrenza.

aggiungono tutta una serie di code: ConcurrentLinkedQueue, LinkedBlockingQueue, ArrayBlockingQueue,


SynchronousQueue, PriorityBlockingQueue, e DelayQueue. A queste vanno poi aggiunte le altre colle-
zioni introdotte con Java 6, come ConcurrentSkipListMap e ConcurrentSkipListSet. Si ottiene così il
seguente quadro delle corrispondenze mostrate nella tabella 3.1.
Queste classi sono state disegnate specificatamente per ottimizzare le performance dell'uti-
lizzo delle collezioni in ambienti MT. In particolare, mentre le collezioni tradizionali permetto-
no di implementare la caratteristica thread-safe in modo assolutamente pessimistico, attraverso
lock a livello dell'intera collezione, serializzando quindi l'accesso da parte di diversi thread, e
quindi penalizzando fortemente le performance in ambienti concorrenti, le collezioni dello
specifico package per la concorrenza implementano meccanismi più granulari, spesso simili
all'optimistic-locking. Si consideri l'implementazione del metodo get della classe HashMap ripor-
tata poco sotto su due colonne. Qualora questa dovesse essere utilizza in un ambiente MT,
sarebbe necessario far sì che il thread richiedente mantenga bloccato l'intero oggetto per tutta
la durata dell'invocazione del metodo, che, in questo caso per esempio, può richiedere la
scansione di intere liste di collisioni (elementi diversi che generano lo stesso codice hash). Nella
versione concorrente, invece, si prosegue eseguendo le varie letture senza alcun lock, salvo poi
eseguire la lettura bloccata qualora accada qualcosa di imprevisto, come per esempio elemento
nullo. Questa condizione può essere generata da una riorganizzazione dell'intero oggetto Map.
Ecco il metodo della classe HashMap e della classe ConcurrentHashMap.

public V get(Object key) I public V get(0bject key) I


Object k = mas/r/Vz///(key); int hash = hash(key); // throws l\IPE if key null
int hash = hash(k); return segmentFor(hash).get(key, hash);
int i = indexFor{hash, table.length); I
Entry<K,V> e = tableji];
while (true) { (the corresponding get)
if (e == null) V get(0bject key, int hash) I
return null; if (count != 0) I // read-volatile
if (e.hash == hash && eg(k, e.key)) HashEntry<K,V> e = getFirst(hash);
return e.value; while (e != null) {
e = e.next; if (e.hash == hash && key.equals(e.key)) I
V v = e.value;
if (v != null)
return v;
return readValuellnderLock(e); // recheck
I
e = e.next;
I
I
return nuli;
I

Si noti nell'implementazione del metodo get della classe C u n c u r r e n t H a S h M a p la presenza


dell'invocazione del metodo SegmentFor. Questo ritorna un oggetto di tipo Segment, che ne è
una classe annidata. In sostanza ogni segmento gestisce una porzione degli oggetti dell'intera
collezione. Ciò è necessario per implementare una strategia di lock nota con il nome di lock
striping. Ciò fa sì che, invece di gestire un solo lock per l'intera collezione, sia possibile averne
diversi ognuno per sorvegliare un ben definito segmento.
Da tener presente che il miglioramento delle prestazioni in ambiente concorrente fornito dalle
nuove collezioni non avviene del tutto gratuitamente. In particolare, proprio per garantire un'elevata
concorrenza, è stato necessario rilassare la semantica dei metodi che operano sull'intero oggetto.
Per esempio, Size. In effetti, non essendoci lock i metodi relativi all'intero oggetto finiscono per
fornire un'indicazione, non garantita dello stato. Per esempio, mentre il metodo sta leggendo il
numero di elementi di una mappa, altri thread potrebbero essere intenti ad aggiungere nuovi elementi.

3.6.7 Utilizzare i lock al posto delle sincronizzazioni


quando le prestazioni sono importanti.
Prima dell'introduzione del package della concorrenza, l'unico modo per proteggere aree cri-
tiche del codice era ricorrere all'utilizzo della sincronizzazione (synchronized).

synchronized ( ObjectLock ) I
/ / u p d a t e the o b j e c t

public synchronized doSomething() I


/ / update the object
1

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 = new ReentrantLockf);

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.

3.6.9 Sostituire Thread con Executor per maggiore controllo e flessibilità


Il modo tradizionale di eseguire i thread (Thread(myRunnable).start()) presenta una serie di limita-
zioni legate ad alcune carenze, come la mancanza di disaccoppiamento tra l'invio di una richie-
sta e la sua esecuzione, la mancanza di policy di esecuzione, di cancellazione e così via. Inoltre,
vi è il problema dell'assenza del concetto di pool. Questo può creare sia problemi in termini di
performance (la creazione di un nuovo thread richiede tempo di esecuzione e quindi una con-
tinua creazione e distruzione di questi oggetti può finire per incidere sulle prestazioni) sia di
controllo del numero di thread nonché di scalabilità.
Pertanto, in tutti quei contesti in cui sia necessario disporre di un maggiore livello di flessibi-
lità e controllo, è fortemente consigliabile ricorrere alle classi che implementano Executor. Que-
sta è una semplicissima interfaccia introdotta con Java 5 che dichiara un solo metodo:

public interface Executor [


void execute(Runnable c o m m a n d ) ;
1

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();

boolean awaitTermlnation(long timeout, TimeUnit unit)


throws InterruptedException;

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

<T> List<Future<T» invokeAII(Collection<Callable<T» tasks)


throws InterruptedException;

<T> List<Future<T» invokeAII(Collection<Callable<T» tasks, long timeout, TimeUnit unit)


throws InterruptedException;

<T> T invokeAny(Collection<Callable<T>> tasks)


throws InterruptedException, ExecutionException;

<T> T invokeAny(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)


throws InterruptedException, ExecutionException, TimeoutException;
I

Come si può notare vi è un livello di indirezione tra la proposizione di un task e la relativa


esecuzione; vi sono servizi relativi al ciclo di vita, come shutdown, isShutdown, isTerminated,
awaitTermination, e cosi via.
Le classi che implementano questa interfaccia sono ScheduledThreadPoolExecutor,
ThreadPoolExecutor a cui si aggiunge la classe astratta AbstractExecutorService.

3.7 Gestire correttamente possibili accessi concorrenti


Disegnare attentamente gli accessi concorrenti a risorse condivise è rilevante in tutti quei casi in
cui si voglia implementare un'applicazione multi-threading, ossia quando l'applicazione utente
voglia gestire diversi thread. Da notare che la JVM è multi-threading indipendentemente dalla
modalità con cui si implementano i programmi che vi dovranno funzionare. Infatti, oltre al thread
dedicato all'esecuzione del metodo main del programma applicativo, ne esistono altri (denomina-
ti daemon) demandati alla gestione di una serie di servizi di base, come il garbage collector.
La selezione del livello di lock da utilizzare, frequentemente, rappresenta un argomento de-
licato nella progettazione e implementazione di applicazioni concorrenti. Sebbene da un lato
una strategia conservatrice potrebbe evitare una serie di problemi tipici di errata sincronizza-
zione (corruzione dei dati, race conditions, comportamenti inattesi in casi non spiegabili, etc.),
dall'altro porterebbe alla generazione di un codice non particolarmente efficiente in cui il livel-
lo di concorrenza è piuttosto ridotto e, in alcuni casi addirittura alla generazione di situazioni di
dead-lock.
Ci sono tre metodi per evitare race-conditions:

1. non condividere un'area data tra più thread;


2. realizzare lo stato come un immutable;
3. sincronizzare l'accesso alle sezioni critiche.

3.7.1 Non sincronizzare metodi rientranti


I thread Java dispongono di un'area di memoria riservata per il proprio stack. Come tale, non
è accessibile da parte di altri thread. Tra gli altri dati (come per esempio l'indirizzo dell'istruzio-
ne da eseguire dopo l'esecuzione del metodo corrente), in questa area sono memorizzate le
variabili locali dei metodi, i valori dei parametri attuali dei metodi e l'eventuale valore di ritor-
no. Si ricordi che in Java i parametri sono passati by value... Chiaramente, nello stack non è
possibile memorizzare oggetti, ma solo i relativi riferimenti e pertanto, qualora sia necessario
passare un array o un altro oggetto, ciò che viene copiato nello stack è il relativo indirizzo. In
ogni modo, se all'interno di un metodo viene creato un oggetto (per esempio una stringa), il
relativo riferimento è visibile unicamente dal thread che in quel momento sta eseguendo il
metodo stesso. Metodi che utilizzano esclusivamente i parametri forniti e le variabili locali sono
definiti rientranti e come tali possono essere eseguiti, contemporaneamente, da diversi thread
senza aver bisogno di alcuna sincronizzazione.

3.7.2 Sincronizzare l'accesso a dati condivisi


Le classi Java disegnate per funzionare in modalità multi-threading devono essere progettate
per essere thread-safe\ devono funzionare correttamente qualora i relativi metodi siano eseguiti
simultaneamente da diversi thread. A tal fine è necessario identificare le aree ad accesso esclu-
sivo (aree che se accedute da più thread contemporaneamente potrebbero dar luogo a compor-
tamenti errati). Nell'accesso alle risorse di I/O queste aree sono già state analizzate e risolte
dall'implementazione delle API Java. Per esempio, la classe java.io.File prevede una serie di
metodi sincronizzati, quali:

private synchronized void writeObject(ObjectOutputStream s) throws lOException;

private synchronized void readObject(ObjectlnputStream s)


throws lOException, ClassNotFoundException

public static File createTempFile(String prefix, String suffix, File directory) throws lOException

Il metodo createTempFile possiede una sincronizzazione nell'interno della relativa


implementazione. Pertanto, nella stesura del codice è necessario identificare correttamente e
gestire conseguentemente le aree dati condivise da più thread. Questo è necessario perché la
JVM gestisce un'unica area dati (denominata heap) condivisa da tutti i thread (anche i daemon
come il Garbage Collector) destinata a memorizzare tutti gli oggetti (o meglio il valore dei
relativi attributi) gestiti dall'applicazione. Alcuni esempi di dati condivisi sono quelli scritti da
un thread che poi dovranno essere letti da altri, o viceversa dati da acquisire prodotti da altri
thread, dati di sincronizzazione, etc. Una volta identificate le aree e i dati da proteggere è
necessario stabilire il tipo di lock richiesto... Questo però è argomento di un'altra direttiva.

3.7.3 Valutare attentamente la necessità di sincronizzare i metodi


L'utilizzo della parola chiave synchronized, come lecito attendersi, ha un impatto sulle perfor-
mance, sia in termini di latenza (latency, tempo impiegato per svolgere un determinato lavoro),
sia in termini di throughput (numero di lavori eseguito nel medesimo arco temporale). Tutti gli
oggetti Java hanno, potenzialmente, un oggetto associato, denominato monitor, ed è necessario
serializzarne l'accesso da parte di diversi thread. Questo però è effettivamente utilizzato solo
quando un oggetto dichiara aree sincronizzate per mezzo della parola chiave synchronized.
Nelle versioni iniziali della, JVM, alcune statistiche evidenziavano che l'impatto di aree sin-
cronizzate poteva giungere fino ad un fattore 50. Le cose sono molto migliorare con le versioni
più recenti. Resta, tuttavia, il fatto che oltre a ridurre le performance, la sincronizzazione dei
metodi può generare colli di bottiglia e avere un effetto negativo sulla scalabilità dell'intera
applicazione. Pertanto, è necessario sincronizzare solo quando sia veramente necessario e sele-
zionare il livello opportuno di lock.

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.

3.7.4 Considerare l'utilizzo di classi di sincronizzazione


Non è infrequente il caso di implementare delle classi e di non sapere a priori se dovranno o
meno funzionare in un contesto multi-threading, o meglio ancora, di implementare classi che
dovranno funzionare in entrambi gli scenari. La strategia più frequentemente utilizzata consi-
ste nell'utilizzare un approccio sicuro e quindi all'introduzione di blocchi a mutua esclusione.
Questo, se da un lato risolve il problema, dall'altro finisce per penalizzare tutti i contesti single-
threaded. Un'ottima strategia, di frequente impiego, consiste nell'implementare la classe senza
aree sincronizzate e quindi adatta per un ambiente single-threaded, e di utilizzarne un'altra che
ne incapsuli i metodi per un funzionamento multi-threading. Un po' come avviene con le classi
Collection che divengono thread-safe con l'esecuzione del comando Collections.synchronizedXXXQ.
3.7.5 Fare attenzione al tipo di lock richiesto
Per ogni blocco di codice Java è possibile selezionare una serie di protezioni per l'accesso
concorrente, quali: nessuna, lock legato alla singola istanza, e lock statico (associato alla classe).
L'acquisizione del lock istanza da parte di un thread (ingresso in un'area synchronized) blocca
eventuali altri thread che ne desiderino eseguire metodi sincronizzati. Ciò chiaramente non
blocca tali thread dall'invocazione di metodi non sincronizzati o sincronizzati a livello di classe.
Ecco il metodo isEmpty della classe java.util.Vector.

* Tests if this vector has no c o m p o n e n t s .

* © r e t u r n < c o d e > t r u e < / c o d e > if a n d only if this vector has


no c o m p o n e n t s , that is, its size is zero;
* <code>false</code> otherwise.
*/

public synchronized boolean isEmptyf) {


return elementCount == 0;
)

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 class TestFinerLock I

// D u m m y object used f o r locking p u r p o s e s

Object lockWrite = n e w Object();

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.

3.7.6 Porre attenzione all'utilizzo della classe Iterator


per gli accessi concorrenti
Le collezioni Java 2 restituiscono oggetti di tipo Iterator implementati secondo la strategia fail-
fast. Ciò significa che se, dopo la creazione dell'oggetto Iterator, la corrispondente lista subisce
delle modifiche strutturali (aggiunta, rimozione di elementi e variazione delle dimensioni), non
ottenute attraverso l'esecuzione dei metodi propri dell'Iterator, questo lancia un'eccezione di
modifica concorrente (ConcurrentModificationException). Pertanto, qualora si verifichi una modifi-
ca strutturale concorrente, l'oggetto Iterator fallisce immediatamente e in maniera pulita (da cui
il nome della strategia), evitando di correre il rischio di generare problemi di comportamento
non deterministico difficili da individuare.
Riassumendo, problemi di modifica concorrente relativi agli Iterator, si verificano nel caso in
cui mentre un thread sta scorrendo un Iterator, un altro modifica la lista sottostante. Inoltre, non
è sempre possibile garantire questo comportamento soprattutto in presenza di metodi che ge-
stiscono le strutture non sincronizzati. Questa strategia è pertanto implementata secondo il
criterio best-effort. Qualora sia necessario navigare gli elementi di una lista in una situazione di
forte concorrenza, è necessario ricorrere a una delle seguenti alternative:

1. eseguire un lock della lista sottostante;


2. ottenere una copia "privata" degli elementi della lista.

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) ) . . . ( .

public class ErrorListManager<T> {

private List<T> errorList = Collections.synchronizedList( new ArrayList<T>() );

public boolean addlfNotPresent(T newError) {

boolean wasPresent = errorList.contains(newError);

if (IwasPresent) I
errorList.add(newError);
I

return wasPresent;
I

public List<T> getErrorList() I


return errorList;

3.7.8 Gli oggetti immutabili sono sempre thread-safe


In alcuni contesti è possibile evitare di ricorrere a dispendiosi costrutti di sincronizzazione
semplicemente realizzando oggetti immutabili, come il tipo stringa Java e gli oggetti di wrapping.
Gli oggetti immutabili sono quelli il cui stato, una volta impostato durante la costruzione, non
può più cambiare. In sostanza si tratta di classi che prevedono il passaggio dei parametri al
livello di costruttore e che non implementano metodi modificatori (setXXX).
L'implementazione di un oggetto totalmente immutabile richiede che i suoi attributi siano
dichiarati final. Ciò per evitare che questi possano essere variati attraverso l'implementazione di
una classe figlia.

3.7.9 Documentare sempre il tipo di sincronizzazione offerta


Ogniqualvolta si implementa del codice, soprattutto qualora debba essere riutilizzato in diver-
si contesti e da diverse persone, è fortemente consigliabile dichiarare esplicitamente se il codice
sia thread-safe o meno, e in caso affermativo, includere la strategia utilizzata per proteggere le
sezioni critiche del codice. Probabilmente, 0 posto migliore dove definire queste informazioni
è l'intestazione della classe.
Nel libro [JVCNPR] (probabilmente uno degli migliori sulla programmazione concorrente
in Java) viene addirittura proposta un'apposita notazione per dichiarare se la classe sia o meno
thread-safe (@ThreadSafe) ed il tipo il meccanismo utilizzato per il garantire la proprietà di thread-
safe (@GuardedBy).

3.7.10 Valutare soluzioni di tipo lock striping


La tecnica del lock striping consiste nel cercare di non bloccare l'accesso a interi oggetti ma
solo a sue parti. Questa tecnica per esempio è implementata dal CurrentHashMap dove l'intera
mappa è suddivisa in un predefinito numero di segmenti, la cui possibilità di modifica è subor-
dinata all'acquisizione di apposito lock. Ciò chiaramente risulta in un incremento di perfor-
mance e scalabilità: mentre un thread detiene il lock per un determinato segmento, gli altri
thread sono liberi di bloccare i restanti.
Capitolo
I commenti
Introduzione
Il corretto funzionamento dei sistemi implementati dovrebbere costituire un prerequisito irri-
nunciabile dell'ingegneria del software, ma pur sempre di prerequisito dovrebbe trattarsi: 0
codice dovrebbe, in teoria, funzionare comunque... Un'elevata qualità del codice, però, richie-
de il soddisfacimento di ulteriori importanti qualità come, per esempio, buona leggibilità e
manutenibilità. Pertanto, un'efficace documentazione è una caratteristica irrinunciabile di qual-
siasi software. Tutti gli sviluppatori, in qualche misura, lo sanno, ma nella pratica lavorativa non
è infrequente imbattersi in codici mal documentati, o, addirittura, non commentati.
Una delle regole fondamentali — frequentemente disattesa — per la produzione della docu-
mentazione consiste nel redigerla di pari passo con la produzione del codice. Ciò, oltre a forni-
re uno strumento di prima verifica della corretta implementazione dei vari algoritmi, facilita la
scrittura di commenti significativi ed evita l'odiosa attività, spesso e volentieri delegata al pro-
grammatore junior di turno, di scrivere frettolosamente la documentazione a posteriori,
magari nel brevissimo intervallo che precede il rilascio del codice.
Il problema sostanziale è che il tipico ciclo di vita di un qualsiasi software prevede che lo
stesso sia mantenuto, in diversi stati della sua evoluzione, da personale diverso dagli stessi
autori; e si tratta spesso di personale che non era presente nelle fasi iniziali di progettazione.
Proprio per questo motivo, il codice deve possedere la proprietà di essere compreso, in ogni
momento, sia dallo stesso autore sia da persone estranee al suo sviluppo. Purtroppo, questa
proprietà è spesso disattesa: gli autori stessi del software si trovano in difficoltà nel comprende-
re, e quindi nel modificare, parti di programma sviluppate da loro stessi, per via di una docu-
mentazione carente o per la mancata applicazione di standard di qualità.
Il monito è che software non ben documentati, e quindi poco leggibili e difficilmente
manutenibili, presentano notevoli problemi di riutilizzo e tendono a far nascere l'irrefrenabile
desiderio di riscrittura nel personale addetto alla relativa manutenzione: non vi è alcuna virtù
in un software difficilmente modificabile.
Come lecito attendersi, il linguaggio preso come riferimento è Java, per quanto, la maggior
parte delle direttive proposte mantenga la sua validità anche nell'ambito di diversi linguaggi di
programmazione. Questo è il caso anche per la documentazione JavaDoc: nessuno vieta né di
utilizzarne un'appropriata versione per linguaggi diversi da Java, né di scriverne una simile.

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).

4.1.1 Produrre la documentazione contemporaneamente


all'attività di codifica
Si tratta probabilmente di una delle principali regole per l'attività di documentazione e proba-
bilmente anche di una delle più disattese. Scrivere la documentazione in linea con la program-
mazione permette di ottenere una serie di vantaggi, quali:

• 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.

4.1.2 Mantenere i commenti aggiornati


Gli sviluppatori esperti redigono le prime versioni del codice in modo assolutamente profes-
sionale: il codice è chiaro, leggibile, ben documentato etc. Non è infrequente però il caso in cui,
man mano che il codice subisce delle modifiche, la conseguente attività di aggiornamento dei
commenti tenda ad essere trascurata. Ciò è particolarmente ricorrente a ridosso della data di
consegna. Il risultato è la presenza di commenti non aggiornati e spesso contraddittori. Ciò è
fonte di dannosa confusione che, in alcuni casi, finisce addirittura con il fuorviare e confondere
il personale demandato alla manutenzione e quindi con il ridurre drasticamente il livello di
manutenibilità del codice.

4.1.3 Scriveve commenti chiari e concisi


L'obiettivo dei commenti è di far comprendere l'intero codice prodotto sia allo stesso autore
sia a persone estranee alla relativa implementazione. Pertanto è opportuno scrivere commenti
concisi, semplici e chiari. Commenti troppo prolissi tendono a ridurre la leggibilità del codice.
Tipicamente si preferisce scrivere i commenti utilizzando la terza persona singolare.
Si supponga di disporre di una lista ordinata di codici di determinati prodotti e di basare il
metodo di ricerca su una determinata versione di un algoritmo di ricerca dicotomica (detta
anche binaria). In questo caso nella documentazione è sufficiente riportare il nome dell'algoritmo
ed eventualmente una brevissima spiegazione, evitando però di dilungarsi nell'esporre i detta-
gli di tale algoritmo per il quale esistono specifici trattati.

4.1.4 Evitare le decorazioni


Il codice prodotto deve essere commentato e particolare attenzione va rivolta alle porzioni di
codice più complesse. I commenti, come visto in precedenza, devono essere professionali, chia-
ri e concisi evitando inutili decorazioni e inappropriati umorismi. Questi, oltre a richiedere
tempo che potrebbe essere investito in maniera migliore, tendono a diminuire la leggibilità del
codice, in quanto ne diminuiscono pulizia e chiarezza, si prestano ad essere fraintesi e spesso
possono irritare il lettore del codice impegnato a comprenderne il senso. Ecco un esempio di
metodo printStackTrace (classe Throwable) appositamente mal commentato.

* Prints this throwable and its backtrace to the specified print stream.

* @param s <code>PrintStream</code> to use for output

*/
public void printStackTracefPrintStream s) I

/ \ // * It Is necessary to synchronize the code to make it thread safe


/ / * * * * * * * * * * " * • " * • • " • " * * * * * * * * *

synchronized (s) I
s.println(this);

/'/ * Prints all trace elements present in the stack

StackTraceElement[] trace = getOurStackTrace();


for (int i=0; i < trace.length; I++)
s.println("\tat" + tracefi]);

n ..................................
// * Prints the cause, if present

Throwable ourCause = getCause();


If (ourCause != null)
ourCause.printStackTraceAsCause(s, trace);
I

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.

4.1.6 Porre attenzione alle situazioni in cui è difficile commentare il codice


Le situazioni in cui risulti difficile commentare efficacemente e semplicemente il codice pro-
dotto dovrebbero essere oggetto di particolare attenzione. Infatti spesso situazioni del genere
sono dovute a codice non ben scritto o paradossalmente non ben compreso.

4.1.7 Una buona documentazione inizia dal codice


È appena il caso di ricordare che una buona documentazione inizia dal codice; pertanto, è
necessario investire nella leggibilità fin dalla fase di codifica.
4.2 Scrivere i commenti JavaDoc
JavaDoc è uno degli strumenti molto apprezzati inclusi nel JDK fin dalle sue prime versioni. In
particolare, permette di organizzare razionalmente e produrre (di default) un insieme di pagi-
ne HTML contenenti la documentazione del codice prodotto. JavaDoc è sicuramente uno
degli strumenti di maggiore successo del J D K tanto che anche altri linguaggi sono stati dotati di
versioni simili di questa utility.
Comunque, nel redigere la documentazione JavaDoc è necessario porre attenzione a un
insieme di regole che, se non seguite, potrebbero portare a risultati indesiderati.

4.2.1 Investire nella documentazione JavaDoc


Anche se questa regola può sembrare ormai acquisita, è sempre opportuno ricordare che una
buona documentazione JavaDoc è caratteristica fondamentale di codici Java di buona qualità.
Questa documentazione fornisce il punto di partenza che i programmatori consultano
ogniqualvolta hanno a che fare con del nuovo codice.
Inoltre, la maggior parte dei software IDE hanno servizi avanzati per assistere la produzione
di commenti Javadoc. Quindi con minimo sforzo è possibile ottenere ottimi risultati.

4.2.2 Applicare la struttura dei commenti doc


I commenti doc devono essere specificati immediatamente prima dell'elemento (classe,
interfaccia, metodo, attributo o particolare porzione di codice) di cui forniscono la documenta-
zione. La struttura prevede una concisa e completa descrizione dell'elemento, eventualmente,
seguita da un insieme di tag. La descrizione iniziale dovrebbe essere costituita da una prima
riga che descriva, quanto più sinteticamente e completamente possibile, l'elemento al fine di
fornirne un'immediata comprensione. Questa, tipicamente, è seguita da altre necessarie per
fornire ulteriori informazioni. JavaDoc considera terminata la prima riga quando incontra un
carattere di nuova riga o non appena incontra una sequenza di tipo: carattere punto e uno
spazio oppure carattere punto e un tab. Pertanto bisogna porre attenzione ad eventuali contra-
zioni, abbreviazioni etc. con punto inserite nella prima riga che potrebbero, involontariamente,
spezzare prematuramente il commento.
Qualora un commento preveda più paragrafi, questi vanno separati per mezzo di una linea
che contenga unicamente l'asterisco iniziale e il tag HTML <p> (cfr. quarta riga del commento
riportato di seguito). Da tener presente che dalla versione 1.4 di JavaDoc, non è più necessario
riportare il carattere asterisco iniziale nelle linee interne del commento. Per esempio si conside-
ri la documentazione JavaDoc fornita con la classe System.

" 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.

' ¡Sauthor unascribed


* @version 1.149. 06/02/04
* ©sirice JDK1.0
7
public final class System I

4.2.3 Scrivere i commenti doc utilizzando i tag HTML di formattazione


Il comportamento di default dell'utility JavaDoc, utilizzato nella stragrande maggioranza dei
casi, consiste nell'inserire i commenti prelevati dal codice in una serie di pagine HTML oppor-
tunamente organizzate. Quindi, a meno di voler ridefinire il comportamento di default di JavaDoc
(a tal proposito è necessario utilizzare la relativa API), quando si redigono commenti doc, è
necessario tenere a mente che questi verranno inseriti all'interno di una pagina HTML. Pertan-
to è opportuno porre attenzione all'utilizzo, nei commenti JavaDoc, dei caratteri considerati
speciali per l'HTML. Per esempio, le parentesi angolari < > vanno sostituite, rispettivamente,
dalle stringhe &lt e &gt. La stessa sorte spetta al carattere & che va sostituito con la stringa &amp,
e così via. Inoltre, per assicurare che la formattazione utilizzata nello scrivere i vari commenti sia
rispettata nelle pagine H T M L generate, è opportuno ricorrere all'utilizzo di tag HTML quali
<pre>, <b>, <i>, e così via. Una trattazione di tali tag è riportata nell'Appendice B.
Da tenere presente che, qualora si intenda scrivere il proprio doclet per la produzione di
documentazione JavaDoc, questo si dovrà far carico di interpretare correttamente i tag HTML
inclusi nella documentazione. Per esempio, si consideri il seguente commento attinto dalla
classe System relativo alle proprietà di sistema, riportato nel listato seguente.

/**

* System properties. The following properties are guaranteed to be defined:


* <dl>
* <dt>java.version <dd>Java version number
* <dt>java.vendor <dd>Java vendor specific string
* <dt>java.vendor.url <dd>Java vendor URL
* <dt>java.home <dd>Java installation directory
* <dt>java.class.version <dd>Java class version number
* <dt>java.class.path <dd>Java classpath
* <dt>os.name <dd>Operating System Name
" <dt>os.arch <dd>Operating System Architecture
* <dt>os.version <dd>Operating System Version
* <dt>file.separator <dd>Flle separator ("/" on Unix)
* <dt>path. separator <dd>Path separator (":" on Unix)
" <dt>line.separator <dd>Line separator ("\n" on Unix)
* <dt>user.name <dd>User account name
* <dt>user.home <dd>User home directory
* <dt>user.dir <dd>User's current working directory
* </dl>
*/

4.2.4 Utilizzare il tag <code>


Il tag <C0de> deve essere utilizzato per evidenziare elementi del codice. Pertanto va usato nei
casi in cui il commento contenga parole chiave Java, nomi di package, nomi di classi, nomi di
metodi, nomi di interfacce, attributi e argomenti e porzioni di codice. Per esempio si consideri
il JavaDoc relativo al metodo clearProperty dalla classe System.

/" *

* Removes the s y s t e m property indicated by the specified key.

' <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>

* ©param key the name of the s y s t e m property to be r e m o v e d ,


* ©return the previous string value of the s y s t e m property,
or <code>null</code> if there w a s no property w i t h that key.

' © e x c e p t i o n SecurityException if a security m a n a g e r exists and its


<code>checkPropertyAccess</code> m e t h o d doesn't allow
access to the specified s y s t e m property.
* © e x c e p t i o n NullPointerException if <code>key</code> is
<code>null</code>.
* © e x c e p t i o n IHegalArgumentException if <code>key</code> is empty.
* ©see #getProperty
* ©see #setProperty
" ©see java.util.Properties
* ©see java.lang.SecurityException
* ©see java.lang.SecurityManager#checkPropertiesAccess()

" ©since 1.5

*/
public static String clearProperty(String key) I

4.2.5 Evitare l'utilizzo di tag HTML di struttura


Come visto in precedenza è possibile e consigliato ricorrere all'utilizzo di alcuni tag HTML di
formattazione per aumentare il livello di chiarezza dei commenti doc. Al contempo è fortemente
consigliato che il ricorso a questi tag non includa elementi di struttura, come <HTML>, <HEAD>,
<H1>, etc., al fine di non interferire con la preesistente struttura della pagina HTML utilizzata
dall'applicazione JavaDoc; altrimenti si corre il forte rischio di generare pagine HTML assoluta-
mente non leggibili (per esempio potrebbero comparire frame riportati nei posti più impensabili),
o addirittura non riproducibili dai vari browser. Questa regola prevede l'eccezione della docu-
mentazione doc per i package, in quanto la preparazione dell'intera pagina è demandata al pro-
grammatore e quindi è assolutamente necessario ricorrere a tag HTML di struttura.

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.

* Sets the s y s t e m properties t o the < c o d e > P r o p e r t i e s < / c o d e >


* argument.

* <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>

* The a r g u m e n t b e c o m e s the c u r r e n t set of s y s t e m properties f o r use


* by the |@link #getProperty(String)l m e t h o d . If the a r g u m e n t is
* <code>null</code>. then the c u r r e n t set of s y s t e m properties is
* forgotten.

* @param props the new s y s t e m properties.


* © e x c e p t i o n SecurityExceptlon If a security manager exists and 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 doesn't allow access
to the s y s t e m properties.
* @see IgetProperties
* @see java.util.Properties
* @see java.lang.SecurityExceptlon
* @see java.lang.SecurityManager#checkPropertiesAccess()
*/
public static void setProperties(Properties props) I

4.2.6 Ereditare i commenti JavaDoc


L'utility JavaDoc, in alcuni casi ben definiti, è in grado di duplicare ("ereditare") la documen-
tazione doc dei metodi evitando al programmatore un'inutile riscrittura. Anche se ciò potreb-
be essere evitato con un semplice copia e incolla, rimarrebbe il problema di dover mantenere
aggiornate le ripetizione dello stesso commento doc.
JavaDoc è in grado di "ereditare" il JavaDoc dei metodi nelle seguenti situazioni:

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.

4.2.7 Scrivere i commenti JavaDoc per tutti gli elementi


Il commento doc va scritto per tutti gli elementi del proprio codice. In particolare è importante
documentare correttamente classi, interfacce, metodi e attributi. La relativa descrizione è ri-
portata nelle sezioni di pertinenza illustrate di seguito.

4.3 Valutare la possibilità di inserire


la documentazione al livello di package
Documentare i package è un'attività non strettamente obbligatoria che tuttavia fornisce impor-
tanti informazioni circa le classi e le interfacce contenute. Questa documentazione è particolar-
mente utile sia in tutti quei casi in cui il criterio di aggregazione di classi e interfacce all'interno
di un medesimo package non sia particolarmente intuitivo, e sia qualora sia necessario include-
re informazioni relative all'intero package. Pertanto, questa documentazione offre la possibili-
tà di concentrare informazioni comuni a diverse classi e interfacce che, altrimenti, dovrebbero
essere ripetute nei vari elementi creando problemi di manutenibilità.
Un altro elemento da tenere in considerazione è relativo al fatto che, alla presenza di nuove
librerie e nuovo codice da apprendere, tipicamente, gli sviluppatori avviano lo studio proprio a
partire da queste informazioni poiché permettono di acquisire un'iniziale cognizione del ruolo
e delle responsabilità dell'intero package, delle classi contenute, etc.

4.2.1 Utilizzare il formato standard di documentazione dei package


Con la versione 1.2 dell'utility JavaDoc è possibile inserire documentazione a livello di package
che poi JavaDoc stesso è in grado di includere nella documentazione del codice generata. Queste
informazioni devono essere incluse in un file H T M L di nome fisso package.html inserito
nell'apposito package insieme a classi e interfacce contenute. Il comportamento di JavaDoc consiste
nel prelevare tutte le informazioni contenute tra i tag <body> e </b0dy>. I tag JavaDoc utilizzabili
in questa documentazione sono @see, @link, @deprecated, @since.

Il template suggerito dalla Sun prevede le seguenti sezioni:

1. l'immancabile copyright dell'azienda;


2. commenti generali (tipicamente riportati subito dopo il tag HTML body), dove inserire
una breve descrizione del package e dei vari servizi offerti;
3. documentazione specifica del package, tipicamente composta da:
a. specifiche relative all'intero package; per esempio nel pacakge AWT sono riportate
specifiche relative all'interazione del framework con i vari sistemi operativi;
b. direttive per ulteriori tool di manipolazione del testo, come per esempio FrameMaker;
c. riferimenti specifici;
4. riferimenti ad altra documentazione esterna, come articoli, libri, etc.
Di seguito è riportato il listato HTML proposto dalla Sun come template per la documenta-
zione dei package Java.

<!D0CTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">


<html>
<head>
<!—

@(#)package.html 1.60 98/01/27

Copyright (c) <anno copyright <nome azienda>


All Rights Reserved

This software is the confidential and proprietary information of <nome azienda>


("Confidential Information"). You shall not disclose such Confidential Information
and shall use it only in accordance with the terms of the license agreement
you entered into with <nome azienda>.
—>

</head>
cbody bgcolor="white">

# # # # # THIS IS THE TEMPLATE FOR THE PACKAGE DOC COMMENTS. #####


# # # # # TYPE YOUR PACKAGE COMMENTS HERE. BEGIN WITH A #####
##### ONE-SENTENCE SUMMARY STARTING WITH A VERB LIKE: #####
Provides for....

<h2>Package Specification</h2>

##### FILL IN ANY SPECS NEEDED BY JAVA COMPATIBILITY KIT # # # # #


<ul>
< l i x a href="">##### REFER TO ANY FRAMEMAKER SPECIFICATION HERE #####</a>
</ul>

<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>

< ! — Put @see and @since tags down here. — >

</body>
</html>
II seguente listato riporta un esempio di utilizzo della documentazione doc al livello di package.

<!D0CTYPE H T M L PUBLIC "-//W3C//DTD H T M L 3.2 Flnal//EN">


<html>
<head>

@(#)package.html 1.7 0 4 / 0 6 / 1 7

Copyright 2004 Sun Microsystems, Inc. All rights reserved.


SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
— >

</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.

<LI> <l>Problem diagnosis by field service engineers</l>. The logging information


used by field service engineers may be considerably more complex and
verbose than that required by system administrators. Typically such information
will require extra logging within particular subsystems.
</OL>
</P>
The key elements of this package include:
<UL>
<LI> <l>Logger</l>: The main entity on w h i c h applications make
logging calls. A Logger object is used to log messages
for a specific system or application
component.
<LI> <l>LogRecord</l>: Used to pass logging requests between the logging
framework and individual log handlers.
<LI> <l>Handler</l>: Exports LogRecord objects to a variety of destinations
including m e m o r y , output streams, consoles, files, and sockets.
A variety of Handler subclasses exist for this purpose. Additional Handlers
may be developed by third parties and delivered on top of the core platform.
<U> <l>Level</l>: Defines a set of standard logging levels that can be used
to control logging output. Programs can be configured to output logging
for some levels while ignoring output for others.
<LI> <l>Filter</l>: Provides fine-grained control over what gets logged,
beyond the control provided by log levels. The logging APIs support a general-purpose
filter mechanism that allows application code to attach arbitrary filters to
control logging output.

<U> <l>Formatter</l>: Provides support for formatting LogRecord objects. This


package includes two formatters. SimpleFormatter and
XMLFormatter, for formatting log records in plain text
or XML respectively. As with Handlers, additional Formatters
may be developed by third parties.
</UL>
<P>
The Logging APIs offer both static and dynamic configuration control.
Static control enables field service staff to set up a particular configuration and then re-launch the
application with the new logging settings. Dynamic control allows for updates to the
logging configuration within a currently running program. The APIs also allow for logging to be
enabled or disabled for different functional areas of the system. For example,
a field service engineer might be interested in tracing all AWT events, but might have no interest In
socket events or memory management.
</P>

<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>

< ! — Put @see and @since tags down here. — >


(«sirice 1.4

</body>
</html>

4.4 Commentare correttamente le classi


La naturale tendenza del disegno OO consiste nel generare una serie di classi opportunamente
relazionate tra loro. Commentare correttamente le classi permette di capirne il ruolo, le re-
sponsabilità e di inserirle rapidamente nel relativo contesto. Da tener presente che non sempre
lo studio di un'API richiede di analizzare tutte le classi e che, anche qualora ciò sia necessario,
la documentazione a livello di classe aiuta a concentrarsi, in ogni momento, su un numero
limitato di aspetti. Per esempio, mentre si analizza una classe, è possibile, temporaneamente,
studiare esclusivamente la descrizione generale delle altre classi relazionate, senza dover neces-
sariamente scendere, fin da subito, nei relativi dettagli.

4.4.1 Includere sempre la dichiarazione del copyright


Tutti i codici sorgenti dovrebbero essere dotati di una sezione iniziale in cui sono specificati i
termini di copyright. Questa parte deve essere riportata a partire dalla prima riga.
L'esempio classico è riportato nel listato seguente, ove le stringhe <anno copyright e <nome
azienda>, come chiaramente indicato dai relativi nomi, vanno sostituiti, rispettivamente, con
l'anno del copyright ed il nome dell'azienda. Per esempio: 2008 e MokaByte Srl.

/'

Copyright (c) <anno copyright <nome azienda>


Ali Rlghts Reserved

" 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

4.4.2 Includere sempre un'intestazione per classi e interfacce


Ogni classe e interfaccia, secondo anche i dettami dei commenti JavaDoc [SJAVCC], deve
essere introdotta da un'opportuna intestazione (header in Inglese) che riporti una breve descri-
zione della classe, i suoi ruoli, le principali responsabilità, l'indicazione che specifichi se si tratti
o meno di una classe persistente, la versione del JDK di riferimento, la lista degli autori, la
versione e la data dell'ultimo aggiornamento ed eventuali riferimenti ad ulteriore documenta-
zione, classi ed interfacce relazionate alla presente.
Per classi 0 cui utilizzo possa non risultare immediato, è inoltre consigliato riportare alcuni
esempi di utilizzo in termini di codice. Un'altra buona norma prevede di riportare una breve
descrizione di eventuali problemi, limitazioni, etc. della classe. Infine, è da notare che dalla
versione Tiger (JDK 1.2.5), i linguaggio Java è stato (finalmente) dotato del concetto di classi
template, denominato generics. Questo fa sì che si possano avere classi parametriche. Pertanto,
in questo caso, l'header deve riportare anche la descrizione dei parametri della classe (@param).
Un esempio di tale intestazione è mostrata nel listato seguente.

* dnserire qui una breve descrizione, possibilmente di una riga >


" <inserire ulteriori informazioni >
' <P>
* <t»ResponsibiNties:</t»<ul>
* <lixinserire qui l'elenco delle principali responsabilità della classe>
* </ul>
" <p><code>
* <esempi di codice>
* </code>

* <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

' @see <altre classi/interfacce relazionate>

Ed ecco un frammento dell'header della classe java.util.concurrent.Future.

/ "

* A <tt>Future</tt> represents the result of an asynchronous


* computation. Methods are provided to check if the computation is
* complete, to wait for its completion, and to retrieve the result ol
* the computation. The result can only be retrieved using method
* <tt>get</tt> when the computation has completed, blocking if
" necessary until it is ready. Cancellation is performed by the
* <tt>cancel</tt> method. Additional methods are provided to
* determine if the task completed normally or was cancelled. Once a
* computation has completed, the computation cannot be cancelled.
" If you would like to use a <tt>Future</tt> for the sake
* of cancellability but not provide a usable result, you can
" declare types of the form <tt>Future&lt;?&gt;</tt> and
* return <tt>null</tt> as a result of the underlying task.

* <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> (

4.4.3 Non inserire la "history"


Alcuni testi di informatica suggeriscono di aggiungere nell'intestazione di classi e interfacce
una breve storia inerente i vari processi di modifica subiti. Questa dovrebbe prevedere una lista
di linee aventi un formato del tipo: data, autore, modifica. Per esempio:

1 0 / M a r / 2 0 0 8 - L.V.T. - Introdotto metodo per il riordino automatico degli elementi della lista.

Sebbene si tratti di informazioni indubbiamente utili e interessanti, si ritiene che il codice


non sia il luogo più opportuno per memorizzarle. In particolare, queste informazioni dovreb-
bero essere di pertinenza di sistemi di controllo dei sorgenti (source control) quali: CVS, IBM
Rational Clear Case, Ms Source Safe, etc. La presenza di queste informazioni, dopo qualche
iterazione del sistema e i conseguenti aggiornamenti del codice, tende a diventare notevole e a
rendere il codice più pesante e meno chiaro. Inoltre, superate le cinque, sei righe, queste infor-
mazioni, insieme alle altre presenti nell'intestazione delle classi ed interfacce, tendono ad essere
sistematicamente tralasciate e quindi a divenire meri dati.

4.4.4 Commentare chiaramente problemi e limitazioni del codice


Alcune volte accade che l'implementazione di una classe o interfaccia presenti problemi o
limitazioni la cui risoluzione non sia possibile o in assoluto, magari per dipendenze dall'hardware
o da software fornito da terze parti, o semplicemente in quel momento per una serie di motiva-
zioni. Alcuni classici esempi sono dati dallo scenario in cui una grande quantità di codice di-
penda da quello da modificare e pertanto, modifiche a quest'ultimo produrrebbero un effetto
domino, oppure dalla presenza di dipendenze da altro codice proprietario, oppure da situazio-
ni in cui la soluzione dei problemi e/o limitazioni non sia d'interesse per la versione attuale del
sistema, magari per mancanza di tempo e/ budget. Un ultimo esempio abbastanza ricorrente è
dato dal fatto che la classe non sia thread-safe per scelta dello sviluppatore. Comunque sia, in
questi casi è necessario documentare dettagliatamente questi problemi e limitazioni. Il punto
migliore dove includere queste informazioni è nell'intestazione della classe o interfaccia (cfr.
punto precedente). Documentare i problemi di una classe è particolarmente importante per il
riutilizzo della stessa, in quanto fornisce agli sviluppatori una serie di informazioni utili per
prendere decisioni, qualora sia possibile o meno procedere con il riutilizzo del codice. Inoltre,
è molto utile per la produzione di versioni successive del sistema e, in taluni casi, anche per la
fase di test. Ecco parte della documentazione della classe java.sql.Timestamp.

' <P><B>l\lote:</'B> This type is a c o m p o s i t e of a < c o d e > j a v a . u t i l . D a t e < / c o d e > and a


* separate n a n o s e c o n d s value. Only integral s e c o n d s are stored in the
* < c o d e > j a v a . u t i l . D a t e < / c o d e > c o m p o n e n t . The fractional s e c o n d s - the nanos - are
' separate. The < c o d e > T i m e s t a m p . e q u a l s ( O b j e c t ) < / c o d e > m e t h o d never r e t u r n s
* < c o d e > t r u e < / c o d e > w h e n passed a value of type <code>java.util Date</code>
' because the n a n o s c o m p o n e n t of a date is u n k n o w n .
' A s a result, the < c o d e > T i m e s t a m p . e q u a l s ( O b | e c t ) < / ' c o d e >
" m e t h o d is not s y m m e t r i c w i t h respect to the
* <code>java.util.Date.equals(Object)</code>
* m e t h o d . Also, the < c o d e > h a s h c o d e < / c o d e > m e t h o d uses the u n d e r l y i n g
" <code>java.util.Date</code>
" I m p l e m e n t a t i o n and therefore does not Include nanos in its c o m p u t a t i o n .
' <P>
* Due to the differences b e t w e e n the < c o d e > T i m e s t a m p < / c o d e > class
* and the < c o d e > j a v a . u t i l . D a t e < / c o d e >
* class m e n t i o n e d above, it is r e c o m m e n d e d lhat code not v i e w
* < c o d e > T i m e s t a m p < / c o d e > v a l u e s generically as an instance ol
* <code>java.util.Date</code>. The
* inheritance relationship b e t w e e n < c o d e > T i m e s t a m p < / c o d e >
* a n d < c o d e > j a v a . u t i l . D a t e < / c o d e > really
* denotes i m p l e m e n t a t i o n inheritance, and not type inheritance.

4.4.5 Commentare l'ordine di esecuzione


Non è infrequente il caso in cui l'implementazione di un metodo o i servizi offerti da una
determinata classe debbano essere eseguiti rispettando un ben definito ordine non immediata-
mente comprensibile. In questi casi è fortemente consigliato documentare attentamente tale
ordine di esecuzione. Nel caso ci si riferisca all'implementazione di un metodo, queste informa-
zioni sono necessarie solo al personale addetto alla relativa manutenzione e quindi possono
essere inserite nell'intestazione del metodo e/o all'interno del metodo stesso. Qualora invece si
voglia documentare l'ordine di esecuzione dei metodi di una classe, (consistentemente con
quando riportato al punto precedente) è necessario riportare tali informazioni nell'intestazione
della classe ed, eventualmente, inserire un rimando nell'intestazione dei vari metodi.
4.4.6 Commentare eventuali scelte/soluzioni non chiare
Nell'implementare una classe, un metodo o addirittura nella definizione della firma dei metodi
che costituiscono un'interfaccia, non è infrequente il caso in cui si ricorra a soluzioni che ini-
zialmente potrebbero sembrare poco chiare o addirittura errate ma che, invece, sono il frutto
di un attento studio e di una lunga valutazione. Tali scelte, per esempio, potrebbero essere
relative all'utilizzo di specifiche strutture dati invece di altre, alla concorrenza, alla gestione
delle eccezioni, alla visibilità di taluni metodi, e così via. In tutti questi casi è opportuno docu-
mentare chiaramente le soluzioni adottate incluse le relative motivazioni. Questa documenta-
zione è essenziale per futura memoria, per non rischiare ingiustificate riscritture del codice, per
evitarne un cattivo uso, per non ripetere dibattiti avvenuti in precedenza, e così via.

4.4.7 Commentare eventuali effetti collaterali a livello di sistema


Spesso l'esecuzione dei metodi di una classe genera effetti collaterali, non ovvi, all'interno del
sistema. In questi casi è opportuno documentare chiaramente tali effetti nell'intestazione della
classe.

4.4.8 Documentare tutti i metodi


Una buona tecnica di documentazione del codice, rinforzata dalla convenzione JavaDoc
[SJAVCC], consiste nell'introdurre tutti i metodi con una ben definita sezione di commento
che includa una breve descrizione del metodo (una riga, seguita da un commento più lungo,
come illustrato nella sezione JavaDoc), un'eventuale descrizione dei parametri formali, del va-
lore di ritorno se presente, delle eventuali eccezioni e, qualora sia il caso, l'indicazione della
deprecazione, come indicato dal listato seguente:

* <Descrizione del metodo sintetica>


' <P>
" <Ulteriori informazioni relative al metodo sintetica>

* @param <nome del parametro <descrizione del parametro


" @param <nome del parametro> descrizione del parametro>
* ©return <valore di ritorno se presente>
* @see eventuali rimandi a classi/interface contenenti informazioni utili>
* @throws <classe eccezìone> <spiegazione delle cause che la generano>
* @throws <classe eccezione> <spiegazione delle cause che la generano>
* ©deprecateci <descrizione della deprecaizone>
'I

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.

" « i p a r a m record the L o g R e c o r d to be published


7
public void log(LogRecord record) I

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à.

4.4.9 Documentare gli attributi


Gli attributi, specie quelli di classe tendono a sfuggire all'attenzione dei programmatori che, a
volte, ne trascurano la documentazione. Invece, anche questa assume un ruolo di primaria
importanza specie quando il relativo utilizzo non è immediato, e per la corretta comprensione
di algoritmi complessi. Come di consueto, è importante riportare una documentazione chiara e
concisa che spieghi l'utilizzo dell'attributo. In casi particolarmente complessi o poco intuitivi, è
consigliabile anche mostrare anche alcuni esempi di utilizzo.
Per gli attributi di classe (il cui campo d'azione è l'intera classe) è consigliabile ricorrere a
una documentazione in stile JavaDoc, specie per attributi la cui visibilità non sia privata. Per
gli attributi dei metodi (comunemente denominati variabili) è invece sufficiente un commen-
to in linea.
Al fine di mantenere il codice pulito e chiaro è consigliabile riportare una dichiarazione per
riga. Di seguito è mostrata la documentazione della variabile boolean della classe java.lang.Boolean
e mostrato un attributo dotato di documentazione JavaDoc.

/ "

* The value of the Boolean.


* ©serial

private final boolean value;

Il seguente è invece un commento in linea relativo all'attributo C presente nell'implementazione


del metodo put(E o) della classe java.util.concurrent.LinkedBlockingQueue.

// Note: c o n v e n t i o n in ali put/take/etc is to prese!


// locai var h o l d i n g c o u n t negative to indicate failure unless set.
int c = -1;

4.5 Commentare attentamente l'implementazione


Il linguaggio di programmazione Java prevede ben tre differenti stili di documentazione del
codice (anche questa ricchezza di stili è l'ennesima dimostrazione dell'importanza attribuita
alla documentazione del software), che sono:
* commenti JavaDoc: questi, come visto in precedenza, sono riconoscibili in quanto rac-
chiusi tra i delimitatori /** e */ (/** commento 7);
* commenti in stile linguaggio C caratterizzati dal formato /* commento */;
* c o m m e n t o di singola linea c h e consiste nel considerare c o m e c o m m e n t o tutto il testo c h e segue
due caratteri barretta obliqua (doppio slash)-. Il c o m m e n t o .

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.

4.5.1 Documentare efficacemente il codice


I commenti presenti all'interno dei metodi sono molto importanti in quanto documentano
l'implementazione vera e propria. Per questo motivo è necessario porre particolare attenzione
alle consuete regole: scrivere commenti concisi e, allo stesso tempo, completi, porsi sempre nei
panni di uno sviluppatore completamente all'oscuro del codice e delle relative decisioni e ricor-
darsi di indicare le motivazioni alla base del codice (il famoso "perché").
Per esempio si consideri l'implementazione del metodo writeObject (utilizzato per serializzare
lo stato dell'oggetto) della classe java.util.ArrayList.

* Saves the state of the <tt>ArrayList</tt> instance to a stream (that


* is. serialize it).

* ©pararti s object o u t p u t stream used to serialize the object


* ©throws lOException IO p r o b l e m s o c c u r r e d d u r i n g the serialization
* © s e r i a l D a t a The length of the array backing the <tt>ArrayList</tt>
instance is emitted (int). f o l l o w e d by all of its elements
(each an <tt>Object</tt>) in the proper order.
*/

private void writeObject(java.io.ObjectOutputStream s)


throws java.io.lOException |

// Write out element count, and any hidden stuff


s.defaultWriteObject();

// Write out array length


s.writelnt(elementData.length);

Write out ali elemenls In the proper order


lor (int i=0; ksize; i++)
s.writeObject(elementData[i]);
I

4.5.2 Ricordarsi di rimuovere parti di codice non più utilizzate


Spesso, durante la messa a punto del codice prodotto o a seguito di operazioni di manutenzio-
ne dello stesso, si procede con l'isolare determinate porzioni di codice escludendone altre
racchiudendole in appositi commenti. Sebbene questa sia una prassi conveniente, può acca-
dere di lasciare commentate delle porzioni di codice perché erronee o non più necessarie. In
questi casi è opportuno rivedere il codice e rimuovere tali parti prima di consolidare il codice
(di farne il check-in). Infatti, porzioni di sorgente racchiuse in un commento (e non parte
integrante della documentazione), e quindi non più utilizzate, finiscono per confondere la
lettura del codice stesso e tendono a creare amletici interrogativi nel personale addetto alla
manutenzione.
Se proprio, per qualche importante motivo (per esempio un servizio da utilizzare in una
versione futura) non si voglia perdere tale porzione di codice, allora è importante riportare una
descrizione che spieghi sia il motivo per cui il codice è stato commentato, sia la data e l'autore
della trasformazione in commento.

4.5.3 Documentare efficacemente cicli annidati


Un codice complesso che includa diversi cicli annidati, ognuno caratterizzato da specifiche
comparazioni, raramente risulta leggibile. Pertanto, in circostanze del genere dovrebbe essere
naturale valutare la possibilità di aumentarne la leggibilità, magari delegando porzioni di codi-
ce a opportuni metodi privati. Qualora ciò non sia possibile, allora è fondamentale commenta-
re attentamente il codice, evidenziando opportunamente i vari cicli. Spesso non è sufficiente
delegare questa responsabilità alla sola indentazione del codice. In presenza di diversi cicli
annidati, quindi è consigliabile commentare attentamente la chiusura di ciascun ciclo con com-
menti del tipo 1 // end of if, 1 // end of while, e così via.
L'esempio del listato seguente è tratto dall'implementazione del metodo indexOf della classe
AbstractList.

public int index0f(0bject o) I


Listlterator<E> e = listlterator();
il (o==null) (
w h i l e (e.hasNext()) I
il (e.next() == nuli) I
return e.previouslndex();
) ;•' end of if
I '7 end of while
) else I
w h i l e (e.hasNextQ) {
it ( o.equals(e.next()) ) I
return e.previouslndex();
I // end of if
I /'/' end of while
I // end of else
return -1 ;

4.5.4 Documentare eventuali effetti collaterali


Al fine di produrre un buon livello di documentazione è necessario documentare chiaramente
eventuali effetti collaterali, non ovvi, generati dall'esecuzione dei metodi. Come di consueto
non è necessario documentare casi lampanti come, per esempio, i metodi modificatori (setXXX)
disegnati specificatamente per generare un effetto collaterale (modificare il valore di una o più
attributi).
Si consideri il metodo riportato nel listato seguente. Questo si occupa si reimpostare la posi-
zione del Buffer. Come riportato nella documentazione doc, questa operazione può avere effet-
to sull'attributo mark. Si tratta appunto di un necessario effetto collaterale.

! "

' Sets this buffer's position. If the mark is defined and larger than the
* new position then it is discarded. </p>

* @paramnewPosition The new position value: m u s t be non-negative


and no larger than the current limit

* © 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
•/

public final Buffer p o s i t i o n a l newPosition) {


if ((newPosition > limit) || (newPosition < 0))
throw new lllegalArgumentExceptionQ;

position = newPosition;

il (mark > position)


mark = -1 ;

return this;

4.5.5 Documentare eventuali limitazioni dell'implementazione di un metodo


Analogamente a quanto riportato a livello di classe (cfr. 4.4.4), non è infrequente il caso in cui
l'implementazione di un metodo presenti qualche problema e/o limitazione che, magari per
questione di tempo o per la presenza di dipendenze da altre parti di codice, non possano
essere immediatamente sistemate. In questi casi è opportuno documentare chiaramente tali
problemi e/o limitazioni. Ciò è fondamentale per semplificare la manutenzione e la riusabilità
di tale codice.

/ "

' Returns the current value of the most precise available system
* timer, in nanoseconds.

* <p>This method can only be used to measure elapsed time and is


* not related to any other notion of system or wall-clock time.
* The value returned represents nanoseconds since some fixed but
* arbitrary time (perhaps in the future, so values may be
" negative). This method provides nanosecond precision, but not
* necessarily nanosecond accuracy. No guarantees are made about
* how frequently values change. Differences in successive calls
* that span greater than approximately 292 years (2<sup>63</sup>
* nanoseconds) will not accurately compute elapsed time due to
* numerical overflow.

' <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>

* ©return The current value of the system timer, in nanoseconds.


* @since 1.5
7
public static native long nanoTimeQ;
Strategia di gestione
delle eccezioni
Introduzione
In questo capitolo l'attenzione è focalizzata su un altro aspetto fondamentale della program-
mazione che ha un ruolo centrale nella realizzazione di applicazioni affidabili: la gestione delle
eccezioni. Indipendentemente dall'impegno profuso nel disegnare e implementare sistemi
affidabili, e da tutti gli sforzi compiuti per raggiungere un elevato livello qualitativo, l'insorgen-
za di un ben definito insieme di problemi è inevitabile: per esempio l'impossibilità di instaurare
connessioni con il sistema di gestione del database, un network particolarmente lento, servizi
non disponibili, e così via. Credeteci: tali problemi si verificano molto più spesso di quanto
sarebbe auspicabile. Pertanto, la strategia adottata per gestire queste anomalie fa la differenza
tra sistemi affidabili e non affidabili. Chiaramente, la sola strategia di gestione delle eccezioni
non è in grado di sopperire a eventuali carenze qualitative: si tratta del necessario completamento
per conseguire un elevato livello di qualità.
L'assenza di una ben definita strategia di gestione delle eccezioni, d'altro canto, può generare
una serie di problemi. In particolare, ogni sviluppatore si trova costretto, nella migliore delle
ipotesi, a utilizzarne una propria, oppure, nei casi peggiori, a decidere, volta per volta, quale
metodologia impiegare. Considerando che i progetti tipici necessitano di decine di sviluppatori, si
genera una situazione decisamente caotica. Il risultato finale, pertanto, comprende tutta una serie
di scenari, che vanno dalla generazione di non poca confusione per il personale addetto al suppor-
to del sistema, dovuta, per esempio, a eccezioni notificate in modo incoerente o addirittura rese
note nelle parti meno appropriate del sistema, alla generazione di sistemi instabili e/o difficili da
monitorare, alla carenza e alla incoerenza nella generazione di log, con conseguente limitata effi-
cacia, o addirittura impossibilità, di utilizzare software forniti da terze parti atti a monitorare il
sistema in produzione, e così via. Una valida strategia di gestione delle eccezioni applicata all'in-
tero sistema è quindi un prerequisito irrinunciabile di qualità per una serie di ragioni:

• realizzare sistemi in grado di esibire un comportamento efficace e coerente al verificarsi


di errori e anomalie;
• agevolare il riutilizzo del codice;
• semplificare le attività manuali di investigazione necessarie dopo errori critici;
• agevolare l'uso di applicazioni esterne per monitorare il corretto funzionamento.

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".

Pertanto, come illustrato di seguito, il meccanismo di gestione delle eccezioni permette di


risolvere elegantemente il classico problema della gestione degli errori, evitando il continuo
passaggio, tra diversi moduli, di codici di errore per via dello stack.

La gerarchia delle eccezioni in Java


Gerarchia
Il linguaggio di programmazione Java prevede una ben definita gerarchia delle eccezioni come
mostrato nel diagramma delle classi di figura 5.1.
La gerarchia delle eccezioni ha inizio con la classe java.lang.Throwable (introdotta fin dalla
versione JDK 1.0) che quindi rappresenta la classe antenata (superclasse) di tutti gli errori e le
eccezioni del linguaggio Java. Pertanto tutti gli oggetti istanze di classi discendenti da questa
sono trattati come eccezioni e come tali possono essere lanciati dalla JVM e/o dalle applicazioni
Java per mezzo della parola chiave throw. Allo stesso modo, solo le classi che derivano da Throwable
possono essere utilizzate come argomenti dei costrutti catch.
Tuttavia, le applicazioni non dovrebbero mai definire delle classi che ereditano direttamente
da Throwable né, tantomeno, dovrebbero intercettare (catch) o lanciare (throw) eccezioni di que-
sto tipo. La classe java.lang.Error (introdotta anch'essa fin dalla versione JDK 1.0) è utilizzata per
comunicare gravi condizioni di errore e pertanto le applicazioni non dovrebbero cercare di
intercettarla. Questo è il motivo per il quale Error deriva direttamente da Throwable e non dalla
classe Exception. La classe java.lang.Exception (introdotta fin dalla versione J D K 1.0) è usata per
comunicare problemi che necessitano di essere gestiti ma che non sono così gravi come quelli
riportati da oggetti di tipo Error. La classe Exception a sua volta, prevede due specializzazioni:

1. eccezioni "a tempo di esecuzione" (runtime exception), implementate ereditando da


RuntimeException;
2. eccezioni controllate (cbecked) dette anche "non a tempo di esecuzione" (non-runtime
exception): tutte quelle che non hanno RuntimeException nella linea di ereditarietà.
java.lang.Object

Z\
i
java .lang.Throwable

java.lang. Error j j a v a . lang. E x c e p t i o n

" 1 j a v a . lang. R u n t l m e E x c e p t l o n

Figura 5.1 - Gerarchia delle eccezioni ]ava.

Eccezioni runtime e checked


Il primo tipo di eccezioni (runtime) è stato ideato per incapsulare errori che si verificano all'in-
terno dell'ambiente di esecuzione Java spesso causati da errori di programmazione. Alcuni esempi
famosi sono: NulIPointerException, ArithmeticException, IndexOutOfBoundException, etc. Una delle ca-
ratteristiche principali di questo tipo consiste nel non richiedere obbligatoriamente la gestione
esplicita, e per questo sono dette anche non controllate (unchecked). Pertanto i metodi che pos-
sono lanciare eccezioni runtime possono ometterne la dichiarazione (clausola throws nella firma
del metodo). Allo stesso modo, metodi che ne invocano altri che possono lanciare eccezioni
runtime non sono obbligati né a gestirle esplicitamente (catch) né a dichiararle sulla linea di
comando. Si consideri, per esempio, il metodo statico di conversione di stringhe in numeri:
lnteger.parselnt(<string>). Per quanto questo possa lanciare un'eccezione NumberFormatException
qualora il parametro fornito non rappresenti un valido valore numerico, non è necessario rac-
chiuderla nel ciclo try ... catch (NumberFormatException nfe) giacché questa è di tipo runtime (eredi-
t a d a RunTimeException).
Le eccezioni controllate, invece, rappresentano errori che si verificano in parti di codice "al
di fuori" dell'ambiente di esecuzione Java, quali errori di I/O. In questo caso, il compilatore
Java ne forza una gestione esplicita: è necessario o effettuarne il catch esplicito oppure dichia-
rarle nella firma del metodo, rinviandone la gestione al metodo chiamante. Ecco l'esempio di
un semplice metodo per la lettura di un file testo.

public String readFile(String filePath)


throws FileNotFoundException, lOException I

File file = new File(filePath);


BufferedReader fileReader = null;
StringBuffer strBuffer = null;

try I
fileReader = new BufferedReader(new FileReader(file));

boolean eof = false;


String nextLine = null;
strBuffer = new StringBuffer();

while (leof) I

nextLine = fileReader.readLine();

eof = (nextLine == null);

if (leof) (
strBuffer.append(nextLine);

I catch (FileNotFoundException fnfe) I


throw fnfe;

I catch (lOException ioe) I


throw ioe;

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.

La controversia relativa all'uso


Per quanto i principi base che hanno portato all'ideazione delle due eccezioni siano piuttosto
chiari, altrettanto chiaro non sembrerebbe esser il loro utilizzo. Non a caso esiste una dichiarata
controversia relativa all'utilizzo (http://java.sun.com/docs/books/tutorial/essential/exceptions/runtime.html).
Pertanto si è ritenuto opportuno riportare le direttive promosse da Sun Microsystem.
Il fatto che il linguaggio di programmazione Java non richieda ai metodi di specificare ecce-
zioni runtime o errori, potrebbe tentare i programmatori a scrivere codice che lanci esclusiva-
mente eccezioni runtime o a definire proprie eccezioni derivanti da java.lang.RuntimeException.
Queste "scorciatoie" permettono agli sviluppatori di scrivere del codice senza preoccuparsi di
gestire le eccezioni, bypassando i controlli del compilatore.
Sebbene, a prima vista, possa sembrare una tecnica molto comoda, questa elude gli intenti
dei requisiti "cattura o specifica" (catch or specify) alla base del meccanismo Java delle eccezioni
e può generare una serie di problemi ai programmatori che dovranno utilizzare classi imple-
mentate con questa tecnica.
Perché i progettisti hanno deciso di forzare i metodi a specificare tutte le eccezioni checked
non gestite che possono essere lanciate nel loro ambito? Ogni eccezione che può essere lanciata
da un metodo fa parte dell'interfaccia pubblica dello stesso. I metodi chiamanti devono essere
messi a conoscenza delle eccezioni che un metodo può lanciare affinché siano in grado di deci-
dere come trattarle. Queste eccezioni sono parte dell'interfaccia programmativa del metodo
così come lo sono i parametri e il valore di ritorno.
La prossima domanda potrebbe essere: se è così valido documentare l'API di un metodo,
incluse le eccezioni che questo può lanciare, perché non forzare la specifica esplicita anche
delle eccezioni runtime? Le eccezioni runtime rappresentano anomalie che sono il risultato di
un problema di programmazione e, come tale, non si può ragionevolmente richiedere alle API
del client di eseguire qualche azione per risolvere il problema e/o di effettuarne una qualsiasi
gestione. Questi problemi includono eccezioni aritmetiche (come la divisione per zero), ecce-
zioni di riferimento (come per esempio il tentativo di accedere alle proprietà di un oggetto
attraverso un puntatore nullo) ed eccezioni di indicizzazione (come per esempio un tentativo di
accedere ad un elemento dell'array attraverso un indice il cui valore sia superiore o inferiore

Eccezioni r u n t i m e Eccezioni checked

D i c h i a r a z i o n e d e l l e eccezioni Non necessario Obbligatorio


lanciate nella f i r m a del m e t o d o
Il m e t o d o c h i a m a n t e deve

Gestione delle eccezioni n e l Non necessario


gestire esplicitamente

metodo c h i a m a n t e l'eccezione (catch) oppure

r i l a n c i a r l a (IhrOWS)

Exception
Classe antenata RunlimeExceplion o Error

Tabella 5.1 - Proprietà delle eccezioni runtime e checked.


alle posizioni valide dell'array). Le eccezioni runtime possono avvenire in ogni parte del pro-
gramma e tipicamente possono essere numerose. Pertanto l'obbligo di dover aggiungere la
dichiarazione di eccezioni runtime in ogni metodo ridurrebbe drasticamente la leggibilità del
programma. Per questo motivo, il compilatore non richiede obbligatoriamente di gestire o
dichiarare eccezioni runtime, (sebbene nessuno vieti di farlo).
Un caso in cui è pratica comune lanciare eccezioni a tempo di esecuzione è relativo a
invocazioni scorrette di metodi. Per esempio, un metodo può verificare se gli argomenti speci-
ficati siano o meno nulli, ed in caso siano nulli, lanciare un'eccezione NulIPointerException che
appunto è di tipo runtime. In generale, non si dovrebbe lanciare una RuntimeException or creare
una sotto classe per il semplice motivo che si voglia evitare di specificare esplicitamente le
eccezioni lanciate dai propri metodi.

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.

5.1.2 Evitare di comunicare situazioni di errore


attraverso i parametri di ritorno
Il meccanismo di gestione delle eccezioni permette di risolvere elegantemente il classico pro-
blema della gestione degli errori, evitando il continuo passaggio di codici di errore, tra diversi
moduli, attraverso lo Stack. Quest'ultima strategia genera una serie di problemi.

• Aumento della complessità. Tutti i moduli chiamanti coinvolti in una sequenza di


invocazioni sono costretti ad occuparsi in maniera esplicita degli errori. Questo implica
la ripetizione della stessa logica necessaria per verificare il valore del parametro di ritor-
no e quindi intraprendere le necessarie operazioni a seconda di tale valore.
• Problemi di firma dei metodi. Questo problema deriva dal fatto che il valore di ritorno
di diversi metodi è utilizzato per fornire possibili codici di errore. Pertanto sono neces-
sarie strategie alternative per gestire il normale passaggio dei valori di ritorno.
• Ridotta capacità informativa. Poiché gli errori sono comunicati esclusivamente attraverso
un singolo codice, ne segue che l'elenco dei codici utilizzati deve essere conosciuto da
tutti i moduli presenti nella catena delle invocazioni con possibili problemi di incongruenza.

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:

• ritorno = 0, non si sono verificati errori; e quindi il parametro di input/output, result, è


valido e contiene il contenuto del file;
• ritorno > 0; è analogo al caso precedente con la variazione che il valore numerico indica
il verificarsi di problemi marginali (warning); nel caso proposto non ci sono warning;
• ritorno < 1 rappresenta l'insorgere di problemi, per esempio -1 rappresenta un errore di
"file not found" e -2 problemi di IO; il problema principale di questo codice è relativo
alla classe invocante (e di diverse tra quelle presenti nella sequenza di invocazione).

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);

BufferedReader fileReader = null;


StringBuffer strBuffer = null;

try I
fileReader = new BufferedReaderfnew FileReader(file));

boolean eof = false;


String nextLine = null;
strBuffer = new StringBuffer();

while (leof) I

nextLine = fileReader.readLine();

eof = (nextLine == null);

if (leof) I
strBuffer.append(nextLine);

result = strBuffer.toStringf);

I catch (FileNotFoundException fnfe) I


errorCode = -1;

I catch (lOException ioe) I


errorCode = -2;

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 = "";

int errorCode = readFile(filePath, result);

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

5.1.3 Utilizzare le eccezioni esclusivamente per comunicare


condizioni di errore
Come illustrato nei paragrafi precedenti, il meccanismo delle eccezioni così come lo si intende
ora è il risultato di una evoluzione relativamente lunga. In particolare, sebbene agli albori fosse
stato proposto un utilizzo più ampio non necessariamente relegato alla gestione delle anomalie,
questo si è dimostrato causa di diversi problemi e confusione nel codice. Pertanto la versione
moderna ne prevede un utilizzo confinato alla gestione degli errori. Quest'ultima strategia è quel-
la divenuta standard e pertanto, onde evitare la generazione di codici confusi e poco leggibili, è
necessario utilizzare le eccezioni solo ed esclusivamente per la gestione delle condizioni di errore.

5.2 Utilizzare correttamente il blocco finally


Un tipico modello di gestione delle eccezioni prevede di chiudere accuratamente (tecnicamen-
te clean-up, "pulire") le varie risorse prima di passare il controllo a ritroso. Il linguaggio di
programmazione Java prevede un apposito costrutto per gestire queste operazioni: il finally. Si
tratta di una parte opzionale del costrutto try (così come lo è la parte catch) e fornisce un mecca-
nismo elegante e opportuno per porre il sistema in uno stato adatto indipendentemente da
quanto succede nel corpo del try ... catch.
Come regola generale, si tenga presente che la clausola finally viene sempre eseguita anche
quando potrebbe sembrare il contrario, come per esempio, in presenza di istruzioni di return
all'interno del try. Una delle pochissime eccezioni a questa regola è data dalla presenza del-
l'istruzione System.exit. Si consideri il seguente frammento di codice che è stato modificato per
includere l'istruzione di return all'interno del costrutto try.

try I
fileReader = new BufferedReader(new FileReader(file));

boolean eof = false;


String nextLine = nuli;
strBuffer = new StringBuffer();
while (!eof) I

nextLine = fileReader.readLine();

eof = (nextLine == nuli);

il (!eof) I
slrBuffer.append(nexlLine);

return strBufter.toString();

I catch (FileNotFoundException fnfe) I


throw fnfe;

I catch (lOException ioe) I


throw ioe;

) 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.

5.2.1 Utilizzare il blocco finally per il clean-up


Utilizzare il costrutto finally ogni qualvolta nel corso del costrutto try ... catch si utilizzano degli
oggetti da porre in un determinato stato prima di passare il controllo a ritroso. Ecco l'ennesima
variazione del listato principale con la chiusura di uno stream senza il blocco finally.

public String readFile(String filePath)


throws FileNotFoundException, lOException I

File file = new File(filePath);

BufferedReader fileReader = nuli;


StringBuffer strBuffer = nuli;
try!
fileReader = new ButferedReader(new FileReader(file));

boolean eof = talse;


String nextLine= null;
strBuffer = new StringButfer();

while (ieof) I

nextLine = tileReader.readLine();

eof = (nextLine == null);

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

I catch (FileNotFoundExceptlon fnfe) 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;

I catch (lOException ioe) {

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.

5.2.2 Valutare la necessità di inserire un try... catch


all'interno del blocco finally
Spesso le operazioni di pulizia presenti all'interno del blocco finally possono generare delle
eccezioni come nei casi presentati in precedenza. In questi casi è opportuno inserire un oppor-
tuno costrutto try ... catch all'interno del costrutto finally. In questo caso però quasi mai è oppor-
tuno riportare, qualora insorgesse, la nuova eccezione: quella che ha generato il problema ini-
ziale è tipicamente più importante. Comunque è buona pratica riportare eventuali ulteriori
eccezioni nel file di log.

5.2.3 Utilizzare finally al posto di finalize


Come visto nei capitoli precedenti, non è opportuno ricorrere al metodo finalize per effettuare
il clean-up degli oggetti. Questo perché non vi è garanzia del momento esatto in cui il GC
(Garbage Collector) reclamerà lo spazio di memoria occupato da un oggetto e quindi invocherà
il metodo finalize. Nel codice Java funzionante all'interno di un application server, il metodo
finalize potrebbe anche venir invocato dopo diverse ore dal momento in cui l'oggetto stesso è
stato referenziato. Pertanto, per eseguire la pulizia degli oggetti, è buona pratica includere
opportuni blocchi finally all'interno dei metodi come visto in precedenza. Chiaramente il co-
strutto finally, ha un funzionamento assolutamente diverso dal metodo finalize. Tuttavia, è conve-
niente utilizzarlo per realizzare un sistema più accurato e affidabile di pulizia delle risorse.

5.3 Non utilizzare i tipi base delle eccezioni


Come visto in precedenza, il linguaggio di programmazione Java dispone di una serie di classi
fondamentali utilizzate per notificare anomalie (figura 5.1). Queste sono: Throwable, Errar, Exception
e RuntimeException. L'idea alla base del meccanismo delle eccezioni è che queste classi (ad ecce-
zione della classe Error che dovrebbe essere utilizzata solo in contesti particolarissimi) rappresen-
tano comodi punti di estensione per permettere la definizione di opportune gerarchie, come
quelle dei package Java. Pertanto, bisognerebbe evitare un riferimento diretto a questi tipi.

5.3.1 Non utilizzare direttamente Throwable, Error,


Exception, RuntimeException
Le applicazioni non dovrebbero mai utilizzare direttamente le seguenti classi: Throwable, Error,
Exception e RuntimeException. In particolare è opportuno che framework e package usino eccezio-
ni derivate da Exception o RuntimeException standard e/o appartenenti a un'opportuna gerarchia.
La comunicazione di eccezioni attraverso le classi Throwable ed Error può severamente com-
promettere la possibilità di eseguire, all'interno della stessa JVM, librerie, framework e package
scritti da terze parti e di riutilizzare il codice.
5.3.2 Non ereditare direttamente dalle classi Throwable o Error
Le applicazioni non dovrebbero mai definire eccezioni derivanti direttamente dalla classe
Throwable né cercare di intercettare (catch) o lanciare (throw) eccezioni di questo tipo. Allo stesso
modo, le applicazioni non dovrebbero definire nuove classi di errore derivanti direttamente da
java.lang.Errar né tantomeno tentare di intercettarle. Si tratta di classi con un significato ben
definito lanciate dalla JVM per segnalare gravi anomalie che non possono essere gestite.

5.3.3 Non utilizzare i tipi base per intercettare le eccezioni


Le eccezioni non dovrebbero mai essere intercettate utilizzando i tipi base come Throwable,
Error, Exception e RuntimeException.
Come regola generale è opportuno cercare di intercettare le eccezioni specifiche evitando di
definire dei "filtri" troppo ampi che possono facilmente causare situazioni di errore. Per esem-
pio, una rifattorizzazione del codice potrebbe portare all'inserimento di istruzioni aggiuntive in
grado di generare nuovi tipi di eccezione; vista la mancanza di selettività della clausola catch, tali
eccezioni finirebbero comunque per essere accomunate alla gestione prevista dal caso generale,
senza fornire al programmatore alcuna indicazione: nella maggior parte dei casi sarebbe fonte
di problemi non facilmente individuabili. Si consideri per esempio il seguente codice:

try I

String content = fileManager.readFile(xmlFile);

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 {

String content = fileManager.readFile(xmlFile);


configVO = (ConfigVO) genericStringUnmarshaller(content);

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.

5.3.4 Valutare l'opportunità di implementare


una propria gerarchia di eccezioni
Nel disegno e nella codifica di framework, package e strati logici è spesso opportuno prevedere
l'implementazione di una specifica gerarchia di eccezioni. Ciò fa sì che le classi client siano
esposte a un insieme di interfacce consistente e minimale. In tali circostanze, tuttavia, è neces-
sario che le nuove eccezioni siano codificate in modo tale da incapsulare quelle originali. Per-
tanto, sebbene implementare proprie gerarchie di eccezioni per librerie, framework, etc. sia
un'ottima pratica per agevolarne la gestione da parte delle classi fruitrici, è comunque
consigliabile mantenere le originali nel codice interno del package, framework, etc.
In questi casi, prima di creare una nuova eccezione, è consigliabile porsi queste domande:

• 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.

5.3.5 Non perdere le informazioni relative all'eccezione iniziale


Un'importante miglioria introdotta con la versione JDK 1.4 è la possibilità di annidare eccezio-
ni. In particolare ciò è stato possibile grazie a un'opportuna revisione della classe
java.lang.Throwabie. Questo perfezionamento permette di lanciare nuove eccezioni senza perde-
re informazioni relative a quella originale. Si tratta, pertanto, di una caratteristica molto utile in
quando rende possibile sviluppare framework che, per comodità delle classi fruitrici, possono
lanciare proprie eccezioni con le eccezioni originali incapsulate all'interno.
Quindi, in tutti i casi in cui si decida di procedere con l'implementazione di opportune classi
eccezione, è importante includere i costruttori riportati nel listato seguente.

/ "

" Constructor melhod


• @ p a r a m errar encapsulated error
*/
public MyExceptionfThrowable error) I
super(error);
| | «v& lant Ei< non [

J )iv&lanu.A untimi E «tfpi Ion | r )a*M«Cu'ily 1


! Goncral$p<urityE»Ceptlon

I |»v»lan». I invillanì. javiutur ty. javuecurlty. )av«wcurity |«v&se<umy. |


IlleialPafameterfiteplion I $ecurilyC»ceplinn I I P'Ovider£«cc ption NòSuchAlqorilhmExCPpl'On Unrecoverable EnlryEnceptionl NoSuchPioviderEiceplion 1 Jnr CQvprablclleyCicppiion J | Keytiception J

javuMurlty. Java.ieci/rity. )ava.*<umy. javikw curily. laviwtunty.


KeyStoreCnception 1 KeyManagcTOntCìcepUon 1 hwilidAlgorillunParameicrEicpplionl Signature Eicvptlor I I DiqeMticcptiOn 1

|a»a.*curny II Java»«urUy. I (" |ava.iecufity.


Invai idPararrwIcrCjicffpl lori 1 1 AcccnComrolE« W o n 1 invalidKcyE «ception

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);

5.4 Fare attenzione alla modalità di notifica delle eccezioni


Non è infrequente il caso in cui le eccezioni che si presentano in un sistema sono relative a
problemi non gestibili. In questo caso è necessario fare in modo che le cause che hanno genera-
to il problema siano opportunamente riportate e illustrate. Qualora, l'eccezione si verifichi
durante l'espletamento di un servizio richiesto da un utente, magari collegato con un browser
remoto, è necessario renderlo consapevole del problema che si è presentato. In ogni caso, le
varie anomalie vanno opportunamente segnalate sia al fine di semplificare eventuali investiga-
zioni manuali, sia per fornire sufficienti informazioni a eventuali meccanismi automatici predi-
sposti per la risoluzione di problemi.

5.4.1 Non assorbire mai le eccezioni silenziosamente


Si dice che un'eccezione è assorbita "silenziosamente" quando il codice incluso nella relativa clau-
sola catch non risolve il problema e non esegue alcuna operazione per notificare il verificarsi del-
l'eccezione stessa. Il listato seguente mostra un esempio di eccezione assorbita silenziosamente:

A
tryi

cistruzione che può causare l'eccezione>


<instruzione>
<instruzione>
) catch (<eccezione> e) (

l // eccezione assorbita sllezlosamente


Anche se si pensa che l'eccezione non si verificherà mai, è sempre il caso di aggiungere una
segnalazione, magari un semplice record di log. In effetti, può sempre succedere che scenari di
eccezione apparentemente innocui diventino problematici a seguito di processi di manutenzione
del codice. Individuare eccezioni assorbite silenziosamente può richiedere un impegno elevatissimo.

5.4.2 Mantenere l'utente informato


Qualora un'eccezione si verifichi durante la gestione di uno stimolo generato da un utente, è
necessario far in modo che opportune informazioni circa l'anomalia siano notificate all'utente.
Chiaramente questo vale per tutte le eccezioni non temporanee. E pertanto necessario:

1. presentare un messaggio che sia comprensibile all'utente;


2. fornire all'utente diverse opportunità sul da farsi e, in casi specifici, fornire la possibilità
di tentare nuovamente la stessa operazione.

Un sistema di qualità dovrebbe sempre fornire all'utente informazioni su quanto accade


dietro le quinte, non solo in presenza di problemi, ma anche qualora l'operazione sia rallentata.

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.

5.4.3 Pianificare e applicare in maniera uniforme e coerente


il formato dei log riferiti alle eccezioni
La realizzazione di sistemi ad alta disponibilità/affidabilità, richiede di includere diversi accor-
gimenti non necessari in altre situazioni. Per esempio, è necessario progettare meccanismi in
grado di rilevare tempestivamente eventuali componenti e sistemi malfunzionanti o gravati da
stress enorme. Una delle tecniche più largamente impiegate consiste nel ricorrere a sistemi
dedicati a monitoraggio e analisi dei file log prodotti dalle varie applicazioni. Ciò al fine di
individuare dei pattern predefiniti che identifichino situazioni di malfunzionamento (per esempio
una linea di testo iniziante con la stringa "ERROR").
Al fine di rendere possibile l'impiego di questi sistemi di controllo è opportuno assegnare ai
log opportuni formati, specialmente a quelli relativi all'insorgere di un'eccezione. Una struttura
abbastanza ricorrente è la lista di attributi separati da caratteri bianchi, in cui le diverse infor-
mazioni prevedono i dati come: data e ora dell'eccezione, livello di log, identificatore univoco
del problema ed informazioni di dettaglio. Per esempio:

2 0 0 6 - 1 2 - 0 3 12:51:07.103 W R N SDS-CN01 com.mokabyte.


tool.umlclassgenerator.service.DiagramParser - ' d i a g r a m not found' diagram=class34

Questo argomento è presentato in dettaglio nel Capitolo 6.


5.5 Valutare attentamente il ciclo di vita delle eccezioni
Il meccanismo delle eccezioni fornisce un'ottima soluzione per notificare eventuali anomalie.
Queste però, oltre a essere opportunamente segnalate devono, ovviamente, essere anche pro-
priamente gestite. Un elemento di particolare importanza nell'implementazione delle proce-
dure di gestione consiste nel selezionare il luogo migliore in cui gestirle. In effetti, si deve
evitare una gestione in posti in cui non è chiaro il da farsi e, al tempo stesso, non è opportuno
notificare all'infinito un'eccezione qualora sia possibile gestirla.

5.5.1 Non provare a gestire un'eccezione in un posto


in cui non è possibile farlo
Un ragionevole principio di programmazione prescrive di non tentare la gestione di un'ecce-
zione in una parte di codice in cui non siano disponibili sufficienti informazioni per farlo. In
caso contrario, nella migliore delle ipotesi, si finisce con il rendere il codice molto complesso e
difficilmente riutilizzabile, mentre nella peggiore si finisce per assumere alcune informazioni,
non sempre valide, generando un insieme di problemi rilevabili solo all'insorgere di casi di
eccezione, spesso non facilmente riproducibili e testabili.
Si consideri per esempio il codice riportato nel listato presentato per primo in questo capito-
lo. Nel caso in cui si verifichi un'eccezione di tipo FileNotFoundException non si hanno sufficienti
informazioni per selezionare l'opportuna gestione, pertanto la cosa migliore da farsi è rimanda-
re la gestione alla classe chiamante. Le eccezioni, quindi, vanno intercettate (catch) quando si
hanno sufficienti informazioni per poterle gestirle correttamente. Questa regola può essere
scavalcata nel caso in cui l'eccezione debba essere incapsulata in un'altra più generale. In que-
sto caso si ha un catch simbolico, come nel caso del listato principale.

5.5.2 Gestire le eccezioni non appena possibile


Questa regola a prima vista potrebbe sembrare in contraddizione con quella precedente. In
realtà, le eccezioni dovrebbero essere gestite non appena si giunge, nel percorso a ritroso della
successione delle invocazioni, a un metodo che abbia sufficienti informazioni per gestirla. Ciò,
in alcuni scenari può coincidere con il comunicare indietro l'eccezione fino al metodo che ha
eseguito la chiamata iniziale della catena di invocazioni.

5.6 Considerare la natura delle eccezioni


Un criterio di raggruppamento delle eccezioni consiste nel suddividerle in base alla natura del
problema che le ha originate. Ciò porta a individuare due gruppi di eccezioni: business e di
sistema. Le prime si riferiscono a eventi che violano una o più business rules (le regole alla base
della logica applicativa), la cui conseguenza sta nell'impossibilità da parte del sistema di fornire
il servizio dove l'eccezione si è verificata. Le eccezioni di sistema, invece, si riferiscono a eventi
che si verificano nel sistema (per esempio, il database diventa indisponibile) e che compromet-
tono la fornitura di un insieme di servizi per un intervallo più o meno lungo. Pertanto problemi
di sistema sono potenzialmente più gravi di quelli di business.

5.6.1 Gestire correttamente le eccezioni business


Le eccezioni di tipo business si riferiscono ad eventi che violano una o più regole della logica
applicativa (business rules) e pertanto non permettono al sistema di portare a termine corretta-
mente il relativo servizio. Per esempio, un sistema di back office bancario potrebbe ricevere un
messaggio contenente un trade relativo a un cliente (counter-party) delle cui necessarie infor-
mazioni il sistema stesso non dispone. Il sistema, pertanto, non può che lanciare un'eccezione
di business relativa all'impossibilità di processare automaticamente tale trade. Situazioni come
questa, come la quasi totalità delle eccezioni di tipo business, richiedono l'intervento umano.
Nel caso in questione, un operatore dovrebbe occuparsi di inserire i dati mancanti nel sistema
oppure di correggere l'identificativo errato del cliente.
Sistemi medio/grandi atti a gestire importanti business (come nell'esempio precedente) ri-
chiedono di progettare meccanismi atti a risolvere prontamente questo tipo di problemi. Nel
caso precedente, per esempio, sebbene il sistema non fosse in grado di processare il trade, non
è pensabile che questo possa tranquillamente rifiutare o scartare il trade: nei sistemi bancari
viaggiano spesso trade relativi a contratti da centinaia di milioni di dollari.
Pertanto, se da un lato è necessario progettare sistemi informatici in grado di processare gran
parte degli eventi automaticamente (i famosi sistemi STP, Straight-Through Processing), dall'al-
tro è necessario prevedere meccanismi da attuare qualora si verifichino eccezioni durante l'ela-
borazione automatica. Questi meccanismi, tipicamente, richiedono di realizzare un apposito
sottosistema, denominato sistema di gestione delle eccezioni business (BEM, Business Exception
Management) incaricato appunto di gestire eccezioni di tipo business. In particolare, questo
sistema si dovrebbe occupare di:

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.

5.6.2 Valutare l'estensione temporale delle eccezioni


Le eccezioni di business tendono per loro natura a essere di tipo permanente. Se per esempio
l'identificativo di un cliente presente in un messaggio è errato, questo rimarrà tale fin quando
non viene intrapresa un'opportuna azione correttiva. Quindi all'interno del servizio non c'è
nulla da fare: una volta generata l'eccezione questa non ha alcuna possibilità di risolversi auto-
maticamente.
Un discorso diverso vale per le eccezioni di sistema. In questo caso, le cause che hanno
portato alla generazione dell'eccezione potrebbero automaticamente risolversi all'interno del-
lo stesso servizio in cui si è manifestata. Si consideri, per esempio, un servizio che tenti di
connettersi al Database Management System e che fallisca per un eccesso temporaneo di traf-
fico presente nella rete. Un altro esempio è relativo a un sistema che tenti di connettersi a un
altro e che, per un eccessivo e temporaneo stress di quest'ultimo, non riceva una risposta entro
l'intervallo di tempo previsto (timeout). Eccezioni di sistema presentano spesso una persistenza
temporanea e quindi una corretta gestione prevede di ritentare l'istruzione che può generarla,
per un numero di predefinito di volte, prima di notificarla a ritroso.
Un esempio di possibile gestione delle eccezioni di sistema è presentata nel listato seguente.

int attempts = 0; // counís the number of failed atlempts


boolean success = false; // success flag
tryl
while ( (attempts < MAX_FAILED_ATTEMPTS) && (¡success) ) I
tryl
< istruzione che può generare un'eccezione>
<istruzione>
•¡istruzione che può generare un'eccezione>
<instruction>
success = true;
I catch (<eccezione specifica> es) I
attempts++;
il (attempts < MAX_FAILED_ATTEMPTS) I
Iryl
Thread.sleep(BASIC_TIME_WAIT*attempts);
I catch (InterruptedException ie) (
effettuare il log dell'eccezione e quindi proseguire>
I
I
I
I
I finally I
<parte del codice richiesto dal finally>
I

5.6.3 Gestire correttamente il perdurare delle eccezioni di sistema


In tutti quei in cui un'eccezione di sistema non si risolva in un arco temporale abbastanza breve
(per esempio il codice del listato visto poco sopra fallisca le varie iterazioni) nasce il problema
intricato di come gestire la situazione. Il sistema, indubbiamente, si trova in una situazione
critica in cui, difficilmente, potrà soddisfare altre richieste. Situazioni del genere, tipicamente,
richiedono l'intervento umano, magari semplicemente per riawiare uno dei sistemi entrato in
un stato di malfunzionamento.
Le alternative disponibili includono: l'avvio della procedura controllata di shut-down, far
transitare il sistema in un particolare stato di inattività (¿die). Tale stato dovrebbe essere carat-
terizzato dal fatto che il sistema inibisca i canali di comunicazione di input (non accetti più
stimoli), eccetto quelli provenienti da un'opportuna console di amministrazione e, a intervalli
di tempo via via crescenti, tenti di verificare l'effettivo perdurare del problema. Questa secon-
da alternativa, sebbene più complessa, permette al sistema di riprendere automaticamente il
corretto funzionamento non appena il problema di sistema sia risolto.
Quale sia la soluzione da implementare, come al solito, dipende da un insieme di fattori,
quali: i requisiti utente, il particolare tipo di eccezione e i meccanismi presenti atti a segnalare
tempestivamente, a un operatore umano, l'insorgere del problema.
Indipendentemente dalla soluzione scelta, è necessario far in modo che i processi business
colpiti dal problema siano gestiti correttamente, come visto in precedenza. Ciò può limitarsi a
inviare una comunicazione all'utente, a cercare di salvare opportune informazioni, e cosi via.

5.7 Considerare i classici problemi relativi all'impiego


di sistemi di messaggistica
La tendenza degli ultimi anni nei sistemi enterprise è stata caratterizzata dalle tematiche di
integrazione. Il recente passato ha visto l'affermarsi di moderni sistemi di integrazione software
come gli Enterprise Service Bus (ESB, Bus di Servizio Aziendale); ciò nonostante i sistemi di
messaggistica sono ancora molto utilizzati. Questi permettono di disegnare e implementare
grandi sistemi in termini di un insieme di sottosistemi interconnessi con elevato grado di
disaccoppiamento. La loro presenza, tuttavia, nella quasi totalità dei casi, richiede di valutare
attentamente e di gestire le seguenti due problematiche: messaggi "avvelenati" (poisoned
messages) e ricezione di messaggi fuori sequenza.

5.7.1 Gestire correttamente i messaggi "avvelenati" (poisoned)


Con il termine di messaggi "avvelenati", ci si riferisci a messaggi viziati da qualche problema che
tendono a restare perennemente nel sistema se non opportunamente rimossi. Questo scenario si
può verificare in situazioni in cui, per un particolare canale, il Message Oriented Middleware
(MOM, infrastruttura orientata al messaggio) è impostato per un funzionamento a consegna
garantita (guaranteed delivery, il messaggio viene consegnato una e una sola volta). In particola-
re, questa situazione si ha quando un sistema riceve un messaggio che viola una o più pre- e/o
post-condizioni del sistema ricevente; il ricevente, quindi, non può far altro che scartarlo gene-
rando un'opportuna eccezione. Il sistema di messaggistica, non ricevendo la conferma dell'avve-
nuta ricezione del messaggio (ossia il commit), dopo un leggero intervallo di tempo, presenta
nuovamente il medesimo messaggio al sistema. Ciò perché si è impostata la modalità di consegna
garantita. Questa successione potrebbe continuare all'infinito qualora nessuna azione sia intra-
presa. Alla presenza di diversi messaggi avvelenati, si potrebbe generare una serie di conseguen-
ze indesiderate come una notevole perdita di performance del sistema, perdita delle informazio-
ni contenute nel messaggio stesso giacché questo non viene gestito, e così via.
Una buona tecnica per gestire problemi di questo tipo consiste nel richiedere ai sistemi
destinatari di gestire apposite tabelle in cui memorizzare tre campi: l'identificatore univoco dei
messaggi, il timestamp dell'ultima ricezione e un contatore di ricezioni. In questo modo, quan-
do questo contatore raggiunge un valore prefissato (per esempio 3), il sistema ricevente è in
grado di accorgersi delle presenza di un messaggio avvelenato e quindi procedere alla sua gestio-
ne. Questa, invece di generare un'eccezione (che spingerebbe nuovamente il messaggio nel
MOM), si deve occupare di spostare il messaggio in un'apposita coda, denominata normalmen-
te "ospedale dei messaggi" e di comunicare, al sistema di messaggistica l'avvenuta ricezione del
messaggio, in modo analogo a quando tutto funziona correttamente. La coda dei messaggi avve-
lenati dovrebbe poi essere monitorata da un opportuno meccanismo in grado di comunicare ad
appositi operatori umani la presenza di messaggi avvelenati che devono essere gestiti.

5.7.2 Messaggi fuori sequenza


Questo scenario, come suggerisce il nome, si riferisce a situazioni in cui un sistema destinatario
riceva dei messaggi in ordine diverso da quello di trasmissione. Sebbene non sempre questo
scenario costituisca un problema insormontabile, in alcune situazioni potrebbe però generare
scenari molto dannosi. Si consideri per esempio il caso di un sistema bancario in cui i messaggi
relativi al prezzo di uno stesso prodotto finanziario siano ricevuti in ordine errato.
Sebbene molti sistemi di messaggistica recenti dispongano di meccanismi interni atti a gesti-
re questo problema, la loro efficacia, in particolari scenari è decisamente ridotta. Si consideri,
per esempio, il caso di un sistema a elevato livello di parallelismo; per un qualsiasi problema,
uno dei sottosistemi diventa improvvisamente instabile ("va in crash") e necessita di essere
riawiato. Si supponga, ulteriormente, che questo sottosistema abbia sottoscritto alcuni canali
con consegna garantita dei messaggi. In questo scenario, i messaggi presenti nei relativi buffer
passano automaticamente in uno stato di bloccaggio finché i corrispondenti socket non vengo-
no chiusi. Ciò può richiedere un intervallo di tempo non trascurabile. Se nel frattempo, un
meccanismo disegnato per l'elevata disponibilità riawia il sottosistema "crashato", magari su
un server diverso da quello originario, ecco che si verifica un'elevata probabilità di avere mes-
saggi recapitati con un ordine diverso da quello in cui sono stati immessi nella coda.
Anche se lo scenario presentato può sembrare un caso limite, in sistemi complessi non lo è
affatto! Il dato di fatto è che il problema dei messaggi fuori sequenza non solo può verificarsi
ma si verifica. Pertanto, qualora ciò possa arrecare danni, è necessario disegnare ed implemen-
tare meccanismi per la gestione di queste anomalie.
Un sistema di intercettazione di questo problema richiede l'utilizzo di un meccanismo simile
a quanto visto per i messaggi avvelenati. In particolare è necessario che i sistemi destinatari
gestiscano una tabella in cui memorizzare l'identificativo del messaggio ricevuto e il momento
in cui è stato ricevuto. Questo identificativo deve essere ottenuto dall'entità incapsulata nel
messaggio stesso, e quindi deve avere una valenza per così dire business.
A questo punto, quando il sistema destinatario riceve un messaggio deve verificare se abbia o
meno già ricevuto un messaggio con il medesimo identificativo del messaggio. In caso negativo,
ovviamente, non ci sono problemi, ed è sufficiente creare un record relativo al nuovo messaggio.
In caso positivo, invece è necessario controllare il timestamp dell'ultima occorrenza ricevuta con
quella del messaggio. Se quest'ultimo si dimostra essere più recente, allora non ci sono problemi
ed è sufficiente aggiornare il relativo record nella tabella. Mentre, in caso contrario, il nuovo
messaggio è stato ricevuto fuori sequenza e quindi è necessario avviare la relativa gestione. Que-
sta in funzione della situazione in cui si presenta, può richiedere diverse soluzioni.
Per esempio, nel caso dei prezzi, qualora si riceva un messaggio fuori sequenza, basta scartar-
lo: il prezzo attuale è sicuramente più recente di quello ricevuto in ritardo; ma in altri contesti
si può giungere fino alla situazione di dover richiedere l'intervento dell'operatore umano.
Capitolo
Il logging
Introduzione
In questo capitolo presentiamo un argomento d'importanza fondamentale per la produzione
di software di qualità: la strategia di logging. Con questo termine ci si riferisce alla politica
utilizzata da un'applicazione per generare informazioni (log record, sorta di "diari di bordo")
relative alle attività eseguite. I log possono essere utilizzati per diversi scopi; per esempio:

• 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.

Più in generale, il logging è un modo sistematico e controllato di rappresentare lo stato di


un'applicazione in una forma comprensibile alle persone. ([APCL4J], Samandra Gupta).
I record di log sono tipicamente scritti su un apposito file, detto appunto file di log, e, meno
frequentemente, in un'apposita base di dati. Queste informazioni rappresentano una preziosa
risorsa di consultazione per l'analisi di eventuali situazioni anomale. Un'applicazione FTP, per
esempio, potrebbe gestire appositi file di log in cui memorizzare i dati relativi ad ogni azione
eseguita: ricezione e trasmissione di file, cambiamento del nome di un file, etc. Questi log
potrebbero includere: il percorso completo del file, la specifica attività eseguita (invio, ricezio-
ne, etc.), il codice dell'utente connesso, data e tempo di inizio e di conclusione del processo,
server di destinazione, ecc. Pertanto in caso di errore, sarebbe sufficiente consultare il file di log
per capire rapidamente cosa sia andato storto. Inoltre, lo stesso file potrebbe essere consultato
per individuare eventuali tentativi di eseguire operazioni illecite.
Il logging, rappresenta la più tradizionale e basilare strategia di debugging del codice (per
questo definita a basso livello) basata sull'analisi di opportuni messaggi emessi dal sistema. Il
logging, infine, costituisce una forma di auditing ("revisione") del sistema. Per esempio, sareb-
be sufficiente analizzare periodicamente i file di log per individuare tentativi di eseguire azioni
non autorizzate. Un buon logging è fondamentale per una serie di attività, come:

1. diagnosi dei problemi in sistemi in produzione: non sempre è possibile usufruire


dell'ausilio di sofisticati debugger per diagnosticare eventuali situazioni anomale. Que-
sto è per esempio il caso di sistemi in produzione. Si consideri la necessità di dover
ricostruire un'anomalia generatasi in uno sportello bancomat (ATM). In questo caso i
file di log, non solo quelli gestiti dallo sportello, rappresentano una risorsa preziosissima
per poter ricreare lo scenario di malfunzionamento.
2. diagnosi dei problemi in sistemi ad elevato grado di concorrenza: in queste situazioni,
spesso i debugger, anche i più sofisticati non sono il migliore ausilio per l'analisi di
malfunzionamenti. Questo sia perché l'introduzione del debugger finisce per influenza-
re il grado di concorrenza dell'applicazione, sia perché rende proibitivo l'intercettazio-
ne di una situazione di errore di un sistema in un contesto di stress test.
3. mantenimento della storia dell'applicazione: il debugging è un'attività transiente, men-
tre il logging è permanente.

Sebbene tutti gli sviluppatori esperti condividano l'importanza di un'appropriata, consisten-


te e controllata strategia di logging, si tratta dell'ennesima attività fin troppo spesso trascurata,
realizzata di fretta nei giorni precedenti al rilascio del sistema e/o eseguita in maniera casuale
senza seguire specifiche linee guide. Altro scenario molto frequente è il caso del logging affida-
to all'esperienza e alla buona volontà dei singoli sviluppatori del team di sviluppo al posto di
seguire una precisa strategia. Gli inconvenienti generati da questi approcci emergono in tutta la
loro drammaticità quando, una volta installato (deployed) il sistema in ambiente UAT (User
Acceptance Test, test di accettazione utente o di staging) o, peggio ancora, in produzione, si
abbia la necessità di correggere i primi malfunzionamenti... L'esperienza insegna che gli errori
possono accadere anche in sistemi ad elevata qualità; anzi, a essere precisi, accadono! 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...
La riproduzione di un'anomalia è ovviamente propedeutica alla sua correzione.
Una politica di logging non ben pianificata è in grado di creare tutta una serie inconvenienti.
Per esempio, un logging eccessivo tende a incidere significativamente sulle performance del
sistema tanto da divenire un serio problema in sistemi gravati da stringenti requisiti non funzionali
(bassissima latenza ed elevato throughput). Inoltre, logging eccessivamente verbosi, finiscono
per generare dati inutili, "rumore", piuttosto che preziose informazioni. Pertanto, il logging
delle applicazioni software deve seguire una ben precisa strategia pianificata a priori e basata su
opportune best practice, che sono presentate nei paragrafi delle direttive.

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:

System.out.printlnf activity message received. (message=" + newMessage.toString() + ")" );

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:

• importare la libreria log4J;


• dichiarare e reperire lo specifico logger (questo è possibile poiché le Category sono orga-
nizzate in un namespace gerarchico: la classe Category ha un attributo nome e una auto-
relazione denominata parent (fig. 6.2).

I principali metodi della classe org.apache.log4j.Logger sono riportati di seguito.

public class Logger {

// Creation & retrieval methods;

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

Figura 6.1 - Schema a blocchi di Log4j.


. ::k>g5j.spi::
LoggerRepository

...::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 ;

Figura 6.2 - Parte della struttura interna di Log4j.

public static Logger getRootLogger();


public slatic Logger getLogger(String name);

// 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);

// generic printing method:


public void log(Level I, Object 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

FATAL estremamente importanti, errori fatali appunto, che impediscono all'applicazione di

funzionare.

Si tratta di un livello molto alto, utilizzato per segnalare condizioni di errore. La

differenza rispetto a FATAL è che problemi appartenenti a questa categoria tendono ad


ERROR
influenzare un numero limitato di transazioni e pertanto non precludono

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 .

Questo livello è riservato p e r m e s s a g g i di a v v e r t i m e n t o circa condizioni che pur non


WARN
c r e a n d o problemi immediati risultano potenzialmente dannose.

M e s s a g g i di i n f o r m a z i o n e utilizzati p e r r i p o r t a r e i n f o r m a z i o n i relativi alla progressione


INFO
d e l l ' a p p l i c a z i o n e a d u n l i v e l l o di a s t r a z i o n e a b b a s t a n z a elevalo.

DEBUG M e s s a g g i relativi alla c o n f i g u r a z i o n e e s e t u p dell'applicazione.

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:

• ConsoleAppender: invia i messaggi di log alle console di output standard: System.out e


System.err, dove la prima è quella di default. Questo appender dovrebbe essere utilizzato
esclusivamente per fini di semplici test.
• FileAppender: questo appender scrive i record di log in un apposito file.
• DailyRollingFileAppender: si tratta di un'estensione all'appender precedente, in cui il com-
portamento aggiuntivo consiste nel gestire una serie di file di log al fine di limitare la
dimensione di ciascun file. La condizione per dar vita ad un nuovo file è basata su op-
portune impostazioni temporali definite dall'utente.
• RollingFileAppender: questo appender è molto simile al precedente con l'unica variante che
il nuovo file di log è generato qualora quello esistente raggiunga una data dimensione.
• WriteAppender: si occupa di aggiungere i record di log a un dato oggetto Writer or
OutputStream a seconda delle impostazioni utente.
• SMTPAppender: questo appender si occupa di comunicare record di log attraverso l'invio di
e-mail. Si tratta di un buon espediente per comunicare errori molto seri come quelli fatali.

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

Figura 6.3 - Diagramma a oggetti di una possibile configurazione di Log4],


DEBUG INFO WARN ERROR FATAL

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.

Un discorso a parte merita l'appender asincrono (AsyncAppender). Questo è stato disegnato


per superare il tipico accoppiamento temporale presentato dagli appender tradizionali. Questo
prevede che l'istruzione di log presente nel codice debba essere eseguita completamente prima
di permettere al codice di passare all'istruzione successiva. Questo comportamento bloccante
può divenire problematico in tutte quelle situazioni in cui sia necessario fornire determinati servi-
zi nell'ordine del secondo. Requisiti simili sono molti frequenti negli ambienti di front office
bancari, in cui, per via dell'agguerrita concorrenza, è necessario fornire determinati servizi con
latenze bassissime onde non perdere la possibilità di aggiudicarsi importanti transazioni finanzia-
rie. Si consideri per esempio il caso delle richieste di quotazione (Request For Quote): le statisti-
che mostrano come nel 9 0 % dei casi i clienti selezionino una delle prime tre quotazioni ricevute.
In questi casi, invece di rinunciare completamente al meccanismo di log, è consigliabile ri-
correre a questo appender in grado di creare uno strato di indirezione, grazie ad apposite code
interne, tra il codice chiamante, e gli altri appender che si occupano di eseguire l'effettiva tra-
scrizione del log. Da quanto riportato risulta evidente che questi software di logging sono stru-
menti molto potenti e, volendo, si prestano ad essere utilizzati anche per scopi diversi fino a
diventare veri tool di integrazione, ma questo esula dai fini di questo capitolo.
Ogni Appender ha una serie di importanti attributi e relazioni (diagramma delle classi di
figura 6.2), tra cui i più importanti sono:

• 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:

• SimpleLayout: si tratta di un layout molto semplice che si limita ad includere il livello di


severità del log ed il relativo messaggio.
• PatternLayout: questo layout si occupa di produrre in output un messaggio di log formattato
secondo il pattern fornito nel costruttore (qualora questo non venga definito, utilizza il
pattern di default). I pattern possono essere definiti secondo una ben definita gramma-
tica. Per esempio: %r [%t] %-5p %c %x - %m%n.
• DateLayout: si tratta di un layout astratto utilizzato per gestire le date.
• TTCCLayout: questo layout rappresenta una specializzazaione del precedente, e deve al
nome alla specifica tipologia di formattazione che include il tempo, il thead, la categoria,
e il contesto di informazione di diagnosi annidato (Time, Thread, Category and nested
diagnostic Context information).
• HTMLLayout: questo layout si occupa di formattare i messaggi di log in opportune tabelle
definite attraverso i tag HTML.
• XMLLayout: in questo caso la formattazione segue le regole della grammatica XML, o
quasi: l'XML prodotto non è ben formattato. L'idea è quella di affidare ad un altro
processore di testo il compito di sistemare l'XML prodotto. L'output è costituito da una
serie di log4j:event elements così come definito nel log4j.dtd.

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

Come si può notare la configurazione imposta:


1. l'abilitazione del debug;
2. il livello di log dell'elemento root a INFO;
3. l'appender della root (e quindi di tutti gli altri) a RollingFileAppender che si occupa di
scrivere i messaggi su un insieme di file di log. In particolare, si occupa di crearne un
altro non appena quello corrente abbia raggiunto una dimensione prefissata (in questo
caso 100KB). Inoltre il numero di file di log utilizzati sono 5: quindi l'appender parte dal
primo, poi dopo aver raggiunto una dimensione di circa 100 kilobyte si muove sul se-
condo, e così via, fino al completamento del quinto, per poi ricominciare dal primo;
4. la formattazione avviene attraverso il PatternLayout ed il pattern selezionato è: %d(yyyy-
M M - d d HH:mm:ss.SSSS) % p %t %c - % m % n .

La configurazione seguente imposta Log4J a inviare e-mail in caso di messaggi di errore:

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.

< ? x m l version="1.0" encoding= UTF-8" ?>


< ! D 0 C T Y P E log4j:configuration S Y S T E M log4j.dtd ">
<log4j:contiguration>
< ! — — >

<!— SMTPAppender —>


<!— —>
«appender name= " E M A I L APP' class="org.apache.log4j.net.SMTPAppender">
<param name="BufferSize" value= "512 / >
<param name="SMTPHost" value="smtp.foobar.com" />
<param n a m e = " F r o m " v a l u e = " a p p l J n z @ m b s e r v e r 0 1 . i t " / >
<param name="To" v a l u e = " a p p l _ n n z _ s u p p @ m b . i t " / >
<param n a m e = " S u b j e c t " v a l u e = " [ S M T P A p p e n d e r ] S u p p o r t m e s s a g e " />
<layout class="org.apache.log4j.PatternLayout">
<param name= " M y C o n v e r s i o n P a t t e r n "
value="[%dllS08601IJ%n%n%-5p%n%n%c%n%n%m%n%n" />
</layout>
<filter class="org.apache.log4j.varia.LevelRangeFilter">
< p a r a m n a m e = ' LevelMin ' v a l u e = " E R R O R " / >
< p a r a m n a m e = " L e v e l M a x " value="FATAL"/>
</filter>
</appender>

<!— —>
< ! — 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

2. d i c h i a r a r e e inizializzare la " c o s t a n t e " d i logging:


private static Logger LOGGER
= L o g g e r . g e t L o g g e r ( < c l a s s N a m e > . class. g e t N a m e Q ) ;

Figura 6.4 - Architettura concettuale di Java Logging.


Categoria Descrizione

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

CONFIG M e s s a g g i relativi alla c o n f i g u r a z i o n e e s e t u p dell'applicazione

FINE Utilizzata p e r s e g n a l a r e m e s s a g g i di d e t t a g l i o , c o m e debugging.

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

Tabella 6.3 - Categorie di log utilizzate dalla libreria java.util.logging.

3. utilizzare il logging:
LOGGER.finestfHello logging!");

Dall'analisi del frammento di codice riportato al punto 3 è possibile notare immediatamente


che le categorie (i livelli) utilizzate dalle due librerie sono diverse. In particolare, il logging
nativo di Java utilizza le categorie riportate nella tabella 6.3.
Analogamente a Log4j, anche java logging dispone dei due ulteriori livelli di severità Level.ALL
e Level.OFF utilizzati, rispettivamente, per abilitare e disabilitare tutti i log.
Come nel caso di Log4J, il Logger è una sorta di proxy che delega la scrittura del log ad una
particolare specializzazione di una classe di Handler. Le istanze di questa classe stabiliscono se
riprodurre o meno il log sul dispositivo di output in base al livello di log impostato. Per essere
precisi, anche i Logger hanno un livello di severità impostato. Questo tipicamente è posto al
livello di INFO. Pertanto tutti i messaggi di quel livello, o di livello superiore, sono notificati
all'handler. Da tener presente che sia i logger, sia gli handler, possono essere dotati di opportuni
filtri (classi che implementano l'interfaccia java.util.logging.f¡Iter dotata dell'unico metodo
isLoggable(LogRecord record) ). Questi forniscono l'opportunità di eseguire controlli a basso livel-
lo al di là delle restrizioni sul livello del log.
Nella java logging gli Appender sono stati sostituiti dagli Hander. In questo caso le opzioni
disponibili (come riportato in figura 6.5) sono:

• MemoryHandler: si tratta di un buffer circolare e pertanto memorizza i messaggi di log


(istanze della classe LogRecords) in un buffer di memoria di dimensione finita. Pertanto,
qualora questo risulti saturo, i log più vecchi sono rimossi per far posto ai nuovi. In
funzione a specifici eventi (livello del log, chiamata esplicita etc.), i vari log vengono
forniti all'handler di destinazione.
• ConsoleHandler: si occupa di pubblicare i messaggi di log nel System.err, formattati, per
default, in base alle direttive del SimpleFormatter.
• FileHandler: scrive i messaggi di log o su un file specifico o, a rotazione, su un insieme di
file. In quest'ultimo caso, non appena un file raggiunge una determinata dimensione, il
file viene chiuso, rinominato per causare la rotazione. La strategia per assegnare il nome
dei file di log usati è definita da un pattern fornito dall'utente. Per default, al nome è
aggiunto un numero progressivo. Il formatter di default di questo handler è XMLFormatter.
• SocketHandler: in questo caso i messaggi di log sono inviati ad un network stream remoto.
Anche in questo caso il formatter di default è XMLFormatter.

Apache Java Logging (JCL)


JCL (http://commons.apache.org/logging/guide.html) rappresenta una valida alternativa ai precedenti
due tool di logging. Per essere precisi non si tratta di una vera e propria alternativa giacché JCL
non è uno strumento di logging, bensì un sottile livello di astrazione dai tool di logging, un
bridge (o proxy) che consegna i messaggi ai tool specifici (figura 6.6). JCL è dotato di un
semplicissimo meccanismo di logging, il cui utilizzo è assolutamente sconsigliabile.
Grazie a JCL gli sviluppatori possono implementare codice utilizzando le API di questo
sottile strato di indirezione, demandando poi al tempo di configurazione la selezione dello
specifico strumento di logging desiderato.
JCL è in grado di funzionare con Log4J, il logging standard Java e Avalon LogKit (http://
avaion.apache.org/closed.html); tuttavia la relativa API somiglia molto a quella di Log4J. In effetti,
JCL è basato essenzialmente su due concetti: Log (il classico logger) e LogFactory (il meccanismo
che permette di generare/acquisire istanze di della classe di Log). Il meccanismo utilizzato per
individuare (discovery) quale log utilizzare è basato sui seguenti passi:

1. Cerca di individuare il valore della seguente proprietà: org.apache.commons.logging.Log. Nomi


minuscoli (log) sono accettati per garantire la retrocompatibilità (convenzione delle ver-
sioni precedenti alla 1.0). Gli attributi di configurazione possono essere specificati o diret-
tamente nel codice (pratica decisamente sconsigliabile) o, come avviene più comunemen-
te, in un apposito file: commons-logging.properties. Questo deve essere presente nel classpath.
Qualora questo file sia presente, ogni sua entry è acquisita come un parametro del log
factory. Qualora siano presenti diversi di questi file di configurazione il comportamento

java::utili::k>gging
Handler

java::ulìli::logging
SocketHandler

Figura 6.5 - Handler della java logging.


Apache JCL j

Log4J Avalon LogKit Java Logging

k J ^ > /

Figura 6.6 - Vista concettuale di Apache JCL.

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:

1. importare le librerie JCL:


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

2. ottenere il logger per la specifica classe:


public class MyClass I
private Log log = LogFactory.getLog(MyClass.class);

3. utilizzare i m e t o d i p e r il logging in b a s e alle specifiche esigenze:


log.fatal(Object message);
log.fatal(Object message, Throwable t);
log.error(0bject message);
log.error(Object message, Throwable t);
log.warn(Object message);
log.warn(Object message, Throwable t);
Iog.in1o(0bject message);
log.info(Object message, Throwable t);
log.debug(Object message);
log.debug(Object message, Throwable t);
log.trace(Object message);
log.trace(Object message, Throwable t);

4. q u a l o r a necessario, utilizzare le seguenti c o n d i z i o n i di g u a r d i a :


log.isFatalEnabledQ;
log.isErrorEnabled();
log.isWarnEnabledO;
log.islntoEnabled();
log.isDebugEnabled();
log.isTraceEnabled();

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.

Quale tool di logging utilizzare?


La selezione del tool di logging da utilizzare è una delle decisioni ricorrenti in ogni progetto. La
notizia buona è che, caso raro, non rientra nell'insieme delle decisioni in grado di minare il
successo del progetto: tutte e tre le opzioni risultano decisamente valide. La notizia negativa
invece è che una volta stabilita una strategia, considerato l'elevato grado di intrusività intrinse-
co del logging, risulta abbastanza complicato cambiarla.
La scelta personale dell'autore tende a ricadere sull'Apache Java Logging. Questo perché si
tratta di un leggerissimo grado di indirezione verso i vari tool, quindi la selezione del tool
specifico è rinviata a tempo di configurazione e può essere variata più facilmente.
La 6.4 tabella mostra alcuni tra i più importanti vantaggi e svantaggi dei vari tool.

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à.

6.1.1 Produrre la documentazione contemporaneamente


all'attività di codifica
Come spesso accade in altri contesti, la strategia migliore per implementare efficaci meccani-
smi di logging consiste nello stabilire una politica di logging standard anche prima di dar luogo
all'implementazione del codice e quindi nell'applicarla rigorosamente durante l'attività codifi-
ca. Questa strategia permette di ottenere ottimi risultati evitando sprechi di tempo.
Troppo spesso accade di dover rendere consistente il logging o addirittura di doverlo inserire
a ridosso della messa in esercizio del sistema o a posteriori. In questi casi, per una serie di
fattori, non ultimo lo "stress da rilascio", si tende a produrre modesti risultati con un grande
investimento di tempo.

6.1.2 Non eseguire il logging attraverso il metodo System.out.println


Come illustrato nel corso del capitolo, sebbene una strategia di logging basata sull'utilizzo del
metodo System.out.println possa sembrare semplice e rapida da implementare, in realtà genera
tutta una serie di gravi problemi, tra i quali i più severi sono:

• non esiste un meccanismo automatico per abilitare/inibire i messaggi di log;


• non è immediato implementare un sistema atto ad organizzare i log per categorie;
• non è possibile eseguire un fine-tuning dei log;

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.

Grado di D a t a la l u n g a p r e s e n z a sul m e r c a t o si S e b b e n e sia il l o g g i n g i n c l u s o n e l J D K ,


utilizzo tratta del t o o l m a g g i o r m e n t e u t i l i z z a t o n o n è a n c o r a r i u s c i t o a c o n t r a s t a r e il
dai s o f t - w a r e realizzati d a t e r z e p a r t i . g r a n d e s u c c e s s o di L o g 4 j . C o m u n q u e si
t r a t t a della s c e l t a o b b l i g a t o r i a di tutti i
prodotti Sun.

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 .

Ricchezza di Molto ricco R i c c o . H a t u t t e le f u n z i o n i n e c e s s a r i e , m a


feature n o n p r e s e n t a un i n s i e m e c o s ì r i c c o c o m e
q u e l l o 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.

Tabella 6.4- Pro e contro dei due principali tool di logging.


• inefficienza implementativa che incide pesantemente sulle performance del sistema;
• difficoltà di consultazione dei record di log. Messaggi inviati al dispositivo standard di
output sono poco utili sia per la fase di sviluppo, sia per la diagnosi in produzione;
• ridotta fattibilità,: per esempio gli application server non mostrano questi messaggi.

6.1.3 Utilizzare un logger per classe


Come visto nei paragrafi precedenti, i logger sono degli oggetti dotati di un nome univoco che
ne consente il reperimento. Pertanto, sebbene sia possibile utilizzare diverse convenzioni circa
il nome da assegnare, quella migliore resta utilizzare il percorso completo della classe:

private static Logger LOGGER = Logger.getLogger(<className>.class.getName());

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.

6.2 Porre attenzione al contenuto del log


La misura dell'efficacia del meccanismo di log è direttamente proporzionale alla qualità del
contenuto dei messaggi. Pertanto, anche il più sistematico degli approcci potrebbe non genera-
re alcun vantaggio qualora i vari record producano dati, rumore e non informazione. Quindi, è
necessario porre attenzione al contenuto dei record dei log. In particolare è consigliabile ripor-
tare frasi ben strutturate, concise ma complete.

6.2.1 Ponderare l'utilizzo del metodo toString()


Uno dei metodi più immediati di eseguire il logging consiste nell'invocare il metodo toString()
sull'oggetto di cui si vuole mostrare il contenuto. Per esempio

LOGGER.infofReceived a new (rade message ("+newMessage.toString()+") ");

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:

• non sempre il metodo toStringQ è implementato correttamente e produce output leggibile.


• spesso eseguire il log di un grafo di oggetti finisce per incidere sulle performance. Per
esempio, nel caso predente, il messaggio ricevuto potrebbe essere la radice di un grafo
di oggetti molto complesso la cui riproduzione sul target del log potrebbe risultare one-
roso in un contesto di elevato throughtput. In questi casi, spesso è probabilmente più
conveniente limitare il contenuto del log ad alcune informazioni autoesplicative, come il
codice univoco del trade, la counterparty, la propria unità organizzativa, etc.

6.2.2 Riportare solo le informazioni necessarie, ma tutte


I file di log di applicazioni medio/complesse tendono rapidamente a diventare estremamente
corposi. Non a caso i tool di logging, come per esempio Log4J, prevedono opportuni meccani-
smi (appender) in grado di utilizzare una serie di file di log invece di uno solo. Ciò nonostante
è opportuno curare le informazioni riportate nel log, per una serie di motivi, come aumentare
l'efficienza del codice e agevolare le attività di reperimento delle informazioni desiderate. Per-
tanto, una buona strategia consiste nel riportare solo le informazioni strettamente necessarie,
ma tutte, evitando, quanto possibile inutili decorazioni.

6.2.3 Valutare l'eventualità di implementare utility da utilizzare


per la "formattazione" dei messaggi di log
Un'importante caratteristica che i log dovrebbero sempre implementare è la consistenza. Que-
sta proprietà offre una serie di importanti vantaggi, come per esempio:

• 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.

6.2.4 Evitare facili ironie nel file di log


Sebbene questa regola possa sembrare fin troppo scontata, non è infrequente imbattersi in log
colmi di commenti ironici o presunti tali. Sebbene l'ironia sia, in generale, una caratteristica
molto positiva, utilizzarla per i log di un'applicazione rappresenta una scelta sbagliata: di solito
il personale destato nel cuore della notte per risolvere un improvviso problema nel sistema non
trova poi così utile o simpatico leggere messaggi del genere. Molto più apprezzati sono messag-
gi chiari, concisi, in grado di agevolare e semplificare il lavoro di individuazione dei problemi.
Lasciamo la sacrosanta ironia per i libri e gli articoli.

6.3 Utilizzare correttamente i livelli di log


Quasi tutti i tool per il logging offrono la possibilità di organizzare i propri log in funzione di
un insieme ben definito di log. Log4J, per esempio, rappresenta queste categorie attraverso la
classe Priority implementata dalla classe Level. I livelli di log offrono tutta una serie di vantaggi
che però vengono meno qualora non siano utilizzati correttamente.

6.3.1 Utilizzare correttamente il livello di severità


I messaggi di log devono essere comunicati utilizzando il corretto livello di severità. Ciò è
necessario per una serie di importanti motivi come:

• 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.

6.3.2 Porre attenzione all'utilizzo dei livelli di debug e info


Questa regola è una sorta di corollario alla precedente, che tuttavia è il caso di riportare conside-
rando sia le discussioni, sia gli errori commessi nell'uso di questi livelli. Sebbene a prima vista i due
livelli possano sembrare molto simili, ci sono importanti differenze che bisogna tenere a mente. In
particolare, debug deve essere utilizzato per informazioni relative alla configurazione dell'applica-
zione e per informazione di dettaglio, mentre info deve essere utilizzato per riportare informazioni
relative all'applicazione, per segnalare eventi significativi, come quelli relativi al ciclo di vita di
importanti entità manipolate dall'applicazione, etc. In particolare, i messaggi di info relativi a uno
specifico servizio dovrebbero essere in grado di specificare in maniera chiara lo stato del servizio
stesso. Per esempio, la ricezione di un messaggio dovrebbe essere un logging info, mentre la comu-
nicazione di ogni dettaglio del messaggio dovrebbe essere riportato con il livello debug.
In caso di incertezza, una semplice regola pratica consiste nel porsi il quesito se sia o meno
necessario che il messaggio in questione sia riportato dal sistema in produzione. In caso di
risposta affermativa, allora il livello del log deve essere info, altrimenti si tratta di un debug.
Tutte le informazioni non desiderabili per sistemi in esercizio vanno riportate come debug.
Da tener presente che un uso scorretto dei due livelli potrebbe portare ai seguenti due scenari:

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).

6.3.3 Valutare bene la necessità di definirsi dei propri livelli di log


Quasi tutti i tool per il log forniscono l'opportunità di definirsi un proprio insieme di livelli di
log o di estendere quello esistente. In Log4j, per esempio, ciò è possibile estendendo la classe
Level. Sebbene ciò sia possibile, si tratta di una pratica sconsigliata: l'insieme dei livelli disponi-
bili dovrebbe essere più che sufficiente per la stragrande maggioranza dei sistemi.

6.4 Valutare l'impatto sulle prestazioni


Come spesso accade durante la produzione del software, è necessario prendere delle decisioni
oculate tra esigenze contrastanti. Ciò si applica anche al caso dei logging. Infatti, se da un lato
potrebbe essere utile eseguire un logging dettagliato dell'applicazione, dall'altro ciò risultereb-
be eccessivamente penalizzante dal punto di vista delle performance. Pertanto, durante
l'implementazione del meccanismo di logging è necessario tenere sempre a mente l'impatto
sulle performance.

6.4.1 Evitare un log eccessivo


Un prima condizione utile alle performance del sistema consiste nello scegliere il corretto livel-
lo di dettaglio del logging. Si tratta di una decisione difficile giacché non esiste una regola
semplice e spesso è necessario prendere delle decisioni basate sul contesto. A volte è possibile
incontrare dei veri "adepti del logging": per evitare ogni problema alla radice, si esegue il log di
ogni passo o addirittura ad ogni ingresso e uscita da ciascun metodo. Questa strategia, sebbene
molto semplice, non è una tecnica vincente. Ciò finisce inevitabilmente per creare una serie di
problemi: il codice diviene meno chiaro per via delle troppe istruzioni dedicate al logging, i
troppi dati prodotti finiscono per generare rumore e non informazione e, fatto importante, si
penalizzano fortemente le performance.

6.4.2 Ricordarsi di inserire opportune guardie del codice


Con il termine di "guardie del codice" (code guards in inglese) si fa riferimento a opportune
porzioni del codice utilizzate per abilitare/inibire l'esecuzione di specifiche istruzioni di codi-
ce. In questo contesto si tratta di meri costrutti if utilizzati per abilitare/inibire le invocazioni
del logger. Per esempio:

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.

6.4.3 Evitare servizi non richiesti e dispendiosi


Dovendo poter essere utilizzati nelle circostanze più diverse, i vari tool di log finiscono per
offrire tutta una serie di servizi: molti di questi non sono necessari nella maggior parte dei casi,
e comunque gravano sulle performance. Un esempio tipico consiste nel meccanismo utilizzato
da Log4J di comunicare i record di log allo specifico handler e a tutti i suoi antenati. Questa
funzionalità, nella maggior parte dei casi, non è necessaria, ma comunque utilizza utili risorse.
Questo meccanismo può essere eliminato attraverso il seguente setting:

LOGGER.setUseParentHandlers(false);

6.4.4 Utilizzare appender asincroni


Molto spesso di fronte ad applicazioni vincolate da requisiti prestazionali molto stringenti in
cui anche frazioni di secondo siano in grado di fare la differenza, molti tecnici decidono di
rinunciare al logging. Prima di optare per una soluzione così drastica è consigliabile valutare la
possibilità di ricorrere a appender asincroni.
Quasi tutti i tool forniscono meccanismi di questo tipo in grado di disaccoppiare
temporalmente la richiesta del log (che quindi non risulta più bloccante) dall'effettivo
espletamento. Per esempio, in Log4j AsynchAppender.

6.5 Implementare un corretto logging delle eccezioni


Una corretta gestione delle eccezioni, come visto nel Capitolo 5, rappresenta un meccanismo
fondamentale per ciascun sistema. Un elemento importante di questo processo è costituito dal
logging delle eccezioni. Spesso 0 sistema non è in grado di gestire l'eccezione e quindi non resta
altro da fare che assicurarsi di riportarla correttamente. Si tratta di un'attività importantissima
sia perché i log spesso forniscono al personale addetto alla manutenzione le uniche informazio-
ni disponibili su eventuali problemi e sia perché i file di log sono tipicamente monitorati da
sistemi esterni atti a individuare condizioni di errore che richiedono un intervento immediato.

6.5.1 Evitare di invocare lo stacktrace


Ogniqualvolta sia necessario eseguire il log delle eccezioni, è necessario ricordare che i tool per
il logging sono dotati di opportuni metodi. Pertanto, è sufficiente utilizzare tali metodi (per
esempio LOGGER.error(" exception", ioException) ) invece di cercare di implementare comporta-
menti già definiti, come per esempio riportare lo stacktrace in una stringa (myException
.printStackTrace(myPrintStram)).

6.5.2 Riportare correttamente le eccezioni


Il logging delle eccezioni è una parte molto importante del meccanismo di gestione delle ecce-
zioni e pertanto è opportuno assicurarsi di riportare tutte le informazioni necessarie. Quindi, a
meno di casi specifichi in cui le eccezioni siano state attentamente codificate o il contesto sia
chiarissimo, bisogna eseguire il log dell'intera eccezione, evitando scorciatoie come quella di
riportare il solo messaggio (LOGGER.errorfEccezione :"+ioExc.getMessage()).

6.5.3 Non eseguire il log ad alta severità delle eccezioni gestite


Le eccezioni sono utilizzate per segnalare condizioni anomale verificatesi in parti del codice in
cui non si abbiano sufficienti informazioni per attuare un comportamento specifico atto a risol-
verle. Pertanto attraverso il meccanismo delle eccezioni si rimanda indietro, a porzioni del
codice chiamante, la segnalazione dell'anomalia nella speranza che si giunga a un livello dotato
di maggiori informazioni per intraprendere opportuni passi di correzione. Spesso, ahimè, non
resta altro da fare che segnalare l'eccezione, mentre, in altri casi, è possibile tentare di corregge-
re l'eccezione. Per esempio, in presenza di determinate eccezioni di sistema, è possibile ritentare
lo scenario sperando che l'anomalia abbia carattere temporaneo. Qualora un'eccezione sia
gestibile, è opportuno non eseguirne il log oppure riportare l'eccezione a livello di debug.
Segnalare come errore un'eccezione gestita genera l'unico effetto di procurare il mal di testa al
personale addetto alla manutenzione del sistema.

6.5.4 Riportare le eccezioni con il corretto livello di severità


Eccezioni non gestibili devono essere segnalate o come errore (LOGGER.error("Exception ", ioExcp))
nel caso in cui l'anomalia crei problemi unicamente alla transazione corrente, oppure come
eccezione fatale nel caso in cui il sistema sia impossibilitato a procedere.
6.5.5 Eseguire il log delle eccezioni una sola volta
L'implementazione di un qualsiasi servizio non banale richiede la collaborazione di un certo
numeri di metodi, spesso forniti da classi poste a diversi livelli dell'architettura. Poiché poi le cose
possono andare male (e lo fanno!), è necessario implementare una corretta gestione delle eccezio-
ni per rendere il sistema più robusto. Questo fa sì che un'eccezione generata a un livello basso
dell'architettura, frequentemente, venga rinviata indietro via via fino ai livelli più alti. A questo
punto bisogna porre attenzione a evitare che una stessa eccezione sia riportata più volte nel log.
Eccezioni riportate diverse volte rallentano le prestazioni del sistema, riducono l'efficacia
del personale coinvolto nell'analisi di possibili problemi, e rendono più difficile il compito di
eventuali tool atti a monitorare i file di log.

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:

• richiede di scrivere molto codice di log;


• fa sì che tutte le eccezioni siano riportate nel log, anche quelle gestibili;
• tende a creare qualche problema di incoerenza qualora il codice sia riutilizzato in diversi
sistemi;
• può ridurre l'efficacia del logging giacché raramente il componente alla fine dello stack
ha sufficienti informazioni per riportare un log coerente e completo;
• tende a generare l'odioso problema di log multipli per lo stesso problema.

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:

• vi è un solo punto logico di controllo;


• è più semplice da implementare;
• si evita agevolmente il problema dei record multipli;
• non si creano problemi nel riutilizzo dei componenti per via dei una diversa strategia di
logging, e così via.
A questi vantaggi, tuttavia vanno controbilanciati alcuni svantaggi: per esempio, non sempre
è possibile evitare che alcuni problemi siano riportarsi dal basso (questo è il caso in cui si
utilizzino librerie offerte da terze parti).

6.5.7 Valutare la possibilità di eseguire un log fortemente strutturato


La realizzazione di sistemi ad alta disponibilità/affidabilità richiede di includere diversi accor-
gimenti che altrimenti non sarebbero necessari in altre situazioni. Per esempio, è necessario
progettare meccanismi in grado di rilevare tempestivamente eventuali componenti e sistemi
malfunzionanti o gravati da un eccessivo stress. A tal fine, una delle tecniche largamente utiliz-
zate consiste nel ricorrere a sistemi dedicati al monitoraggio e alla analisi dei file log prodotti
dalle varie applicazioni. Ciò al fine di individuare dei pattern predefiniti che identifichino situa-
zioni di malfunzionamento (per esempio una linea di testo iniziarne con la stringa "ERROR") o di
warning.
Al fine di rendere possibile l'impiego di questi sistemi di controllo è opportuno assegnare ai
log, specialmente a quelli relativi dall'insorgere di un'eccezione, opportuni formati.
Una struttura abbastanza ricorrente consiste in una lista di attributi separati da caratteri
bianchi, in cui le diverse informazioni prevedono:

• 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:

<exception_date_time> <log_level> <unique_id> <class_path> <message> ([arguments]]

Per esempio:

2 0 0 6 - 1 2 - 0 3 12:51:07.103 W R N SDS-CN01 com.mokabyte


.tool.umlclassgenerator.service.DiagramParser -'diagram not found' diagram=class34

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.

I vantaggi dei test di unità


Produrre i test di unità è indubbiamente un lavoro dispendioso; tuttavia, si tratta di un'attività
fondamentale e come tale, va considerata come un vero e proprio investimento e non come una
dissipazione di risorse. Un'opportuna implementazione dei test di unità offre una indubbia
serie di vantaggi.

• Individuare rapidamente gli errori. In particolare, questi test permettono di individuare


eventuali malfunzionamenti, non appena redatto il codice (secondo alcuni estremisti
anche prima!). Inoltre, l'indagine è spinta a un livello molto approfondito, tipicamente
superiore a quello raggiunto con il classico test randomico. La logica conseguenza è un
significativo risparmio di tempo e quindi di denaro. Frequentemente, errori individuati
in fasi successive del processo di sviluppo del software, durante i test di accettazione o
addirittura a sistema in produzione, risultano difficili da investigare, da risolvere e, in
ultima analisi, decisamente più onerosi.
• Aumentare la qualità del codice. La verifica estensiva del codice permette di individuare
e risolvere un'elevata quantità di errori, spesso anche quelli più nascosti, e quindi di
garantire una maggiore qualità del sistema nel suo complesso. Ciò, inoltre, semplifica la
produzione del codice basato su quanto già prodotto, aumentando, contestualmente, il
grado di confidenza degli stessi programmatori.
• Semplificare il processo di refactoring. La presenza di estesi test di unità e di integrazio-
ne permettono di minimizzare gli inevitabili fattori di rischio connessi a estesi processi
di refactoring e, allo stesso tempo, aumenta il livello di sicurezza del team di sviluppo. In
effetti, eventuali problemi introdotti dai processi di aggiornamento (chiamanti regression
bug, errori regressivi) sono immediatamente individuati dalla batteria di test. La presen-
za di questi test, quindi, minimizza il rischio di degenerare la qualità del sistema a segui-
to di processi di modifica del codice e fornisce ai programmatori la tranquillità e la
fiducia necessaria per affrontare importanti aggiornamenti del codice. Non è infrequen-
te il caso in cui l'assenza di estesi testi di unità, finisca per far desistere o comunque per
scoraggiare i programmatori dall'eseguire importanti variazioni del codice.
• Semplificare il processo di integrazione . Sebbene la pertinenza dei test di unità, per
loro stesso principio, non riguardi l'integrazione di parti del sistema, il fatto che ogni
elemento sia stato verificato in dettaglio, prima di essere assemblato in elementi più
grandi, riduce il livello di incertezza e quindi semplifica e velocizza il processo stesso di
integrazione.
• Migliorare la documentazione . In alcuni casi, lo studio di determinate API o di parti di
codice è notevolmente semplificato dall'analisi di esempi concreti come quelli imple-
mentati nei test di unità. Pertanto, tali test possono essere considerati a tutti gli effetti
come parte integrante della documentazione del codice, oltretutto assolutamente in li-
nea con l'ultima versione del codice, cosa che spesso non succede con la tradizionale
documentazione: non è infrequente il caso in cui i programmatori tendano a dimentica-
re di mantenere la documentazione sincronizzata con il codice.

Processi iterativi e incrementali


Oltre all'importanza dei test, un'altra pratica che trova consenso tra la quasi totalità dei moder-
ni processi di sviluppo del software, indipendentemente dalla loro diversa natura, è la necessità
di integrare approcci iterativi e incrementali nel processo di sviluppo del software prescelto.
L'idea alla base di questi consiste nel produrre il sistema finale attraverso una serie opportuna-
mente pianificata di incrementi. L'intero processo di sviluppo del software si risolve in un
insieme di mini progetti, ognuno dei quali produce una nuova versione (incremento) del siste-
ma finale. Gli incrementi possono essere relativi sia allo sviluppo di nuovi servizi, sia alla varia-
zione tecnologica di alcune parti del sistema (processi di refactoring).
Questo approccio è molto conveniente per una serie di motivi: permette di controllare più
accuratamente l'evoluzione del sistema, di gestire attivamente i fattori di rischio concentrando
rischi maggiori nelle prime versioni del sistema, fa sì che, ad ogni iterazione, il personale possa
concentrarsi su un ben definito insieme di elementi, permette di ottenere il feedback continuo
ed anticipato degli utenti, e così via.
Tuttavia, a dispetto di questi importantissimi vantaggi esistono degli inevitabili effetti
collaterali. In particolare, è tipico degli approcci iterativi e incrementali che stesse porzioni di
codice siano variate con una certa frequenza durante l'evoluzione del processo. Logica conse-
guenza di ciò è che anche i relativi test di unità debbano essere soggetti a continui processi di
aggiornamento/riscrittura. Ciò comporta una serie di importanti conseguenze.

• I test di unità devono presentare un adeguato livello di qualità. Pertanto la tradizionale


equazione codice test = bassa qualità perde di validità. In primo luogo, test mal scritti ten-
dono a generare ripercussioni negative e quindi a ridurre i vantaggi propri relativi alla loro
presenza; poi, il processo di manutenzione risulta complicato e quindi, in ultima analisi,
può addirittura causare il rallentamento dell'intero processo di sviluppo del sistema.
• I test devono essere disegnati ed implementati pensando all'estensibilità, ovviamente
senza cadere nella facile tentazione àéRover-engineering. Ciò perché è tipico dei proces-
si iterativi ed incrementali generare la variazione di determinate parti di codice. Pertan-
to, gli stessi codici di test devono essere scritti accuratamente soprattutto per quanto
riguarda le parti che presentano un'elevata possibilità di dover essere variati.
• i test diventano parte del sistema e come tale sono soggetti a modifiche richieste dalle
varie iterazioni. Pertanto, sebbene sia necessario verificare la maggior parte possibile del
sistema, è altresì necessario evitare di scrivere test ridondanti che, come tali non aggiun-
gono valore, ma comunque devono essere mantenuti.
La copertura dei test di unità
Uno degli argomenti ricorrenti nella produzione dei test di unità, oggetto soprattutto in passa-
to di accesi dibattiti, riguarda il livello di copertura di tali test (rapporto tra le linee di codice
eseguite dai test e quelle totali). In teoria, questi test dovrebbero interessare tutte le classi
incluse nel sistema che si sta sviluppando (1 a 1), escludendo, ovviamente, le classi appartenen-
ti al JDK e le librerie esterne, il cui rilascio è soggetto al superamento di uno specifico e appro-
fondito processo di test e di Quality Assurance. Anche se questo processo non sempre è infal-
libile, interviene un problema di ordine pratico: tentare di verificare tutte le classi del J D K o
delle librerie fornite da terze parti causerebbe un dispendio di energie assolutamente impropo-
nibile. Pertanto, giocoforza, è necessario far affidamento sui test eseguiti dai fornitori delle
librerie utilizzate, ed eventualmente scrivere specifici test solo qualora ci siano fondati sospetti
che determinate classi/metodi non presentino il funzionamento atteso. Infine, in ultima analisi,
è necessario considerare che le classi appartenenti a librerie esterne sono comunque verificate,
sebbene indirettamente, attraverso i test relativi alle classi sviluppate che ne incapsulano il
comportamento. I test di unità, quindi, dovrebbero riguardare tutte le classi prodotte. Il quesi-
to però che resta da sciogliere è relativo a quali metodi verificare e come.
Nella pratica, ci si può imbattere in professionisti un po' estremi che suggeriscono test in grado
di verificare ogni possibile dettaglio, ogni possibile linea di codice del sistema. Questa strategia,
contrariamente a quanto si sarebbe portati a pensare e a dispetto del relativo enorme consumo di
tempo e budget, raramente risulta efficace, ed è inoltre percorribile solo in un limitato insieme di
casi. La copertura esaustiva (100%) del sistema è inutile e spesso addirittura deleteria.
In primo luogo, i test di unità non verificano assolutamente i requisiti del sistema ma si
occupano di verificare il funzionamento dei singoli componenti; qualora lo facessero poi, non è
sempre possibile anticipare tutte le possibili combinazioni degli stimoli di input di un sistema,
tanto meno è possibile ricreare tutti gli scenari di evoluzione dei thread presenti in un sistema
multi-threaded da verificare, e così via. Pertanto anche nel caso in cui si riuscisse, attraverso i
test di unità, a esercitare tutte le linee di codice del sistema (copertura = 100%), ciò non assicu-
rerebbe assolutamente il corretto funzionamento dello stesso. I test di unità, sono solo un pri-
mo livello delle procedure di test del sistema, le quali devono essere pianificate attentamente
anche prima dell'inizio dell'implementazione del sistema. Il matematico Kurt Godei [GDLPRF]
ha ulteriormente dimostrato come non sia possibile provare tutti gli aspetti di un sistema dal-
l'interno del sistema stesso. In particolare, ha dimostrato che tutti i sistemi logici contengono
necessariamente una serie di assiomi, ossia verità che devono essere accettate per buone e la cui
verifica può essere effettuata solo al di fuori del sistema stesso.
Coperture esaustive sono difficilmente raggiungibili anche per motivi pratici; per esempio,
tutti i sistemi includono delle parti di codice semplicemente non raggiungibili, come catch che
possono anche non verificarsi mai. Alcuni tecnici, in tali condizioni, commettono l'errore di
modificare il codice per "ottenere" una copertura completa. Quindi, è importante capire che
realizzare test esaustivi del sistema è una strategia non sempre efficace e realizzabile, e pure
dispendiosa. Si consideri il frammento di codice riportato di seguito. Si tratta dell'implementazione
del metodo equals di un Data Transfer Object/Value Object (DTO/VO, ossia semplici oggetti
Java, POJO, utilizzati per trasportare dati). Questo oggetto possiede i seguenti attributi: id, login,
name, middlename, stimarne, dob (date of birth), email (principale), addresses (oggetto composto che
contiene ogni tipo di recapito aggiuntivo: e-mail, telefono, cellulare, pager, indirizzo, etc.),
userStatus (stato dell'utente) e orgllnit (organhational unti, dipartimento di appartenenza).
public boolean equals(Object obj) I

if (this == obj) {
return true;

it (Isuper.equals(obj)) I
return false;
I

if (getClass() != obj.getClass()) I
return false;
I

final UserDTO other = (UserDTO) obj;

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

(her id !=null 'kj equals(other id|

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

La verifica completa di questo semplice metodo richiederebbe di scrivere circa 25 test! La


figura 7.1 fornisce una rappresentazione del grafo dei diversi percorsi che un test completo
dovrebbe coprire. I test da produrre per verificare i diversi percorsi del metodo equals sono
illustrati nella tabella 7.1.
E veramente necessario produrre test capaci di esercitare tutti i possibili percorsi del metodo
equals? In definitiva, no... anche se ogni percorso non analizzato finisce con il ridurre la percen-
tuale di copertura. Il costo di produzione iniziale e di manutenzione di test esaustivi è, nella
quasi totalità dei casi, insostenibile. Nel caso del metodo equals, si consideri, per esempio, l'im-
patto generato sul codice di test relativo all'introduzione di un nuovo attributo.
Quindi è importante discernere tra i metodi che non è necessario verificare approfondita-
mente e quelli di cui invece è necessario verificare ogni singolo dettaglio.
Si consideri come ulteriore esempio il caso di semplici metodi getXXX e setXXX; a meno che
questi non eseguano ulteriori operazioni, non è estremamente utile verificarli. Inoltre, poiché i
metodi accessori (getXXX) dovrebbero essere l'unico modo per accedere ai dati di un oggetto,
tali metodi sono comunque oggetto di investigazione da parte di altri test.
Qualora il metodo da verificare fosse l'implementazione di un algoritmo di un servizio business
di rilevanza fondamentale, allora probabilmente la situazione sarebbe diversa. In tal caso infatti,
a differenza del metodo di uguaglianza, sarebbe opportuno verificarne tutti i possibili percorsi.
Sebbene non sia possibile dar luogo a una copertura esaustiva, questa deve essere sicuramen-
te estensiva. Coperture insufficienti possono generare una serie di dannosissimi effetti collaterali,
quali ad esempio una bassa qualità del codice, una falsa sensazione di robustezza, continui
malfunzionamenti del sistema in produzione a seguito di processi di aggiornamento dovuti a
test di regressione incapaci di eseguire approfondite analisi del sistema, etc.
Purtroppo, nella pratica lavorativa, accade anche spesso di imbattersi in progetti in cui i test
di unità coprono una minima porzione del codice... Coperture inferiori al 50-60% del codice
dovrebbero generare qualche preoccupazione al capo progetto di turno.
Anche se la produzione di questi test può sembrare un'attività eccessivamente dispendiosa e
poco sostenibile, soprattutto in situazioni in cui il progetto risulti costretto da severi vincoli in
termini di tempo e denaro, si tratta di una strategia che permette di realizzare più rapidamente
sistemi di maggiore qualità; e la loro validità è evidenziata ad ogni iterazione/processo di
refactoring. Pertanto, l'impegno profuso nella scrittura dei test di unità è ripagato, nel medio/

Num Descrizione Esito


i T u t t i gli a t t r i b u t i d e i d u e o g g e t t i i m p o s t a t i a l l o s t e s s o v a l o r e n o n n u l l o true

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 del m e t o d o s u d u e o g g e t t i u g u a l i c o n tutti gli e l e m e n t i i m p o s t a t i a n u l i true

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 tutti gli e l e m e n t i u g u a l i , e c c e t t o l ' i d del


6 false
p r i m o i m p o s t a t o a nuli

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 tutti gli e l e m e n t i u g u a l i e c c e t t o gli id


7 false
(diversi m a n o n nulli)

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

I n v o c a z i o n e del 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


9 false
login (diversi m a n o n nulli).

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 c o n tutti 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


24 false
orgllnit del p r i m o i m p o s t a t o a nuli

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 tutti 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


25 false
orgllnit (diversi m a n o n nulli)

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.

JUnit prima della versione 4


Tradizionalmente una classe di test presentava una struttura simile a quella riportata di seguito.

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.

protecled void setUp() I

orgllnitBO = EasyMock.createMock(IOrgUnitBO.class):
profileBO = EasyMock.createMock(IProfileBO.class);
userBO = EasyMock.createMock(IUserBO. class);

UserBS userBS = new UserBS();

userBS.setUserBO(userBO);
userBS.setProfileBO(profileBO);
userBS.setOrgl)nitBO(orgUnitBO);

this.userBS = userBS;
I

protected void tearDown() throws Exception {

EasyMock.ve/-/fy(new Object[] ( userBO, profileBO, orgUnitBO 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:

public class MyUnitSuite I

// CONSTANTS SECTION

// list ot test classes to run


private static Test[] testToRun = (
XYZTest.suite(),
MyTest.suite()

// ATTRIBUTES SECTION

// METHODS SECTION

/ "

' Return the test suite defined in this class


' Other test classes has to be added in the testToRun attribute
" © r e t u r n TestSuite - test suite related to the VO classes
7

public static TestSuite suite() {

TestSuite suite = new TestSuite("My Test Suite");

for (int ind = 0; ind < testToRun.length; lnd++) I

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.

Brevemente, il meccanismo delle annotazioni permette di introdurre, direttamente nel codice


s o r g e n t e , delle m e t a - i n f o r m a z i o n i . Q u e s t e r a p p r e s e n t a n o i n f o r m a z i o n i , più p r e c i s a m e n t e
"dichiarazioni", circa il codice stesso. Il meccanismo è del tutto equivalente a quello utilizzato
dal parser J a v a D o c per generare la documentazione: vi è una parola chiave, preceduta dal carattere
Tuttavia, in questo caso, l'utilizzo è decisamente più circoscritto per evitare ambiguità. Per
esempio è possibile dichiarare che un m e t o d o sia deprecato p r e m e t t e n d o alla relativa dichiarazione
la parola chiave @deprecated:

@deprecated public void destroy()

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:

• non occorre importare il package: junit.framework.TestCase;


• bisogna importare le annotazioni di test (org.junit.Test);
• non è più necessario che le classi di test estendano dalla classe TestCase;
• non è obbligatorio aggiungere il suffisso test ai metodi di test: questi sono evidenziati
attraverso l'utilizzo dell'annotazione @Test.

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:

• eventuali eccezioni attese: @Test (expected=<nome eccezione;-.class); test definiti in questo


modo hanno successo solamente se l'eccezione attesa è effettivamente eseguita;
• un eventuale timeout: @Test (timeout=6000); questo parametro permette di specificare il
tempo massimo, espresso in millisecondi, a disposizione del test per essere eseguito:
un'eventuale durata eccedente il timeout genera l'automatico fallimento del test.

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.

7.1.1 Investire nella qualità dei test di unità


Dalla definizione dei test di unità consegue che il rapporto esistente tra questi test e il codice
testato dovrebbe essere un'approssimazione della relazione l a i . Giocoforza, gli stessi test
finiscono per far parte del sistema stesso. Pertanto non dovrebbe sorprendere il fatto che prò-
cessi di refactoring del codice generino importami ripercussioni sui test di unità. Logica conse-
guenza quindi è che i processi di refactoring non sono limitati al codice sorgente ma si estendo-
no, necessariamente, ai test. Al fine di semplificare il relativo processo di aggiornamento è
quindi necessario che questi presentino un adeguato livello di qualità, altrimenti si potrebbe
giungere alla fastidiosa situazione caratterizzata dal processo di sviluppo rallentato dalle attivi-
tà di manutenzione dei test.

7.1.2 Scrivere i test di unità non è un'attività banale


Come si vedrà nel corso di questo capitolo, scrivere test di unità non è assolutamente un'attivi-
tà banale. I test di unità sono parte integrante del sistema e come tali necessitano di essere
rivisitati e aggiornati con l'evoluzione del sistema; la loro scrittura richiede la conoscenza di una
serie di strumenti e tool quali i sistemi di controllo di copertura, di implementazione di oggetti
mock up etc. La scrittura dei test di unità, inoltre, fornisce una prima valutazione sulla qualità
del disegno delle parti oggetto di verifica, e così via. Pertanto, la scrittura dei test di unità è
un'attività che va pianificata accuratamente, assegnata a personale qualificato e soprattutto
eseguita contemporaneamente alla scrittura del relativo codice.

7.1.3 Evitare l'over-engineering


Implementare i test ponendo attenzione alla loro qualità non equivale assolutamente a dissipa-
re tempo nella scrittura di test ultra flessibili, super efficienti, etc. I test sono un'importante
parte dello sviluppo del software, sono oggetto di continui refactoring, ma comunque non è
opportuno renderli più flessibili e sofisticati di quello che dovrebbero effettivamente essere. Il
tempo necessario per implementare test eccessivamente raffinati, over-engineered, per metterli
a punto, per spiegarli ad altre persone, etc., potrebbe essere investito in maniera decisamente
migliore.

7.1.4 Evitare di scrivere metodi di test eccessivamente lunghi


Ogni classe di test, come visto in precedenza, dovrebbe far riferimento a un ben definito compo-
nente, tipicamente a una classe. Inoltre è opportuno che queste classi contengano una serie di
semplici metodi di test, ciascuno demandato alla verifica di uno specifico e limitato aspetto.
Ciò sia per far sì che i test siano più facilmente comprensibili e mantenibili, sia per facilitare
l'analisi dei report prodotti dall'esecuzione dei test: tool comeJUnit, infatti, si limitano a riferi-
re solo il primo test fallito di una batteria di test.
L'inconveniente relativo alla scrittura di molti semplici metodi di test potrebbe risiedere
nella necessità di dove ripetere diverse volte una stessa porzione di codice. Questa eventualità
in passato veniva risolta scrivendo opportuni metodi privati alla classe di test da invocare nel
codice di ogni metodo di test. Ciò non è più necessario con la versione 4 di JUnit e in partico-
lare grazie all'introduzione dell'annotazione @Before.
Nel codice seguente, viene mostrato come decomporre un unico test complesso suddividen-
dolo in alcuni test semplici.

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);
]

7.1.5 Scrivere i test di unità insieme alle classi cui si riferiscono


Questa regola potrebbe risultare abbastanza controversa. In effetti, vi è tutto il movimento
basato sullo "sviluppo guidato dai test" (TDD, Test Driven Development), di cui XP è una delle
istanze più popolari, che evoca la scrittura dei test addirittura prima della scrittura delle stesse
classi da implementare. In sostanza si tratterebbe di un vero e proprio strumento per disegnare
il sistema. Questa regola non intende assolutamente fornire direttive circa il processo di svilup-
po da adottare, tuttavia è importante che i test di unità siano prodotti con il codice stesso. Que-
sto sia per evitare processi di scrittura dei test frettolosi e tipicamente superficiali, sia per far sì
che questi assolvano al loro compito: permettere il rilascio di codice di qualità e quindi sempli-
ficarne il processo di integrazione, fornire informazioni circa l'utilizzo delle varie classi, etc.

7.1.6 Non assegnare la scrittura dei test al personale junior


Un altro problema che spesso interviene nella redazione dei test di unità è che la loro scrittura
sia demandata, immediatamente prima della consegna, a programmatori junior. Questa prati-
ca, spesso, si traduce nella scrittura veloce e superficiale dei vari test e quindi genera una ridu-
zione o addirittura l'annullamento dei vantaggi propri della scrittura dei test di unità. I test,
come visto in precedenza, vanno scritti non appena una classe si rende disponibile o, se si
desidera abbracciare tecniche estreme, addirittura prima di redigere il codice stesso.
Una pratica spesso utilizzata consiste nel far scrivere le classi e i relativi test a diverse perso-
ne. Ciò però è ben diverso dallo scrivere i test di corsa poco prima della consegna del sistema.
7.2 Utilizzare JUnit
JUnit è uno dei framework open source di maggiore successo, tanto che ormai è diventato lo
standard de facto per la produzione di test automatizzati in Java. Si tratta di un framework ben
progettato, efficace, collaudato, divenuto utilizzato come normale strumento di lavoro per tutti
i programmatori Java, etc. Pertanto il suo utilizzo non dovrebbe essere oggetto di discussione.

7.2.1 Utilizzare gli Assert


G l i assertXXX (assertTrue, assertNotNull, AssertEquals, Fail, etc.) rappresentano u n o dei meccanismi
fondamentali del framework JUnit. In particolare questi metodi permettono a JUnit di verifica-
re l'esito dei vari test. Pertanto, ogni metodo di test deve includere almeno uno statement di
assert. Sebbene questa regola possa sembrare piuttosto ovvia, capita di imbattersi in codici di
test in cui la verifica sia affidata a meccanismi diversi, tra cui eccezioni, stampe a video, etc. Un
altro valido accorgimento sta nell'utilizzare il campo etichetta degli assert, specialmente nei casi
in cui uno stesso metodo di test ne contenga diversi. Sebbene l'etichetta sia un campo opzionale,
in caso di fallimento può semplificarel'individuazione del problema, anche se i tool moderni
permettono, attraverso un click del mouse, di posizionarsi direttamente sull'assert non riuscito.

7.2.2 Non utilizzare il System.out.println a fini di test


Può capitare di visionare metodi di test in cui la verifica del corretto funzionamento del sistema
sia affidato a stringhe inviate a video (System.out.println). Si tratta chiaramente di una pratica
fortemente sconsigliata per una serie di motivi: il più importante è che la verifica del corretto
funzionamento del sistema è così demandata alla presenza di persone. Pertanto, ciò rende
impossibile automatizzare le verifiche, eseguire continui controlli anche durante orari nottur-
ni, etc. Inoltre, rende gli stessi controlli poco efficaci e decisamente complessi. La verifica di
algoritmi non banali, infatti, spesso richiede di controllare molteplici informazioni, difficil-
mente verificabili attraverso semplice ispezione di schermate video. Si considerino per esem-
pio le tipiche schermate che riportano i prezzi di prodotti finanziari.

7.2.3 Implementare le classi TestSuite


Nel disegno e nell'implementazione di sistemi software è possibile individuare una serie di
"macro-componenti" i cui elementi sono accomunati da importanti caratteristiche. Per esem-
pio, una tipica architettura multistrato di un sistema prevede:

• strato di presentazione (presentation layer);


• strato di business service (business service layer);
• srato di business object (business object layer)\
• strato di integrazione (integration layer).

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.4 Inserire le classi di test in una struttura equivalente


a quella delle corrispondenti classi verificate
Nell'implementazione delle classi di test è molto utile far sì che il package (e non il path) delle
classi di codice e di test coincidano, (figura 7.2). Ciò è molto utile per fare in modo che le classi di
test possano accedere agevolmente alle risorse friendly (elementi con visibilità di default) delle
classi da verificare senza interferire nella struttura. Non è infrequente che alcuni sviluppatori
decidano di sistemare le classi di test insieme alle corrispondenti classi implementate. Chiaramen-
te ciò non è assolutamente una buona pratica, in quanto rende meno chiari i package di codice,
rende meno agevole il processo di implementazione (bisogna sempre porre molta attenzione alle
classi da aprire), rende più difficile l'impacchettamento in JAR dei sorgenti, e così via.

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

Figura 7.2 - Package delle classi di test e di quelle di codice.


classi di test consiste nel riportare il nome della classe verificata con l'aggiunta del suffisso
"test", come mostrato in figura 7.2.

7.2.6 Utilizzare la parola "test" per enfatizzare metodi di test


Qualora si utilizzi una versione JUnit precedente alla 4.1, è obbligatorio premettere al nome
dei metodi la parola "test" per distinguere i metodi di test (che quindi devono essere invocati
dal framework) dagli altri di utilità. Qualora invece si utilizzi una versione JUnit 4.1 (o superio-
re), i metodi di test sono individuati per mezzo dell'apposita annotazione: @Test. Tuttavia, rap-
presenta una buona strategia identificare i metodi di test riportando questo nome direttamente
nel nome del metodo.

7.3 Test di unità e mock objects


I test di unità, per definizione, sono utilizzati per verificare che un particolare frammento di
codice presenti il comportamento atteso. Pertanto, il campo d'azione dei test di unità è costitu-
ito da singole classi e componenti. Domini di maggiore estensione sono oggetto dei test di
integrazione e di sistema. Il problema di mantenere i test di unità nel proprio ambito di appli-
cazione deriva dal fatto che i sistemi Object Oriented tendono naturalmente a essere costituiti
da una miriade di classi interrconnesse. Quindi il corretto funzionamento di ciascuna di queste,
quasi sempre, dipende dal corretto funzionamento di altre classi, e di componenti, package,
sistemi esterni, e chi più ne ha più ne metta. Per esempio, spesso l'erogazione di determinati
servizi è subordinata all'autenticazione e autorizzazione dell'utente che sono generalmente re-
sponsabilità di un sistema esterno. Ancora, in architetture multi-tier, componenti appartenenti
allo strato di presentazione dipendono da quelli del business service layer; questi, a loro volta,
dipendono da quelle appartenenti al business object layer, e così via. Come fare quindi ad
implementare test di unità che non eccedano il proprio campo d'azione?
La risposta viene dai mock objects. Si tratta di oggetti che fingono il comportamento di deter-
minati oggetti reali di cui si ha bisogno. In particolare, questi oggetti, sviluppati rapidamente
grazie ai relativi framework, implementano la medesima interfaccia degli oggetti reali simulati;
però, invece di implementarne il comportamento reale, forniscono risposte pre-definite e/o
comportamenti molto semplici. Si consideri per esempio un oggetto che si occupi di effettuare
l'autenticazione degli utenti di un sistema, magari eseguendo un'invocazione remota. Il relativo
oggetto mock up potrebbe limitarsi a fornire sempre una risposta affermativa oppure autenti-
care solo gli utenti il cui id è memorizzato in un'apposita lista eventualmente hard-coded, e così
sia... Quindi i mock objects servono per sostituire, con semplici simulazioni, le entità che col-
laborano con quella oggetto di test.
Al termine di questa spiegazione viene riportato un listato. L'oggetto del test è una classe che
fornisce il servizio di autenticazione (AuthenticationService). A tal fine, utilizza i servizi esposti
dalle classi ProfileBO e UserBO, entrambe localizzate nel Business Object layer (da cui il suffisso
BO). Poiché si tratta di un test di unità (e non di integrazione) non è necessario coinvolgere
anche queste classi nel test in questione. Quindi, in questo contesto, al loro posto è possibile
utilizzare appositi mock objects. La verifica del corretto funzionamento delle classi in questio-
ne (ProfileBO e UserBO), come lecito attendersi, è affidato alle relative classi di test.
Per la realizzazione delle classi di mock up, si è deciso di utilizzare il tool EasyMock (http://
www.easymock.org/) la cui guida all'uso non è oggetto di questo libro. Il funzionamento base,
però, è abbastanza semplice:
1. si genera l'implementazione di mock up relativa alle classi di cui è necessario simulare il
c o m p o r t a m e n t o ( p e r e s e m p i o : IProfileBO profileBO = EasyMock.createMock(IProfileBO.class));
2. per ciascun metodo degli oggetti simulati coinvolti nel test:
a. si dichiara la relativa invocazione corredata dai parametri attesi (per esempio:
profileBO.findActiveProfileByLoginld("invalid"));
b. si specifica la risposta attesa (per esempio EasyMock.expectLastCall().andReturn(null););
3. quando tutti i metodi richiesti dal test sono stati specificati, è necessario far sì che il
framework ne prenda atto; ciò fa sì che l'oggetto simulante, dopo aver memorizzato
invocazioni e risposte, si predisponga a ripetere le risposte (per esempio EasyMock.replay(
new Object[] I userBO, profileBO})).

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.

public class AuthenticationServiceTest extends TestCase I


/ * * The profile BO. */'
private IProfileBO profileBO = EasyMock.creafe/Woc/r(IProfileBO.class);
/ " * The profile BO. */
private lUserBO userBO = EasyMock.creafe/Woc/((IUserBO.class);
/ " The authentication service. */
private AuthenticationService authenticationService = null;

/ * * Default Constructor */
public AuthenticationServiceTest() I
authenticationService = new AuthenticationServiceQ;
authenticationService.setProfileBO(profileBO);
authenticationService.setUserBO(userBO);
I

/**

* This method verifies that the service is able to detect


* requests related to not existent user.
* This is a negative test
* @throws Exception a problem occurred during the process
*/
public void testlnvalidLoginld() throws Exception 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

public void testCorrectUserAuthentication() throws Exception I

RefUserDTO correctUser = new RefUserDTOQ;


correctUser.setLoglnf'correctUser");
correctUser.setPassword(generateTmpPassword());
comclUser.se\Smus(UserStatusDTO.USER_ACTIVE),
correctUser.setValidFromfnew Date(new Date().getTime() - 1000000));
correctUser.setValidTo(new Date(new Date().getTime() + 1000000));

RefProfileDTO correctProfile = new RefProtileDTO();


correc\Proti\e.se\S\a\us{ProfileStatusDTO.PROFILE_ACTIVE);
correctProfile.setValidFrom(new Date(new Date().getTime() - 1000000));
correctProfile.setValidTo(new Date(new Date().getTime() + 1000000));
correctProfile.setUser(correctUser);

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

7.3.1 Utilizzare i test di unità per verificare singoli componenti


La prima regola di questa sezione non poteva che prescrivere di utilizzare i test di unità compa-
tibilmente al relativo principio base: verificare che particolari frammenti di codice presentino il
comportamento atteso. Ciascun test, quindi, dovrebbe verificare una determinata caratteristica
considerata isolatamente. Ora, in sistemi non banali, la verifica di singole parti considerate iso-
latamente è un'attività spesso molto complessa. Pertanto è consigliabile utilizzare appositi
framework progettati per consentire ai programmatori di generare rapidamente oggetti di mock
up: semplici oggetti in grado di simulare specifici comportamenti dei corrispondenti oggetti del
sistema reale. Il ricorso a oggetti di mock up permette di implementare test di unità più appro-
fonditi e più dettagliati e quindi, in ultima analisi, sistemi di maggiore qualità.

7.3.2 Implementare un sistema facilmente verificabile


Questa regola, probabilmente, potrebbe appartenere ai capitoli relativi l'implementazione del
sistema e non dei relativi test. Tuttavia, alcuni problemi di disegno si scoprono proprio durante la
scrittura dei test. Inoltre, questa regola è di facile comprensione nel contesto dei mock objects.
Tutti i sistemi dovrebbero essere realizzati considerando, tra gli altri fattori, l'attività di test.
Ciò non significa che si debba cambiare il disegno del sistema per semplificarne la verifica,
come per esempio implementare tutti metodi pubblici in modo da semplificare l'implementazione
dei relativi test; si tratta però di considerare, nella scelta delle diverse alternative implementative
di uno stesso disegno, anche la facilità di verifica.
Per esempio, qualora una classe utilizzi i servizi esposti da altre classi (come nel listato prece-
dente AuthenticationService utilizza i servizi esposti dalle classi ProfileBO e UserBO), una buona
implementazione consiste nel fornire i riferimenti a tali istanze attraverso il costruttore di
AuthenticationService o per mezzo di opportuni metodi di set (come nel caso in questione). In
altre parole, si tratta di utilizzare il pattern Inversion Of Control (inversione del controllo). Ciò
non solo risolve correttamente il disegno: al tempo stesso, ne semplifica il test.
Un'altra alternativa valida consiste nell'utilizzare il pattern di Factory. In questo caso però,
nella scrittura dei test è necessario estendere la classe Factory con una necessaria ai fini di test.
Questa classe estesa, in particolare, si deve occupare della generazione degli oggetti mock up
utilizzati durante il test. Uno scenario che invece rende complessa l'implementazione dei test si ha
quando la generazione delle istanze delle classi utilizzate avviene all'interno del codice. In questo
caso la sostituzione degli oggetti reali con appositi mock up è un'attività piuttosto complessa.

7.3.3 Utilizzare mock objects per simulare condizioni


difficilmente riproducibili
La realizzazione di alcuni test di unità può risultare estremamente complessa, per esempio quan-
do è necessario verificare la risposta del sistema a situazioni anomale, per esempio connessioni
perse durante la sessione, sistema remoto che transita in uno stato di errore, e così via. Tali
situazioni si prestano a essere simulate efficacemente attraverso i mock objects. In particolare,
questi framework possono essere utilizzati o per implementare un proxy del server oppure un'op-
portuna simulazione del server stesso. E quindi è possibile decidere, programmaticamente, quan-
do far simulare comportamenti corretti e quando far generare degli errori.

7.4 Utilizzare tool di analisi della copertura


Utilizzare un test di copertura è assolutamente importante per una serie di motivi.

• 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.

7.4.1 Eseguire periodicamente il test di copertura


Come appena visto, è molto importante utilizzare appositi tool di analisi della copertura dei
test. Chiaramente si tratta di una condizione necessaria ma non sufficiente. E necessario esegui-
re frequentemente l'analisi della copertura per esaminarne i risultati prodotti. In particolare,
bisognerebbe eseguire l'analisi ogni qualvolta si includano nuove classi, siano queste di codice
o di test. In genere non è necessario eseguire una nuova analisi per ogni singola classe aggiunta
(a meno che questa non sia una classe che include un numero significativo di test); è sufficiente
eseguire una nuova scansione all'aggiunta di nuovi package, dopo sostanziali processi di aggior-
namento/refactoring. Un discorso diverso ovviamente vale qualora si stia cercando di
razionalizzare i test: aggiunta di test atti ad aumentare la copertura, rimozione di test ridondan-
ti, e così via. In questo caso può aver senso eseguire i test di copertura più frequentemente.

7.4.2 Aggiungere nuovi test in maniera oculata


Il livello di copertura del codice da raggiungere è un valore che dipende da diversi fattori, per
esempio il dominio dell'applicazione. Per esempio, è plausibile che dovendo implementare il
sistema di controllo di un reattore nucleare si voglia raggiungere una copertura molto prossima
al 100% (sebbene questo non ne garantisca automaticamente il corretto funzionamento...),
mentre è altrettanto probabile che si accetti un fattore di copertura decisamente più rilassato
quando si implementa un sistema di gestione degli ordini.
In ogni modo, qualora il livello di copertura ottenuto non risulti conforme a quello prestabilito,
è opportuno implementare ulteriori classi di test. Tuttavia, invece di procedere come spesso av-
viene (implementare nuovi metodi in modo randomico, il che aumenta ben poco il livello di
copertura), è consigliabile studiare i risultati dell'analisi di copertura al fine di identificare aree
poco esercitate dai test e che quindi beneficierebbero maggiormente di ulteriori verifiche. Chiara-
mente è opportuno anche fare in modo che aree di particolare interesse/sensibilità presentino un
livello molto elevato di copertura. Nella produzione di test è facile notare come sia abbastanza
semplice e veloce raggiungere coperture dell'ordine del 60-70% e come poi piccoli incrementi
richiedano un elevato impegno. Pertanto è opportuno mirare attentamente le aree oggetto di test.

7.4.3 Valutare la possibilità di rimuovere test ridondanti


La presenza di test ridondanti non è un grosso problema, tuttavia finisce per accrescere l'im-
patto dei processi di refactoring. Questi, durante l'ultimo decennio, sono diventati una prassi
nella pratica lavorativa, principalmente come conseguenza dell'adozione sempre più frequente
di processi di sviluppo del software che incorporano approcci iterativi e incrementali.

7.5 Scrivere test chiari ed efficaci


Alcuni programmatori proclamano di considerare non funzionante, parzialmente o interamen-
te, qualsiasi codice privo di una copertura estesa di test automatici. Sebbene tale affermazione
sia abbastanza estrema, è necessario considerare che la presenza di test scritti rapidamente, in
modo superficiale e senza un'attenta analisi può generare una serie di conseguenze negative,
per esempio la falsa sensazione di aver prodotto un codice perfettamente funzionante. Sebbe-
ne questo problema possa essere minimizzato grazie software atti a misurare la copertura dei
test di unità, non sempre è possibile valutare l'efficacia dei vari test prodotti. In particolare,
non è immediato verificare se i test coprano correttamente scenari di errore, verifichino le
dimensioni delle strutture dati e così via. Pertanto è importante produrre i test in modo che
questi siano veramente utili ed efficaci.

7.5.1 Limitare l'utilizzo dei costruttori nelle classi di test


Nell'implementazione dei test di unità, è necessario limitare l'utilizzo dei metodi costruttori. In
particolare, è opportuno non implementare costruttori:

• 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.

7.5.2 Implementare classi di help


Non è infrequente il caso in cui l'esecuzione di diversi test JUnit richieda di disporre di una
stessa serie di oggetti prepopolati che costituiscono appunto la base del test. In questi casi, al
fine di evitare lavoro tedioso e ridondanze del codice, è opportuno creare delle classi helper o
Object Builder atte proprio a fornire oggetti prepopolati necessari per l'esecuzione dei vari
test. Queste classi, dovrebbero essere implementate a livello di package.
Per esempio, potrebbe risultare necessario disporre di un generatore di "oggetti" di prova di
tipo utente, sia per verificare i metodi della classe stessa, sia per verificare il funzionamento di
altri servizi come quelli di autenticazione, di rilascio di un nuovo profilo, di memorizzazione di
informazioni di auditing, e così via. In effetti, è abbastanza normale che, all'interno di un siste-
ma, una stessa classe dati (DTO/VO) sia utilizzata in diversi contesti. Riportiamo di seguito il
frammento di una classe di help utilizzata per produrre istanze della classe UserDTO:

public class UserTestMelper (

/ * * 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 "3", UserStatusDTO.USEfì_DEACTIVED, "093445943", "AlbertE",


"Albert", nuli,"Einstein", "14-03-1879",
"Albert@Einstein.com","AlbertE", "34567", "06-10-1886", "10-10-1890")

I;

i"
' Return a pre-set user
* @param index index ot the requested user
' ©return the requested user

public static UserDTO getRequestedUser(int index) I

UserDTO userDTO = nuli;

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;
)

7.5.3 Non variare il disegno del sistema per facilitarne il test


Durante il processo di revisione dei sistemi, può capitare di imbattersi in classi il cui codice
presenti delle stranezze; per esempio, possono esserci metodi che, sebbene siano assolutamente
da dichiarare privati, sono invece pubblici. Spesso si tratta di variazioni di implementazione
effettuate durante la stesura del codice di test per agevolare oltremisura la scrittura dei test di
unità. Si tratta ovviamente di una pratica pessima.
Sebbene il codice debba essere scritto tenendo in mente anche la facilità di verifica, ciò non
significa assolutamente che il disegno sia alla mercé dei test. L'obiettivo principale della scrittu-
ra dei test è aumentare la qualità del sistema prodotto e non di ridurla.

7.5.4 Inizializzare eventuali risorse esterne


Molto spesso il corretto funzionamento di determinati test dipende dalla corretta disponibilità
di risorse esterne quali database, directory, file, etc. In questi casi è opportuno che il codice di
test si occupi sia di inizializzare correttamente tali risorse prima dell'avvio dei test, sia di rila-
sciarle a test avvenuti o in caso di errore.

7.5.5 Verificare tutti gli scenari


Nella scrittura delle classi di test è necessario implementare sia i test positivi (positive test), sia test
negativi (negative test). I primi servono a verificare che, in condizioni corrette (dati in ingresso
validi, sistema funzionante, e così via), l'applicazione sia in grado di produrre i risultati previsti.
Mentre i test negativi servono per verificare che 0 sistema sia robusto, ossia sia in grado di indivi-
duare condizioni anomale, come dati in ingresso non validi, e quindi di eseguire le corrispondenti
procedure di gestione, per esempio il lancio di un'opportuna eccezione, l'esecuzione di un per-
corso alternativo, la ripetizione controllata dell'istruzione che ha generato il problema, etc.
Nella scrittura delle classi di test è quindi necessario verificare anche che, in caso di errore, il
sistema comunichi le eventuali eccezioni previste come mostrato nei seguenti listati. In partico-
lare, tra quelli seguenti, il primo listato mostra mostra la versione funzionante con JUnit 4,
mentre quello successivo mostra l'implementazione necessaria con le versioni precedenti.
Il seguente test ha successo solo se l'eccezione attesa è effettivamente lanciata.

@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

' © t h r o w s Exception - a p r o b l e m o c c u r r e d d u r i n g the test

public void testlnvalidLoginld() throws Exception I

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");

I catch (MySystemSecurityException secExp) I

assertTrue(lrue);
I
I

7.5.6 Implementare test in grado di verificare i costrutti decisionali


Un aspetto molto importante da verificare sono i costrutti decisionali. In particolare, è oppor-
tuno scrivere diversi test in grado di verificare le combinazioni dei vari percorsi. Questa attività
può risultare decisamente tediosa, in quanto, frequentemente, il numero delle combinazioni
dei percorsi presenti all'interno di ciascun metodo sono elevati. Il lato positivo è che i tool di
copertura sono in grado di rilevare eventuali percorsi intentati e che non è necessario verificare
ogni dettaglio di tutte le classi implementate.

7.5.7 Verificare i cicli


Questa regola è molto simile alla precedente. Tuttavia è importante sottolineare il rilievo di
verificare attentamente i cicli. Verificare che questi non diano luoghi a cicli infiniti, ma che
terminino quando sia previsto.

7.5.8 Verificare i limiti delle strutture dati


Qualora l'oggetto dei test da implementare sia una classe che incapsula strutture dati (tipica-
mente si tratta di Value Object o di Data Transfer Object) è importante verificare la capacità
della stessa classe di gestire correttamente tentativi di violare i limiti delle strutture dati. Le
tipologie delle violazioni sono diverse e quelle applicabili dipendono dall'oggetto utilizzato per
memorizzare la struttura: array, liste, tabelle hash, etc. Per esempio, è possibile eseguire tenta-
tivi di inserire più dati di quelli gestibili, di leggere oltre l'ultimo elemento e così via.
In questo caso la mancanza di opportuni test non è rivelata da tool come quelli di copertura.

7.5.9 Non implementare test con dipendenze temporali


Nella scrittura dei test di unità è importante evitare che il corretto funzionamento di alcuni test
dipenda dall'awenuta esecuzione senza errore di altri. Pertanto i test devono essere scritti
senza effettuare alcuna supposizione circa l'esecuzione degli altri. Ciò è necessario qualora si
utilizzi JUnit, poiché questo non fornisce alcuna direttiva circa l'ordine di esecuzione dei test
inseriti in una classe. Ciò è una diretta conseguenza del fatto che JUnit utilizza i meccanismi
della riflessione Java e che il JDK non fornisce alcuna garanzia circa l'ordine con cui i metodi di
una classe sono restituiti qualora siano riferiti attraverso i meccanismi della reflection.
Ecco di seguito un esempio errato di test ad accoppiamento temporale:

A
public slatic Test suite() {

suite.addTest(new FirstTestCase("TestToPer1ormFirst"));
suite.addTest(new
return suite; SecondTestCasefTestToPerformSecond"));

7.5.10 Valutare attentamente eventuali test con effetti collaterali


È importante valutare attentamente l'implementazione di test in grado di generare effetti
collaterali, che possano per esempio cambiare lo stato del sistema, lasciare dati nel database e
così via, poiché:

• 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.

7.5.11 Implementare test efficienti


Per quanto l'implementazione di test caratterizzati da buone performance non sia di certo un
requisito prioritario nello scrivere i test, è comunque bene puntare a soluzioni con le migliori
prestazioni, quando ciò sià possibile. Sistemi di dimensioni medie e grandi sono generalmente
costituiti da diverse migliaia di classi; a questo numero corrisponde una quantità confrontabile
di test, che solitamente vanno eseguiti molto di frequente durante il processo di sviluppo del
software. Pertanto, ricorrere a soluzioni più efficienti evita che l'esecuzione delle procedure di
test impieghi tempi eccessivi. Chiaramente, scegliere soluzioni efficienti non significa eseguirne
l'over-engineering.
Capitolo 8
Test di integrazione
Introduzione
I test di integrazione rappresentano un'importante attività del processo di sviluppo del software
a carico (almeno parzialmente) del team di sviluppo. Da un punto di vista logico, seguono i test
di unità e precedono quelli di accettazione da parte utente (UAT, User Acceptance Test): dopo
aver verificato il corretto funzionamento dei singoli elementi, è necessario assicurarsi che gli
stessi, una volta assemblati in elementi più complessi, continuino a funzionare correttamente e
implementino correttamente i requisiti.
Il termine di test di integrazione è utilizzato per identificare una vasta gamma di test (figura
8.1): si va dal test di più componenti integrati, fino a giungere all'intero sistema (tanto è vero
che lo stesso UAT è un test di integrazione), includendo i test delle performance e gli stress test.
I test di integrazione rappresentano una parte fondamentale del workflow di test e una buo-
na strategia permette di ottenere importanti vantaggi, come:

• 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

Tuttavia, l'implementazione di un'efficace strategia di test non è assolutamente un'attività


immediata, soprattuto nel contesto di processi moderni di sviluppo del software iterativi e
incrementali (iterative and incrementai) dove anche questa attività deve ubidire ai principi tipici
di tale approccio. Nei tradizionali processi di sviluppo del software basati su una strategia a
cascata iwaterfall), specialmente nelle loro versioni iniziali, tutto era (concettualmente) più
facile per via del fatto che questi prevedevano un sola fase di test, rigidamente pianificata nel
periodo precedente la consegna. Sebbene questa fosse una strategia sconsigliabile per tutta una
serie di importanti motivi (feedback degli utenti fortemente posticipato, ridotta capacità di
gestione dei rischi, enorme costo di correzione dei difetti, etc.), offriva il magro vantaggio reltivo
la semplificazione dell'iniziale pianificazione di tali test. I moderni processi di sviluppo del
software, invece, sono basati su strategie di tipo iterativo e incrementale e prevedono che l'inte-
ro progetto sia organizzato in una serie di sottopogetti ognuno dei quali genera una nuova
versione del sistema sottoposta alla valutazione degli utenti. Pertanto, mentre una parte del
team procede all'implementazione dei requisiti della nuova fase, un altro gruppo di sviluppatori
segue i test della fase precedente. Questa strategia, sebbene risolva tanti dei problemi tipici
degli approcci tradizionali, crea una serie di nuove problematiche, come per esempio la mag-
giore difficoltà di pianificazione/gestione dei test di integrazione, di coordinamento tra le varie
fasi e tra team. In particolare i test devono essere rivisti ad ogni iterazione generando la neces-
sità di scriverne di nuovi, di aggiornare preesistenti e spesso anche di rimuovere alcuni resi
inutili/ridondandi dalla nuova versione del sistema. Pertanto, l'adozione di approcci iterativi e
incrementali richiede di individuare un'adeguata strategia capace di bilanciare la necessità di
verificare completamente il sistema e la riduzione del tempo utilizzato per l'implementazione
di test transitori.
A questo va poi aggiunto un altro classico problema spesso presente in molti progetti: il
tempo dedicato ai test di sistema... Spesso non viene allocato un sufficiente lasso di tempo,
oppure accade che il tempo inizialmente previsto sia consumato da eventuali ritardi di
implementazione. La logica consequenza è che può succedere di di mettere in esercizio un
sistema non sufficientemente testato con tutte le conseguenze del caso.

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.

Catalogazione dei test di integrazione


Dallo studio della letteratura specifica si nota subito un certo disorientamento circa la
nomenclatura e struttura delle diverse tipologie di test di integrazione. Questa confusione è poi
spesso accentuata nei vari ambienti professionali. Pertanto, prima di procedere oltre, si è deciso
di presentare una catalogazione razionale di questo insieme di test (figura 8.1). In primo luogo,
i vari test di integrazione possono essere suddivisi in due grandi categorie: funzionali e non-
funzionali. I primi si occupano di verificare che i servizi offerti dal sistema siano corretti, ossia
implementino effettivamente i relativi requisiti, mentre i secondi focalizzano l'attenzione su aspetti
di carattere non funzionale, come performance, sicurezza, fault-tolerance, etc. I test funzionali
poi si suddividono in test a carico del team di sviluppo (IST, Development) e i test UAT. Come
visto in precedenza, questi sono dei particolarissimi test di integrazione il cui superamento rap-
presenta la condizione necessaria ma non sufficiente per mandare il sistema in produzione.
I test di sviluppo, a loro volta, prevedono quattro specializzazioni: interni, esterni, sanity check e
smoke test. Questi ultimi due sono argomento del paragrafo successivo. I test di integrazione inter-
ni verificano più componenti integrati insieme, i singoli servizi, fino a giungere all'intero sistema (o
sottosistema), ma senza coinvolgere strutture esterne, quali database, sistemi di messaggistica e
altri componenti del sistema. La loro presenza, invece, caratterizza degli IST test veri e propri.
Entrambe le tipologie sono a carico dei team di sviluppo utilizzati a fini implementativi.

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

Figura 8.1 - Una classificazione dei test di sistema.


Sanity Check e Smoke Test
Smoke Test (test di fumo) e Sanity Check (test di sanità) sono due particolari esempi di test di
integrazione spesso confusi tra loro. Ciò è dovuto al fatto che queste due tipologie di test,
sebbene differenti dal punto di vista semantico, presentano forti analogie.
Smoke test rappresenta un test non esaustivo (e pertanto "superficiale") atto a verificare che
le funzionalità principali del sistema non vengano compromesse da recenti variazioni del codi-
ce. Come tale è tipicamente utilizzato non appena nuove porzioni di codice sono integrate
(check-in) nel sistema di gestione dei sorgenti (CVS, SVN, ClearCase, etc.). Chiaramente si
tratta di un meccanismo molto utile in presenza di sistemi di dimensioni medio-grandi.
Il nome smoke test deriva da una pratica spesso usata in passato nell'ingegneria elettronica.
In particolare, dopo aver cambiato o riparato alcuni componenti, questi venivano riassemblati
nella scheda di provenienza e quindi alimentati. Il test passava, qualora il componente non si
bruciava e quindi non produceva fumo; falliva, negli altri casi.
Il sanity check, sebbene presenti alcune similitudini con lo smoke test (la più importante è
che il suo obiettivo è verificare il corretto funzionamento delle sole principali funzionalità), ha
anche caratteristiche fondamentali che lo contraddistinguono. In particolare questo è tipica-
mente utilizzato per verificare che la nuova versione del sistema sia pronta per ulteriori test
(qualora sia eseguito su un sistema installato in un ambiente di test) o che sia stata correttamen-
te installata e configurata nell'ambiente di produzione. Quindi smoke test si presta ad essere
utilizzato come controllo dei sorgenti mentre il sanity check per la verifica di nuovi deployment.
I dettagli relativi a questi test sono molto interessanti, ma esulano dagli obiettivi del capitolo.

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:

• TestNG (http://testng.org/doc/) disegnato per unire il meglio di JUnit e NUnit introducen-


do al tempo stesso importanti features che lo rendono capace di risolvere elegantemente
le esigenze di diversi tipi di test (unit, funzionali,integrazione, di sistema, e cosi via).
Alcune delle nuove features sono: esteso utilizzo delle annotazioni, elevato grado di
flessibilità di configurazione, avanzato modello di esecuzione non più basato sui TestSuite,
supporto dei test guidati dai dati, e così via.
• DBunit (http://dbunit.sourceforge.net/) che, come suggerisce il nome, è un'estensione a JUnit
atta a semplificare l'implementazioni di test che prevedono l'accesso al database come
passo fondamentale del processo di verifica. In particolare, DBUnit fornisce una serie di
interessanti feature come: importazione/esportazione automatica dati nel e dal database
attraverso opportuni file XML. Ciò permette di implementare rapidamente i classici
passi che ogni sviluppatore eseguirebbe manualmente: set-up del database, esecuzione
di una serie di servizi, verifica visuale basata su un predefinito numero di SELECT.
• JunitEE (http://www.junitee.org/) progettato per semplificare il test di applicazioni Java
EE. In particolare, questo implementa una versione di TestRunner in grado di emettere
l'output in formato HTML-XML e una servlet da utilizzare per agganciare i propri test.
• HttpUnit (http://httpunit.sourceforge.net/). Questa estensione è stata disegnata per imple-
mentare una serie di test di siti web focalizzando l'attenzione sulle comunicazioni HTTP.
• htmlUnit (http://htmlunit.sourceforge.net/). Anche questa estensione è stata disegnata per
verificare il funzionamenti di siti web, in questo caso però l'attenzione è focalizzata sulla
gestione di pagine HTML. In particolare, vi sono feature atte a "complilare" eventuali
form, simulare "click" del mouse, etc.
• xmlUnit (http://xmlunit.sourceforge.net/). Questa estensione fornisce una serie di feature
assolutamente utile per il test e la manipolazione di formati XML.

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:

1. la prima riga è utilizzata per dichiarare la particolare fixture in grado di interpretare la


tabella;
2. la seconda riga riporta i nomi dei parametri e il metodo da invocare per produrre il
risultato da verificare;
3. le restanti forniscono le «-pie (inputl, input 2, ..., input k, output).

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.

8.1.1 Valutare la possibilità di implementare un sistema di test


Uno dei problemi classici di ciascun sistema sta nel verificare che il sistema implementi corretta-
mente i propri requisiti. La più semplice, e decisamente meno efficace, delle strategie consiste
nell'eseguire i test manualmente: si invocano dei metodi e quindi si analizza il risultato, magari
accedendo al database... Tale approccio, oltre a soffrire di una serie di evidenti problemi (alto
costo, difficile riproducibilità, curva di apprendimento del personale, e così via), presenta anche
il grande svantaggio legato a processi di refactoring/rilascio del sistema. In effetti, ogni nuova
versione che non sia limitata a piccoli bug fixing, tende ad azzerrare tutti i test eseguiti fino a quel
momento sul sistema. Pertanto, in presenza di test manuali, si causa uno spreco totale di risorse.
Una strategia decisamente migliore, in grado di risolvere molti degli svantaggi presentati dal
precedente metodo, consiste nell'implementare un sistema automatico capace di verificare il
corretto funzionamento del sistema alla presenza di ogni nuova versione. La presenza di questi
test fa sì che il sistema generato non sia già legacy nel momento in cui è prodotto ma fornisce ai
team di sviluppo strumenti fondamentali per far crescere il sistema.
Poiché questo sistema è a sua volta un progetto software a tutti gli effetti, presenta dei propri
requisiti funzionali: l'insieme dei test case del sistema da verificare deve inoltre obbedire a un
piano che, ad ogni release, tenga conto dei casi d'uso implementati dal sistema da verificare.
Sistemi del genere sono di immediata implementazione eccezion fatta per le GUI che risultano
ostiche da verificare automaticamente. Infine, l'implementazione di questi sistemi rappresenta
un tipico task che si presta, senza grandissimi rischi, ad essere assegnata a team off-shore.

8.2 Gestire correttamente i vari progetti di test


Ogni progetto di dimensioni medio-grandi richiede l'implementazione di un insieme di pro-
getti di test (come illustrato in figura 8.1), veri e propri sistemi eseguibili di grandissimo valore
per il successo del programma. Il problema è che alcune di queste tipologie di test presentano
delle differenze, spesso così sottili che difficilmente vengono comprese dai vari sviluppatori.
Tuttavia è importante che i vari membri del team di sviluppo siano sincronizzati sulle varie
differenze tra questi progetti, sappiano quali servizi di test implementare, in quale progetto
inserirli, e così via.
Le batterie di test sono veri e propri progetti a sé di importanza fondamentale. Questi sono
indubbiamente importanti durante i primi rilasci del sistema, e, contrariamente alle tipiche
credenze, diventano ancora più indispensabili durante le fasi successive. Ciò perché è di fonda-
mentale importanza assicurarsi che le nuove versioni non compromettano il corretto funziona-
mento dei servizi già consegnati, sia in termini di funzionalità sia in termini di caratteristiche
non funzionali.
Presentiamo di seguito una serie di direttive molto pratiche per aiutare a comprendere le
demarcazioni fra i vari progetti di test.

8.2.1 Non sovraccaricare i test di unità


Molto spesso diverse tipologie di test vengono implementate utilizzando direttamente JUnit.
Anche se ciò apparentemente non sembrerebbe essere un problema, in realtà questa soluzione
presenta alcuni effetti collaterali. In primo luogo, JUnit da solo non è adatto per ogni tipologia
di test. Pertanto il suo utilizzo al di fuori dei test di unità richiede l'implementazione di una
serie di servizi che altri framework forniscono di base. Un altro grosso problema, inoltre, deriva
dall'errata associazione mentale che si genera nella mente degli sviluppatori: "si utilizza JUnit,
quindi si tratta di test di unità". Questo spesso porta ad alcune controindicazioni. Per esempio
si finisce con il sovraccaricare i test di unità a discapito degli altri, generando tutta una serie di
problemi, come test di unità troppo pesanti e lunghi da eseguire, test di integrazione troppo
leggeri, e così via. I test di unità dovrebbero limitarsi a singole classi fino a giungere a singoli
componenti/package.
Ora, un'altra importante parte dei test ohe il team di sviluppo dovrebbe implementare è
costituito dai test IST interni ove i componenti vengono via via assemblati tra loro e sottoposti
a test. Anche se questi, per essere precisi, appartengono ai test di integrazione, possono essere
tranquillamente inseriti tra i test di unità. Importantissimo però è assolutamente non includere
sistemi esterni come il database, il sistema di messaggistica, altri sistemi, etc. che vanno simulati
con appositi oggetti/componenti di mock-up.
La presenza di elementi esterni, come il database e il sistema di messaggistica, sono parte dei
test IST esterni e come tali vanno allocati in un altro progetto.

8.2.2 Implementare i test di integrazione esterni


La presenza dei test precedenti è la classica condizione necessaria ma non sufficiente: i test di
unità e di integrazione interni si occupano di verificare che i singoli elementi funzionino corret-
tamente e che, una volta assemblati in entità più grandi, implementino correttamente i requisi-
ti. Tuttavia non rappresentano ancora un banco di test dove il sistema è installato e quindi
sottoposto a veri e propri stimoli esterni attraverso i canali previsti. Questo è l'obiettivo dei test
di sistema esterni: il sistema (o sotto-sistema) viene installato nel relativo ambiente di test e
quindi viene sottoposto a un processo automatico di verifica utilizzando tutti gli elementi ester-
ni richiesti: sistema di messaggistica, database, etc.
La differenze tra questi test e i precedenti, non è solo relativa ai confini del sistema, ma anche
a quando eseguirli e al tempo a disposizione per la relativa esecuzione. In particolare, i test
precedenti (unità e IST interni) devono essere eseguiti durante lo sviluppo e assolutamente
prima di integrare il proprio test nel sistema di versionamento. Ciò fa sì che siano eseguiti
molto frequentemente, e ne consegue l'esigenza di poter essere eseguiti velocemente. I test di
integrazione esterni, invece, devono verificare accuratamente il corretto funzionamento del
sistema. Pertanto, possono richiedere molto più tempo fino alla classica esecuzione notturna.
Chiaramente, questi devono essere eseguiti dopo aver integrato del codice nel sistema di
versionamento.

8.2.3 Valutare il ricorso all'utilizzo dei sanity check


Molto spesso, soprattutto in progetti di medio-grandi dimensioni, accade di avere dei test di
unità e di integrazione interni molto veloci da seguire ma in grado di verificare solo il corretto
funzionamento dei componenti interni, e poi di disporre di test di integrazione che, per poter
verificare il corretto funzionamento dell'intero sistema, finiscano per richiedere molto tempo.
Per questo motivo, molto frequentemente finiscono per essere eseguiti di notte.
In queste situazioni, per evitare di attendere la notte per verificare il corretto funzionamento
del sistema e per non appesantire troppo i test di unità, spesso si decide di ricorrere ai sanity
check (test di integrazione esterni atti a verificare il corretto funzionamento di un numero molto
limitato di servizi). I sanity check pertanto rappresentano un buon compromesso tra l'esigenza
di mantenere i test di unità leggeri e quella di iniziare a verificare più in dettaglio i vari servizi.

8.3 Gestire correttamente i vari ambienti


Come organizzare i vari ambienti? Per quanto insolito possa sembrare, non è infrequente il
caso in cui i vari ambienti (sviluppo, IST e UAT) possano condividere importanti risorse (come
per esempio database) creando non pochi problemi. Uno dei più classici consiste nell'assistere
al fallimento di importanti e prolungati test di integrazione per via del fatto che, avendo predi-
sposto uno schema del database condiviso tra gli ambienti di integrazione e di UAT, il test di
integrazione in corso viene alterato da un altro eseguito da un altro utente. Oppure, che impor-
tanti informazioni presenti nel database necessarie per eseguire ulteriori test o per investigare
cause di fallimento, siano eliminate/alternate da test di altra natura.

8.3.1 Assegnare ad ogni sviluppatore il proprio ambiente


Non è una novità: ogni sviluppatore deve disporre di un proprio ambiente, incluso database,
per poter eseguire il propri test in isolamento senza essere disturbato dai test eseguiti da altri
sviluppatori o da processi automatici, e senza intralciare, a sua volta, gli altri.

8.3.2 Mantenere gli ambienti di IST e UAT completamente divisi


Analogamente a quanto prescritto nel caso precedente, è di fondamentale importanza far si
che gli ambienti IST e UAT siano per quanto possibile separati. Mentre, per ovvi motivi econo-
mici, non è sempre possibile disporre di tutti i server necessari e di diversi network per mante-
nere i vari ambienti completamente separati anche dal punto di vista fisico, è assolutamente
necessario salvaguardare almeno la separazione di alcune risorse fondamentali come il database.

8.4 Pianificare correttamente i test di integrazione


Il disegno della stategia di integrazione è un'altra attività estremamente improtante per i pro-
getti software. Mentre nei processi tradizionali era sufficiente allocare il doppio del tempo
ritenuto necessario per tali test, nei processi moderni di tipo iterativo e incrementale la situa-
zione è notevolmente più complessa, poiché questi vanno ripetuti, rivisti, manutenuti, etc.
Chiaramente, la strategia di integrazione è largamente subordinata al processo di sviluppo
del software utilizzato e al progetto elaborato dal Project Manager. Per esempio, qualora il
progetto preveda di dedicare le prime milestone al rilascio del'archtitettura, è chiaro che la
strategia di integrazione, volenti o nolenti, debba riflettere tale approccio. Ugualmente, qualo-
ra il piano devesse prevedere di rilasciare interamente il servizio X, prima di procedere con altri
servizi, la strategia di integrazione dovrebbe procedere nella stessa direzione. I test di integra-
zione devono "copiare" il progetto studiato dal Project Manager. Detto ciò, è altresì importan-
te notare che, tipicamente, all'interno degli orizzonti posti dal progetto, è sempre possibile
trovare degli spazi per la strategia di integrazione.

8.4.1 Selezionare una corretta strategia per i test di integrazione


Le strategie per eseguire i test di integrazione sono essenzialmente due:

1. Top-down (dall'alto verso il basso). Questo approaccio consiste nell'inziare a verificare


il funzionamento dei moduli a più alto livello per poi scendere. Il principio alla base di
questo approccio sta nell'eseguire, il prima possibile, i controlli dei principali flussi e
servizi gestiti dal sistema. Sebbene questo approccio offra evidenti vantaggi, presenta
anche alcuni inevitabili svantaggi. Per esempio non è molto compatibile con la filosofia
di cominciare, appena possibile, a rilasciare progressive versioni del sistema. Un altro
piccolo svantaggio consiste nel dover creare un numero spesso significativo di oggetti
di mock-up per simulare gli strati sottostanti. Questo problema, tuttavia, può essere
ridotto utilizzando opportuni tool atti a generare rapidamente oggetti di mock-up (come
visto nel capitolo precedente). Inoltre è sempre molto utile disporre di test focalizzati
sui singoli livelli del sistema considerati isolatamente.
2. Bottom-up (dal basso verso l'alto). Come è lecito attendersi, si tratta dell'approccio op-
posto al precedente e quindi è basato sul procedere dall'integrazione dei livelli sottostanti
via via verso l'alto. I vantaggi di questo approccio pertanto sono relativi al fatto che si
minimizzi la necessità di creare oggetti di mock-up e che, entro certi limiti, sia possibile
eseguire iniziali rilasci dell'applicazione. In effetti, una volta che i vari livelli sottostanti
siano disponibili, è possibile iniziare a rilasciare servizi completi e non semplicemente
simulati. Gli svantaggi invece sono relativi al fatto che spesso l'implementazione di livelli
di test intermedi richiede di implementare logiche complesse e quindi si finisce con il
rimandare sempre l'avvio dei test relativi ai servizi/flussi principali.

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.

8.5 Verificare gii elementi principali


Mentre nel caso dei test di unità è chiarissimo cosa verificare (o almeno dovrebbe esserlo),
spesso la situazione non è altrettanto chiara nel caso dei test di integrazione. In questa fase è
necesario focalizzare l'attenzione su servizi esposti, ciclo di vita delle principali entità business
e meccanismi di delegazione e cascading.

8.5.1 Verificare tutti i servizi esposti dal sistema


Questa regola dovrebbe essere fin troppo ovvia, tuttavia non è infrequente il caso in cui i test di
integrazioni siano interamente focalizzati sui principali servizi trascurandone altri secondari.
Sebbene la logica alla base di tale strategia sia assolutamente condivisibile, è altrettanto impor-
tante ricordare una regola d'oro: tutto il codice ditribuito (di cui si effettua il deployment), e
quindi a maggior ragiore tutti i servizi esposti, devono essere sottoposti a test intensivi. Qualora
ciò non sia possibile per questione di budget/tempo, è cosnigliabile valutare la possibilità di
non abilitare i servizi non verificati accuratamente. Probabilmente è meglio ritardare la conse-
gna di qualche servizio secondario invece che correre il rischio di figure imbarazzanti.

8.5.2 Verificare il ciclo di vita delle principali entità business


Nella stragrande maggioranza dei sistemi software è possibile identificare una serie di entità
business il cui ciclo di vita è sancito da un ben definito insieme di stati e di stimoli. Una buona
pratica consiste nel rappresentare queste macchine a stati finiti attraverso opportuni diagram-
mi di stato. Alcuni esempi sono un biglietto areo, un trade, una richesta di quotazione, un
thread in esecuzione etc. La rappresentazione di queste macchine a stati finiti è molto impor-
tante non solo per l'implementazione, ma anche per la codifica dei test di integrazione. Infatti,
è importante implementare insiemi di test in grado di verificare:

• 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.

8.5.3 Verificare i principali meccanismi di delegazione e cascading


Ogni qualvolta si implementa un sistema software, è necessario affidarsi a diversi meccanismi di
delegazione e cascading. Il corretto funzionamento di questi meccanismi è fondamentale per il
corretto funzionamento dei servizi esposti. Pertanto, è assolutamente necessario implementare
appositi test che verifichino l'effettivo funzionamento di questi neccanismi. Per esempio, si con-
sideri uno scenario abbastanza comune di un sistema che riceva dei dati via un sistema di
messaggistica. In questo caso, è necessario assicurarsi le capacità del sistema di individuare possi-
bili scenari di optimistic locking e di gestirli correttamente. Una possibilità, per esempio, consiste
nel non fornire l'acknowledge del messaggio e quindi di richiederne la riconsegna posticipata.

8.6 Test di applicazioni Java EE


Tutti coloro che hanno implementato applicazioni Java E E sanno bene quanto difficile sia
implementare i relativi test di itegrazione.
Con l'introduzione della versione EJB 3 la situazione è drastricamente migliorata, i compo-
nenti somigliano sempre di più ai vecchi oggetti Java (POJO), l'Inversion Of Control facilita
l'impostazione dell'ambiente, etc. Tuttavia, verificare componenti dispiegati (deployed) negli
application server è ancora problematico.

8.6.1 Verificare i singoli strati (layei) isolandoli dagli altri


Ogni applicazione Java E E che si rispetti è organizzata in strati, i più comuni sono:

• Presentazione. Questo livello è tipicamente scomposto in funzione delle diversi tipi di


protocolli e di UI gestiti. Alcuni esempi sono: thin UI che include Servlet, J S P e handler
per interfacce HTML, MDB utilizzati per gestire messaggi JMS, etc.
• Logica Business tipicamente suddivisa in servizi business, oggetti business. Il primo
contiene servizi business, mentre il secondo rappresenta complessi oggetti business come
trade, ordini, etc.
• Integrazione. Anche questo strato è tipicamente suddiviso in tanti sotto-strati quante
sono le tecnologie da integrare (database, messaggistica, risorsa LDAP, etc.).

Caratteristica peculiare di queste architetture è la stretta dipendenza di uno strato da quello


immediatamente sottostante: il livello di presentazione dipende da quello dei servizi business,
quest'ultimo dipende dallo strato degli oggetti business, e così via.
Una parte importante dei test di integrazione consiste nel verificare che i singoli strati funzio-
nino correttamente in isolamento. Per ottenere ciò, è necessario focalizzare l'attenzione su un
determinato strato e quindi implementare opportuni oggetti atti a simulare lo strato sottostante.
Come al solito, questo non è l'unico test di integrazione, ma ne rappresenta una parte importante.

8.6.2 Implementare dei test al di fuori del container (out-of-containei)


Implementare dei test fuori dal container è un'attività molto utile per sistemi Java EE. Questo
per una serie di importanti motivi. Per esempio: implementare test all'interno del container è
un'attività complessa richiede script e deployment particolari, l'esecuzione dei test è molto labo-
riosa e richiede molto tempo, implementare molti test ha un'incidenza significativa sul budget.
Con l'introduzione di EJB 3, gli enterprise JavaBeans sono divenuti POJO. Come tali si pre-
stano ad essere testati al di fuori del container. Al fine di rendere ciò possibile è necessario trarre
0 massimo vantaggio da parte dell'introduzione dell'inversione di controllo. Quindi non deve
essere il componente che si "cerca" le necessarie risorse, ma un'entità "esterna" (l'application
server o il framework di test) deve impostarle. Tutto ciò però è la classica condizione necessaria
ma non sufficiente. Infatti, gli application server sono utilizzati proprio perché forniscono tutta
una serie di servizi, come la gestione del multithreading, del ciclo di vita, della transazionalità e
così via. Tuttavia, per i fini dei test funzionali, è spesso sufficiente utilizzare delle implementazioni
leggere fornite dai software dedicati, come per esempio MockEJB (http://www.mockejb.org/).

8.7 Investire sui test della GUI


Implementare o meno i test della GUI (Graphic User Interface, interfaccia utente grafica) è un
dilemma che a lungo tempo ha tenuto banco nelle discussioni di molti addetti ai lavori. Da un
punto di vista teorico l'intera comunità concorda con l'importanza di implementare tali test. In
alcuni ambienti estremi, come per esempio i front-office delle banche di investimento, le GUI
devono essere in grado di supportare migliaia di messaggi per frazioni di secondo, e contempora-
neamente gestire gli stimoli dell'utente che magari ha la necessità di prenotare rapidissimamente
un trade da decine di milioni di dollari. In questi contesti, abbastanza estremi, si immagini quali
ripercussioni possano creare alcuni malfunzionamenti, come per esempio frapposizioni di prezzi
e race condition. Nonostante questo consenso almeno in linea teorica, all'atto pratico la situazio-
ne si complica, complici anche i primi tentativi di costruire dei tool di supporto all'implementazione
automatica di tali test. Tutti coloro che si sono trovati a implementare delle GUI dei sistemi di
front-office testé menzionati cominciando più di dieci anni fa, sanno benissimo a quali pene si
andava in contro nel costruire sofisticati sistemi di test. In effetti i primi esempi di tool di suppor-
to ai test delle UI tentavano di pilotare le interfacce utenti creando delle mappe grafiche dei vari
oggetti sfruttandone le coordinate cartesiane. Sebbene, dopo diverse iterazioni, questi tool riu-
scissero ad alleviare l'onere della configurazione iniziale, grazie a meccanismi atti a memorizzare
1 movimenti del mouse, restava il problema della fragilità rispetto ai cambiamenti: ogni variazione
della UI generava un grosso lavoro di manutenzione dei test tanto da scoraggiarne l'utilizzo. I
tool attuali, basati su una visione a oggetti (anche per questo motivo la versione precedente è
spesso erroneamente denominata "analogica") e quindi in grado di riconoscere più agevolmente
i vari elementi, hanno risolto molti di questi problemi: ma comunque il danno è fatto.
La GUI svolge un ruolo importantissimo: è "l'involucro" con cui l'utente interagisce e che è
in grado di giudicare. Ben poca soddisfazione avrebbe l'utente qualora si realizzasse un sofisti-
catissimo sistema, se poi la relativa interfaccia utente risultasse difettosa e/o difficile da utilizza-
re. Paradossalmente, gli utenti tendono ad accettare più volentieri la situazione opposta.
Volenti o nolenti le UI devono essere verificate approfonditamente: pertanto si può scegliere
di assumere delle persone per eseguire manualmente e ripetutamente gli stessi test, oppure
implementare una volta per tutte un insieme di test da eseguire ad ogni cambiamento.

8.7.1 Implementare i test per le GUI "business"


Nell'ambito di uno stesso progetto, soprattutto in contesti di progetti di medie-grandi dimensio-
ni, è abbastanza frequente implementare tutta una serie di sistemi UI, di cui alcuni strettamente
a carattere business e altri necessari per eseguire delle routine di servizio, amministrare il sistema,
e cosi via. Come lecito attendersi, mentre per quanto concerne le seconde è possibile rilassare la
quantità e qualità dei test, lo stesso approccio non può assolutamente essere adottato per le
interfacce business. Queste sono l'ambiente di lavoro degli utenti che vi interagiranno per la gran
parte della loro giornata lavorativa. Inoltre, la GUI è la parte del sistema che gli utenti sanno ben
valutare (fin troppo bene), grazie alla loro esperienza, anche qualora non dispongano di sofistica-
te nozioni informatiche. Malfunzionamenti a queste GUI possono nel migliore dei casi infastidire
il pubblico degli utenti, mentre nel peggiore possono addirittura generare grossi danni.
Si incontra personale "esperto" che "risolvono" il problema alla radice implementando GUI
semplicissime e mantenendole più semplici possibile. Sebbene difficilmente si potrebbe obiet-
tare sull'importanza di disegnare UI lineari, pulite, consistenti, etc., l'esperienza insegna che gli
utenti desiderano ambienti sempre più evoluti, interattivi, ergonomici, la cui apparente sempli-
cità cela complessi meccanismi. Pertanto, GUI così semplici da non richiedere opportuni siste-
mi di test tendono ad essere assolutamente poco interessanti, soprattutto per gli utenti.

8.7.2 Verificare il funzionamento dell'API


Tutti i sistemi, inclusi quelli demandati a gestire l'interfacciamento con l'utente, dovrebbero
essere disegnati e implementati secondo i dettami delle architetture a strati. In questo conte-
sto l'interesse è centrato sulla necessità di separare chiaramente, attraverso un insieme di
interfacce ben definite, lo strato di presentazione da quello dei servizi. Questa separazione
offre tutta una serie di importanti vantaggi, come per esempio, il poter implementare diverse
interfacce utente anche basate su tecnologie diverse, il poter disporre isolare i vari strati
schermandoli dai cambiamenti degli altri e il poter verificare il corretto funzionamento dei
vari strati. Quindi è importante disegnare architetture a strati, basare l'interfaccia utente su
API ben architettate e, soprattutto, implementare opportuni sistemi di test sia per la UI sia
per le API su cui si poggia.

8.7.3 Scegliere accuratamente il tool di supporto al test


La scelta del tool di supporto più idoneo per le proprie esigenze è importante in quasi ogni
lavoro che si intraprenda, tuttavia in alcuni casi il tool più adeguato può veramente fare la
differenza: è il caso dei tool per il supporto dei test dell'interfaccia utente. In particolare, è
importante selezionare framework in grado di:
1. raggiungere un elevato livello di astrazione dagli aspetti grafici dell'interfaccia; per esem-
pio, è fortemente consigliato evitare tool che tentino di costruire la mappa dell'interfaccia
utente attraverso coordinate cartesiane;
2. semplificare enormemente non solo la configurazione iniziale dell'interfaccia utente del
sistema, ma anche le inevitabili variazioni;
3. simulare il più fedelmente possibile le tipiche interazioni dell'utente.

8.7.4 Non reinventare la ruota


Questo suggerimento ha indubbiamente una valenza universale... Soprattutto in tutti quei casi
in cui ciò che si vuole reinventare non è strettamente collegato al business degli utenti, bensì
l'oggetto è uno strumento di supporto, come in questo caso. Non è infrequente infatti imbat-
tersi in team di sviluppo impegnati a implementare propri framework di test che, inevitabil-
mente, finiscono per evidenziare tutte le tipiche limitazioni de sistemi ideati/disegnati male,
costruiti in breve tempo senza una larga base conoscitiva etc. In questi casi, una strategia deci-
samente migliore consiste nel ricorrere a framework open source: se proprio non si resiste
all'irrefrenabile desiderio di costruire qualcosa di proprio, è possibile partecipar allo sviluppo
comune di un progetto.
Un esempio non infrequente di contravvenzione a questa regola basilare è data da team che,
dopo aver implementato interfacce utente utilizzando le API Java Swing, decidano di implemen-
tarne il sistema di test ad hoc ricorrendo alla classe Java Robot (java.awt.Robot). Questa classe è
stata introdotta con la versione Java DSK 1.3 al fine di facilitare l'implementazione di test auto-
matici della UI (ciò però non significa che i team di sviluppo debbano utilizzare un tool a così
basso livello quando sono disponibili una miriade di framework a più alto livello). Robot è utiliz-
zata per generare nativi eventi di input, demo autogestite e altre applicazioni che richiedano il
controllo del mouse e/o tastiera. Questo approccio, nella stragrande maggioranza dei casi si
rivela uno grosso spreco di risorse; i limiti emergono in tutta la loro drammaticità con le inevita-
bili variazioni dell'interfaccia utente per via del grandi ripercussioni generate sul sistema di test.

8.8 Test di integrazione delle performance


I precedenti paragrafi sono stati dedicati ai test di carattere prettamente funzionale: il loro obiet-
tivo consiste nel certificare che il sistema implementi correttamente i vari servizi o, adottando
un'ottica meno ottimista, nell'individuare il maggior numero possibile di errori. Sebbene queste
verifiche siano molto importanti, da sole non sono assolutamente sufficienti. Infatti, un altro
importante insieme è dato dai requisiti non funzionali e in particolare dalle performance. La
verifica del raggiungimento di questi requisiti richiede la realizzazione di tutta una serie di test.
Da tener presente che la definizione dei vari servizi dovrebbe essere accompagnata dal loro
livello di qualità (SLA, Service Level Agreement). Questo, tipicamente, stabilisce una serie di
accordi come quelli relativi a latenza e throughput del servizio sia quando il o i server sono a
riposo, sia quando questi sono sottoposti a un grande stress. Queste informazioni sono di vitale
importanza in tutti quegli ambienti in cui il fattore tempo giochi un ruolo fondamentale. Tanto
che in questi è molto sentita la necessità di implementare una serie di test atti a verificare il
soddisfacimento dei SLA con gli utenti e/o altri sistemi client.
Per avere un'idea della complessità di questo argomento è sufficiente considerare i principa-
li tool open source disponibili per implementare tali test (Apache JMeter, Benerator, CLIF,
Curl-loader, DOTS Database Open-source test suite, DBMonster, Deluge, Dieseltest, Faban,
FunkLoad, Grinder, Hammerhead, Hammerora, httperf, http load, JCrawler, Lobo, NTime,
OpenSta, OpenWebLoad, p-unit, Seagull, Siege, Sipp, SLAMD, Soap-stone, Stress_driver,
TestMaker, TPTest, Tsung, Valgrind, Web Application Load Simulator...). Pertanto, compati-
bilmente con gli obiettivi del presente testo, in questo paragrafo vengono fornite un'insieme di
regole di sopravvivenza, demandando ai lettori più volenterosi i necessari approfondimenti.

8.8.1 Implementare i test delle performance


Ogni sistema che si rispetti dovrebbe essere corredato da un'insieme di informazioni relative
agli aspetti non funzionali salienti, come per esempio la latenza tipica di ogni servizio, quella a
sistema sotto-stress, la degradazione del servizio in funzione del numero di utenti contempora-
neamente collegati, etc. Queste informazioni costituiscono uno dei criteri principali per deci-
dere se il sistema sia pronto o meno per essere messo in esercizio.
La logica conseguenza è che, volenti e dolenti, questi test devono essere eseguiti almeno
prima del rilascio di ogni nuova release del sistema. Inoltre, i risultati di questi test rappresen-
tano informazioni di fondamentale importanza per altre attività come il tuning dell'applicazio-
ne. Pertanto, le opzioni disponibili sono due: eseguire ogni volta questi test manualmente con
tutti i relativi svantaggi, oppure implementare, una volta per tutte, una batteria automatica di
test atti a valutare le performance del sistema. L'esperienza insegna che la seconda strategia
tende a evidenziare tutti i suoi vantaggi nel medio-lungo termine.

8.8.2 Valutare l'implementazione del test continuo delle performance


Come visto in precedenza, i dati relativi alle performance del sistema costituiscono una sorta di
scheda caratteristica ("personale") del sistema e come tali dovrebbero essere verificati ad ogni
major release dello stesso. In alcuni contesti, come per esempio i sistemi di front-office bancari,
in cui la performance dei vari servizi assume un'elevata criticità, è consigliabile implementare
soluzioni in grado di monitorare continuamente le performance dei vari servizi, o perlomeno di
quelli particolarmente critici, al fine di intercettare immediatamente eventuali deviazioni dai
requisiti non funzionali. Chiaramente la condizione necessaria ma non sufficiente per imple-
mentare tali test è l'aver definito chiaramente il SLA dei servizi oggetto di test.

8.8.3 Utilizzare un ambiente appropriato


Sebbene l'ambiente di sviluppo rappresenti l'habitat naturale per mettere a punto i vari test
delle performance, questo rarissimamente rappresenta l'ambiente ideale per eseguirli. Questo
per una serie di ben ovvi motivi, tra cui i principali sono relativi al fatto che l'ambiente è tipica-
mente molto differente da quello di produzione, e che è decisamente più difficile controllare gli
elementi ambientali. Eseguire i test nell'ambiente di sviluppo per ottimizzare il sistema può
essere addirittura dannoso: i dati forniti potrebbero essere drammaticamente diversi da quelli
che si otterrebbero nell'ambiente di produzione. Ciò potrebbe addirittura portare a realizzare
delle presunte ottimizzazioni che in realtà risultano pessimizzazioni.

8.8.4 Minimizzare le influenze ambientali


Realizzare accurate misurazioni dei sistemi software è spesso un'attività tutt'altro che banale.
Ciò per via di una serie di motivi, incluse le condizioni ambientali: efficienza del network, stato
dei vari server, servizi in funzione, etc. Per comprendere quanto queste possano incidere sui
propri test, è sufficiente considerare che, anche nell'infrequente situazione in cui un server ese-
gua esclusivamente l'applicazione da monitorare, il sistema operativo è comunque impegnato
da una serie di task che generano un significativo impatto sulle misurazioni. Le attività del siste-
ma operativo e di altre applicazioni tendono ad aumentare il livello di competizione nell'aggiu-
dicarsi le risorse del sistema come il microprocessore, la memoria, gli accessi sul disco e l'utilizzo
del network. Tale aumento è in grado di interferire significativamente con le varie misurazioni.
Pertanto, in tutti quei casi in cui sia necessario ottenere misurazioni accurate e/o dell'ordine
delle frazioni di secondo, è necessario minimizzare le variabili ambientali. Ciò si ottiene, per
esempio, riducendo il numero di programmi in esecuzione (possibilmente solo il sistema da
monitorare), minimizzando i servizi del sistema operatvo in esecuzione, cercando di utilizzare
un sub-network dedicato o almeno in orari in cui vi siano condizioni ripetibili di carico, etc.

8.8.5 Valutare il sistema a regime


Non è infrequente individuare test automatici (e a volte anche operati dal personale) che si
limitino a eseguire un determinato servizio un paio di volte al fine di ottenere le misurazioni
desiderate. Chiaramente non si tratta di una strategia ideale, e anzi è il modo migliore per ottene-
re dati altamente suscettibili alle condizioni ambientali. Un approccio decisamente migliore con-
siste nell'ottenere questi parametri come risultato della media dei valori ottenuti eseguendo lo
stesso servizio migliaia di volte, con diversi dati in input e soprattutto scartando le prime cinque-
cento/mille invocazioni al fine di permettere al sistema di stabilizzarsi, di simulare più fedelmen-
te l'ambiente reale e di scartare eventuali iniziali fattori aleatori (per esempio la JVM tende a
eseguire ottimizzazioni a runtime). Questo stesso approccio dovrebbe essere utilizzato per il
tuning del sistema. In particolare, l'efficacia di ogni modifica va verificata con la stessa strategia.

8.8.6 Cercare di simulare situazioni reali


La qualità della valutazione del comportamento del sistema, come lecito attendersi, è diretta-
mente proporzionale alla qualità della simulazione dell'ambiente di produzione. Pertanto è
necessario investire tempo nel cercare di ricreare il più fedelmente possibile tali condizioni. Per
esempio, sebbene sia corretto utilizzare tool di test delle performance del sistema in grado di
creare un certo numero di utenti virtuali contemporaneamente interagenti con il sistema, è
altresì importante tenere nella giusta considerazione il modus operandi di un utente vero. In
particolare, un utente medio tende a valutare la risposta ottenuta dal computer, prima di sotto-
porre un'altra; pertanto una buona simulazione dovrebbe tener conto di fattori come questo e
quindi aggiungere dei ritardi tra l'invio di due richieste successive.

8.8.7 Pianificare attentamente i dati di partenza


La corretta esecuzione di tutti i test di integrazione richiede che il sistema disponga di un certo
quantitativo di informazioni iniziali tipicamente memorizzate nel database. Scegliere tale insie-
me di dati è attività molto importante. In particolare si vuole che questi rappresentino l'insieme
che fa funzionare sia i test positivi sia quelli negativi, e che l'insieme sia il minimo possibile: è
molto importante evitare dati ridondanti. Ciò soprattutto perché questi devono essere mantenu-
ti e quindi è opportuno minimizzare l'impatto dei cambiamenti. Inoltre, questi dati devono
essere memorizzati su formati (come per esempio fogli elettronici, file di testo, etc.) facili da
gestire e da inserire nel sistema prima dell'esecuzione di ogni test. Ciò spesso comporta la neces-
sità di implementare tool atti a inserire automaticamente tali dati nel database del sistema.
Capitolo
Organizzazione
di un progetto, build
e deploy
Introduzione
Questo capitolo è dedicato ad una serie di argomenti organizzativi di importanza fondamenta-
le per progetti di medie e grandi dimensioni, come la struttura da utilizzare per organizzare i
progetti software, e i processi di build e deploy. Sebbene si tratti di argomenti di estrema impor-
tanza, non è infrequente il caso in cui questi siano trascurati fino al momento in cui il livello di
entropia del progetto abbia raggiunto livelli tali per cui non sia più possibile procedere oltre
senza prima aver dato luogo a una totale riorganizzazione dei vari moduli del progetto, ad una
razionalizzazione del processo di build e deploy, e così via.
Il problema di fondo è che ogni progetto dovrebbe disporre di almeno una risorsa dedicata
a tale scopo. Ciò, spesso anche per false economie, non è possibile e quindi ci si ritrova nella
spiacevole situazione di dover assegnare questo compito ad un membro del team dopo aver già
prodotto una serie di iterazioni.Nonostante ciò sia stato studiato appositamente per applicazio-
ni software sviluppate in Java, i concetti introdotti presentano una validità generale che esula
dallo specifico linguaggio di programmazione. Inoltre, sebbene la struttura presentata sia ab-
bastanza indipendente dai vari tool, come Ant e Maven, questi ultimi presentano vincoli e
offrono importanti vantaggi che, probabilmente, sarebbe poco intelligente non considerare.
Le linee guida proposte, coerentemente con l'intera impostazione del libro, sono frutto di
un approfondito studio atto ad incorporare diversi importanti elementi, come una vasta espe-
rienza nello sviluppo di sistemi software di grandi dimensioni con team dislocati in diverse aree
geografiche, utilizzo di eterogenee tecnologie, impiego di tool moderni per la produzione del
software, e così via.
Disporre di una razionale organizzazione del progetto è particolarmente utile per una serie
di motivi, tra i quali i principali sono:

• 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:

1. struttura del progetto dal punto di vista del file system;


2. processi di build e deploy

Le tematiche presentate sono stata elaborate considerando i seguenti principi base:

• 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:

1. generare su richiesta i file di progetto per l'IDE richiesto;


2. supportare lo sviluppo del codice e l'esecuzione dei test;
3. generare nuovi manufatti da "pubblicare" in luogo condiviso;
4. eseguire i test di integrazione e i regression test (possibilmente sempre all'interno dell'IDE)

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:

1. integrare il processo di build con altri tool, come per esempio:


a. quello per la gestione dei sorgenti (SCM, Software Configuration Management quali
CVS, SVN, ClearCase, etc.)
b. quello dedicato alla continua integrazione del sistema, necessario per verificare con-
tinuamente che lo stato del build del sistema, identificare tempestivamente possibili
errori notificandoli all'apposito personale;
2. disporre un sito in cui pubblicare le varie informazioni relative al build;
3. produrre rapidamente i vari manufatti richiesti dal sistema, pubblicandoli in un apposi-
to spazio condiviso;
4. personalizzare i vari manufatti necessarie per l'installazione nei vari ambienti: integration
test, UAT e produzione, previa corretta esecuzione dei vari test previsti;
5. generare nuove versioni, opportunamente etichettate, in maniera consistente;
6. fornire la possibilità di eseguire il roll back ad una precedente versione funzionante
qualora quella corrente presenti dei malfunzionamenti.

I tool: filosofia e uso di Ant e Maven


Ant
Ant è uno strumento atto ad automatizzare il processo build delle applicazioni, ideato e svilup-
pato in Java, presso quella fucina di idee e software open source di Apache (http://ant.apache.org/).
Inizialmente disegnato per supportare il build delle applicazioni Java, Ant ha riscosso un succes-
so così clamoroso che ne è stato effettuato il porting del codice per altri ambienti come per
esempio .Net. La versione disponibile al momento in cui viene scritto questo capitolo è la 1.7.0
(rilasciata nel dicembre 2006).
L'autore iniziale di Ant, James Duncan Davidson, scelse il nome come acronimo di "Another
Neat Tool" (un altro strumento lineare, "pulito" se traduciamo letteralmente). Tuttavia
l'omonimia con il nome "formica" fu ovviamente voluta... In effetti, si volevano enfatizzare
concetti come la capacità delle formiche di eseguire un ottimo lavoro nel costruire ("ants do an
extremely good job in building things"), e il fatto che, per quanto molto piccole, le formiche
possono caricarsi di pesi dozzine di volte superiori al proprio ("ants are very small and can
carry a weight dozens of times their own").
L'ideazione di Ant avvenne all'interno del progetto TomCat per risolvere, come spesso acca-
de, un problema pratico: semplificarne il processo di build evitando i problemi esistenti dei
build implementati ricorrendo agli esistenti make script. Da tener presente che ogni nuova ver-
sione di TomCat doveva essere preparata (effettuarne il build) e verificata sulle principali piatta-
forme di produzione, e pertanto era spesso necessario aggiornare tutta una serie di build scripts.
La prima versione "utilizzabile" di Ant fu la 1.1. rilasciata nel Luglio del 2000 insieme alla
versione TomCat 3.3. Da allora tale è stato il successo che attualmente è presente in moltissimi
progetti Java.
Ant è un parente (remoto) della famiglia delle applicazioni in grado di eseguire script di
build. Alcuni antenati famosi sono make, gnumake, namake e jam. Sebbene si trattasse di tool
molto utili, questi presentavano serie limitazioni di cui la più importante, probabilmente, era
dovuta alla loro dipendenza dalla piattaforma (lava ancora non era stato implementato!). So-
prattutto le versioni per ambienti Unix erano a tutti gli effetti dei script Shell. Ciò, se da un lato
era conveniente perché ne aumentava la flessibilità ed estendibilità, dall'altro ne limitava il
livello di standardizzazione (apprendere il funzionamento di un uno di questi file era pressoché
equivalente ad apprendere un modulo di codice!), difficili da implementare e controllare, etc.
Inoltre, erano strumenti datati non più compatibili con le esigenze dei moderni linguaggi di
programmazione. Per esempio, il build delle applicazioni Java richiede di compilare tutte le
singole classi. L'esecuzione di uno script Make in tali circostanze richiederebbe di lanciare, per
ogni singola classe, il compilatore Java (JavaC). Moltiplicando ciò per mille, diecimila classi,
l'impatto era decisamente notevole, specialmente considerando il numero di volte che, durante
10 sviluppo di un'applicazione, questa viene compilata per essere verificata. Questi script, inol-
tre, erano veri e propri mini programmi difficili da scrivere, manutenere e verificare. Inoltre,
non presentando alcuna interazione con la macchina virtuale Java, non erano in grado di
interagirvi per realizzare servizi avanzati, etc.

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 è:

<nome-task attributo1="valore1" attributo2="valore2"... />

La riga successiva mostra un esempio di utilizzo del comando per la cancellazione dei file:

<delete file="xyz.txt" failonerror="false" />

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>

< ! — set global properties for this build — >


<property name="src" location="src/main/java" />
<property name="lib" location="/lib" />
<property name="build" location="target/classes" />
<property name="dist" location="target" />
<property name="webxml" location="src/main/resources/META-INF" />
«property name="prjdepjar" location^'../.." />
«property name="distr.filename" value="myProject.war" />

< ! — ant all — >


ctarget name="all" depends="-init, -compile, -war, -term" />

< ! — ant tnit — >


«target name="-init">
< ! — Create the time stamp — >
<tstamp/>
< ! — cleanup the existing jar file — >
«delete file="$ldist|/$(distr.filenamel" failonerror="false" />
< ! — cleanup the .class files directory — >
«delete dir="$|buildl" includeEmptyDirs="true" failonerror="false" />
«mkdir dir="$|buildl" />
</target>

< ! — compilation — >


«target name="-compile">
«javac destdir="|build)">
<classpath>
«pathelement location=
"${lib|/commons-cli/commons-cli/1.0/commons-cli-1,0.jar"/>
«pathelement location="${lib}/log4j/log4j/1.2.14/log4j-1.2.14.jar"/>
«pathelement location=
"$|libl/javax/servlet/servlet-api/2.2/servlet-api-2.2.jar"/>
«pathelement location =
"$lprjdepjar)/xmlservices/implementation/dist/ xmlservice.jar"/>
«pathelement location=
"$|prjdepjar)/common/implementation/dist/filepoller.jar"/>
</classpath>
<src path="$|src|" />
«/javac>
</target>

< ! — war — >


<target name="-war">
<war destfile="$ldlst|/$|distr.filename|" webxml="$(webxml]/web.xml">
<classes dir="$|build|" />
<lib file="$|libl/log4j/log4j/1.2.14/log4j-1.2.14.jar" />
<lib file="$(lib|/commons-cli/commons-cli/1.0/commons-cll-1.0.jar"/>" />
<lib file="$(lib]/javax/servlet/servlet-api/2.2/servlet-api-2.2.jar " />
<lib file="$|libl/com/sun/xml/bind/jaxb-impl/2.1,3/jaxb-impl-2.1,3.jar "/>
elib file="$|lib]/javax/xml/jaxb-api/2.0/jaxb-api-2.0.jar"/>
elib flle="$|lib|/com/sun/xml/bind/jaxb-impl/2.1.3/jaxb-impl-2.1.3.jar"/>
<lib file="${lib]/com/sun/xml/bind/jaxb-xjc/2.1,3/jaxb-xjc-2.1,3.jar"/>
<lib file="$|lib|/javax/xml/bind/jsr173_apl/1.0/jsr173_api-1.0.jar
<lib file="$lprjdepjar|/xmlservices/lmplementation/dist/xmlservice.jar" />
<lib file="$lprjdepjar|/common/implementation/dist/filepoller.jar" />
</war>
</target>

< ! — final clean-up — >


<target name="-term" >
< ! — cleanup the .class files directory — >
<delete dir="$|build|" includeEmptyDirs="true" failonerror="false" />
cmkdir dir="$|buildl" />
</target>

</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:

<property name="src" location="src/main/java" />


<property name="distr.filename" value="myProject.war" />

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.

<src path="$(src)" />

Ant, come è lecito attendersi, dispone della seguente lista di proprietà predefinite (built-in):

• basedir: percorso assoluto della directory di base del progetto


• ant.file: percorso assoluto del file di build
• ant.version: versione di Ant che sta eseguendo il build
• ant.project.name: nome del progetto in corso di esecuzione
• ant.java.version: versione della JVM individuata da Ant

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:

<target name="all" depends="-init, -compile, -war, -term" />

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).

Breve storia di Maven


L'idea iniziale, come spesso accade nel mondo dell'Open Source, e in maniera completamente
analoga a quella che ha portato alla realizzazione di Ant, è nata dall'esigenza di risolvere un
problema pratico, inizialmente scaturito, nell'ambito del progetto Jakarta Alexandria (attual-
mente abbandonato) e quindi migrata al progetto Turbine (framework Servlet disegnato per la
rapida produzione di applicazioni web). L'obiettivo iniziale era di implementare un tool atto a
semplificare, uniformare ed automatizzare il processo di build di sistemi complessi. A tal fine
era necessario sia creare un modello di progetto, sia una struttura di file system standard. Ciò
anche per risolvere un'altra problematica, ossia quella di far in modo che i diversi progetti
Apache funzionassero in maniera analoga. In effetti, prima del rilascio di Maven, ciascun pro-
getto Apache, presentava differenti approcci alla compilazione, distribuzione e alla generazio-
ne del sito web relativo al progetto, creando evidenti problemi relativi all'allocazione delle
risorse umane, al riutilizzo di script, all'applicazione di best practice, e così via. Questi proble-
mi non erano da poco, visto che non era infrequente il caso in cui sviluppatori dovevano spen-
dere una notevole porzione del loro tempo a configurare i vari ambienti, a comprenderne il
funzionamento e a mantenere i vari script di compilazione. Ciò, oltre a distrarli da quello che
invece doveva essere il loro obiettivo principale: realizzare sistemi software di elevata qualità,
spesso finiva anche per far desistere nuovi potenziali collaboratori.
Dato l'elevato livello di interdipendenza dei progetti open-source, la capacità di Maven di
standardizzare l'ubicazione dei vari file (sorgenti, documentazione, distribuzione ,etc\), di for-
nire una struttura comune per i vari progetti, di permettere il reperimento dei file di distribu-
zione attraverso un meccanismo di repository condiviso e di far sì che il processo di build
diventasse più agevole, trasparente e meno complesso, furono elementi chiave che ne assicura-
rono il successo fin dalle prime versioni.
Già Ant aveva risolto molti problemi del processo di build; Maven, al netto di malfunzionamenti,
ha fatto un passo in avanti, risolvendo diversi problemi presenti nello sviluppo di sistemi com-
plessi, basati su sotto-progetti interdipendenti. In particolare, Maven ha permesso di:

• ridurre considerevolmente la necessità di disporre di diversi script di build (questi sono


sostituiti da file POM) da eseguire secondo un rigido ordine, di copiare porzioni di file
di script da un progetto ad un altro con la conseguente necessità di doverne gestire una
moltitudine;
• rimuovere l'esigenza di copiare i vari file JAR, soprattutto di quelli prodotti dai vari
sotto-progetti. Copiare i file .jar è una pessima soluzione che può essere eliminata anche
con Ant, ma solo attraverso l'implementazione di complessi plugin;
• risolvere i problemi relativi alla condivisione delle librerie (diverse versioni, mancanza
di allineamento, memorizzate più volte e quindi l'aggiornamento di tutte era spesso
problematico, e così via).

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:

• semplificazione del processo di build. In particolare, Maven si fa carico di risolvere tutta


una serie di dettagli senza dover ricorrere all'utilizzo di file di script;
• sviluppo di un ambiente uniforme di build. Maven gestisce progetti basati sul proprio
Modello Oggetto del Progetto (POM, Project Object Model). Per quanto molti aspetti
possano essere personalizzati, Maven dispone di una serie di "standard", studiati
appositamente per essere utilizzati efficacemente nei vari progetti. Pertanto, una volta
compreso il funzionamento di un progetto gestito da Maven è automatico comprendere
qualsiasi altro (a meno di forti personalizzazioni). Ciò evidentemente si traduce in un
notevole risparmio di tempo, energie e frustrazione.
• produzione di informazioni qualitative circa il progetto. Per quanto Maven non sia né uno
strumento di documentazione né un generatore di siti web, offre un'ottima infrastruttura
che permette di utilizzare tutta una serie di plug-in per la generazione di informazioni utili
relative ai progetti. Per esempio, permette di generare documenti relativi alle variazioni
effettuate (interagendo con i sistemi di source control), ai riferimenti incrociati dei sorgenti,
alle mailing list, alle dipendenze, ai rapporti relativi alla copertura del codice da parte dei
test, etc. Il tutto in maniera assolutamente trasparente ed automatica.
• erogazione di linee guida corredate da un opportuno supporto per l'applicazione di
best practice per lo sviluppo di sistemi. L'esempio più evidente è relativo alla presenza
esplicita, nel processo di build standard, della definizione, specifica ed esecuzione dei
test di unità. Inoltre vi sono workflow necessari per il rilascio e la distribuzione di nuove
release del sistema.
• supporto alla migrazione, quanto mai trasparente, verso nuove feature.
Caratteristiche principali di Maven
Le principali caratteristiche offerte da Maven 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:

• coerenza: le varie organizzazioni possono standardizzare la gestione dei progetti Java


utilizzando l'insieme di best practice alla base di Maven. Accettando una serie di
convenienti standard, si accede a tutta una serie di servizi predefiniti. Il livello di qualità
dei vari progetti, inevitabilmente, si eleva, i progetti stessi diventano quindi più trasparenti,
si minimizza il tempo necessario per comprendere i vari progetti e quindi si facilita il
movimento delle risorse umane.
Prolile
(profile.xml)

Maven Maven Maven


plug-in plug-in plug-in

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

Figura 9.1 - Componenti principali di Maven.

• 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.

Componenti principali di Maven


Nella figura 9.1 sono evidenziati i componenti principali di Maven. Brevemente:

• 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:

mvn archetypexreate -Dgroupid=com.mokabyte.finance -Dartifcatld=PriceBlotter

L'esecuzione di questo comando genera, essenzialmente, due azioni:

1. la creazione della directory con il nome dell'applicazione (PriceBlotter), con la relativa


struttura base (questa include la directory per i file sorgenti e per i corrispondente file di
test);
2. la generazione del file pom.xml base.

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

figura 9.2 - Archetipo "standard" Maven.

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>

<!— POM Relationships —>


<groupld>.. </groupld>
<artifactld>.. .</artifactld>
<version>...</version>
<packing>...</ packing>
<parent>...</parent>
<dependencyManagement>.. .</dependencyManagement>
<dependencies>.. .</dependencies>
<modules>.. ,</modules>

<!— Project Information —>


<name>...</name>
<description>.. ,</description>
<url>...</url>
<inceptionYear>.. ,</inceptionYear>
<licenses>...</licenses>
<developers>.. .</developers>
<contributors>.. .</contributors>
<organization>.. .</organization>

<!— Build Settings —>


<packaging>.. .</packaging>
<properties>... </properties>
<build>...</bui\d>
<reporting>.. ,</reporting>

<7— Build Environment —>


</— Environment Information —>
<issueManagement>.. .</issueManagement>
<ciManagement>.. .</ciManagement>
<mailingLists>.. .</mailingLists>
<scm>...</scm>

<7— Maven Environment —>


<prerequisites>...</prerequisites>
<repositories>.. .</repositories>
<pluginRepositories>.. ,</pluginRepositories>
<distributionManagement>.. .</distributionManagement>
<profiles>...</profiles>
</project>

I campi assolutamente obbligatori di un POM sono le coordinate necessarie per identificare


univocamente un file di distribuzione. In particolare:

1. groupld: (identificatore del gruppo);


2. artifactld: (identificatore del manufatto);
3. version: (versione).
groupld
Coordinates artifaclld
version

Aggregation Modules module

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

Configuration piugm specific

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

Figura 9.7 - Struttura del POM relativa alle impostazione dell'ambiente.

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:

• POM. In questo caso è necessario impostare le sezioni <repositories> <repository>...


<repositories> <repository>. Poiché è una sezione ereditabile, è possibile limitarse ad
impostarla nel P O M genitore e quindi ereditarla automaticamente in tutti i POM figli.
Da notare che i repository di default sono presenti nel POM di Maven di default: il
famoso super POM.
• settings. In particolare, è necessario impostare l'apposita lista nel file dei settings
memorizzato localmente (~/.m2/settings.xml; in macchine Windows, un tipico path è:
c:\Documents and Settingsklogin id>\.m2).

Indipendentemente dalla loro tipologia i repository mantengono la medesima struttura. Il


repository centrale più famoso, per motivi storici, è sicuramente iBiblio (http://www.ibiblio.org/
maven/). In effetti, l'intero meccanismo del repository Maven si deve proprio all'incontro tra il
team Maven e quello di iBiblio. Data questa notorietà, è praticamente impossibile utilizzare
direttamente il repository iBiblio: le prestazioni sono insoddisfacenti. Lo stesso sito invita a
utilizzare gli appositi siti mirror, che, tutto sommato, non sembrerebbero presentare grosse

. 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.

Figura 9.9 - Esempio tipico di organizzazione dei respository server di Maven.


Adcfress ^J C\
: Documents and Setr*3s\lvetita\.m2\repository\(og4j\log-1)U .2. M
Folders X Marne * Sfec I T y p e
L-: O m .2 _^]lüg*)]-l .2. M| . ar 359 KB Executable Jar File
It 'O repository plog4]-1.2.1t.jâr.shal 1 KB SHA1 File
[ti O ant jà|log4]-1.2,14.pom 3 KB POM Fie
antlf :Ej|loq4j-l.¿.14.pom.shal I KB SHA1 File
••"*i asm
i ••»* avalon-framework
!•! ^J cgkb
i* ^ dassworlds
It. com
1 commons-beanurt its
'ti ' ^ commons-codec
j commons-colections
•' I i 'i commons-configuration
I»! . *> commons-digester
r*, ^ commons-ixpath
it. commons-lang
'*. , 'i commons-loggrig
'i dorrri)
l±I lJ easymock
'•i ; e chopomt
I* Ii *i ehcache
"Ji freemarker
J hsqldb
•+: javax
•I 'i )boss
!•) t_J jdorri
[*] ¡_J (line
i+i LJ lunit
:-] Ú log4|
-J ü log-i)
Cl 1-2.12
iti J logkit
it] net
it|lJ org

Figura 9.10 - Esempio della struttura di un repository locale.

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

che si o c c u p a di ciò è denominato "dependency management engine"). In particolare, e molto

ma molto brevemente, dato un file o un qualsiasi testo, il processo di hashing si o c c u p a di generare

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:

• groupld: per esempio: org/hibernate


• tipo di file: jars, javasource, javadoc, licenses, poms; per esempio: org/hibernate/jars
• artifactld: per esempio: org/hibernate/jars/hibernate
• versione: per esempio: org/hibernate/jars/hibernate-3.1.2.jar
Da notare che non tutti i repository rispettano esattamente la stessa struttura. Il problema è
che attualmente vi è una mescolanza di stili. Questo genera una serie di problemi, per esempio
è possibile notare come la struttura dei manufatti Hibernate vari a seconda della release:
groupld=hibernate (Hibernate 1-3), groupld=net.sf.hibernate (Hibernate 1-2), groupld=org.hibernate
(Hibernate 3).

I comandi di Maven
I comandi Maven presentano una struttura standard descritta nella tabella 9.1. Per esempio:

mvn archetypexreate -Dgroupid=com.mokabyte.finance -Dartifcatld=PriceBlotter

L'esecuzione di questo comando genera, essenzialmente, due azioni:

1. la creazione della directory con il nome dell'applicazione (PriceBlotter), con la relativa


struttura base (questa include la directory per i file sorgenti e per i corrispondente file
di test);
2. generazione del file pom.xml base.

Elemento Descrizione Obbligatorio

mvn serve per e s e g u i r e il p r o g r a m m a M a v e n Si

<nome-plugin> n o m e del plug-in da c o n s i d e r a r e Si

Si

[<nome_goal>] n o m e del goal da e s e g u i r e tra quelli presenti nel plug-in No

[l<parametro>]| lista dei parametri da Fornire al goal da e s e g u i r e No

Tabella 9.1 - Struttura dei comandi Maven.

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

g e n e r a il file di d i s t r i b u z i o n e (nel file p o m . x m l di default; il file di


m v n package
d i s t r i b u z i o n e è un J A R )

p e r m e t t e di installare il file di d i s t r i b u z i o n e nel r e p o s i t o r y . in m o d o


m v n instali
tale c h e progetti d i p e n d e n t i p o s s a n o utilizzarlo

m v n site g e n e r a un sito w e b b a s i l a r e c o n le i n f o r m a z i o n i del p r o g e t t o

m v n clean r i m u o v e dalla d i r e c t o r y t a r g e t i file g e n e r a t i in p r e c e d e n z a

m v n idea:idea g e n e r a i file descrittori del p r o g e t t o utilizzati d a l l ' I D E InrelliJ I d e a

m v n eclipse:eclipse g e n e r a i file d e s c r i t t o r i del p r o g e t t o utilizzati d a l l ' I D E I B M E c l i p s e

Tabella 9.2 - Comandi base di Maven.


Concetto Maven Ant

File standard di build pom.xml (settings.xml) build.xml

Tasks invocabili goal Target

File con i meta-dati del progetto pom.xml -

Gestione delle dipendenze in


Si (rcpository) -
termini di file di distribuzione

Definizione di goal/task
plug-in -
cross-project

XML-Java
Definizione di goal/task XML-Java
(Jelly fino alle versione 2)

Riutilizzo di task cross-project Si Si

Sono rigidamente incapsulate


Sono abbastanza statiche a meno
nell'implementazione, ma possono
Regole di build che non si faccia ricorso al task
essere alterate interagendo con
<script>.
sofisticate API.

Linguaggio di scrittura dei plug-in Java Java

L e regole di build non sono


I goal possono essere estesi estensibili per loro disegno, ma
Estensibilità delle regole utilizzando tag quali <preGoal> utilizzando i task <SCript> è possibile
e <postGoal> simulare un comportamento simile
ai tag <preGoal> e <postGoal>.

Tabella 9.3 - Similitudini e differenze tra Maven e Ant.

La tabella 9.2 mostra l'insieme dei comandi base di Maven.

Confronto tra Ant e Maven


Dalla lettura dei paragrafi precedenti, dovrebbe essere chiaro che i due tool hanno diversi
obiettivi e, soprattutto, un diverso livello di astrazione. Ant, essenzialmente è un tool per il
build cross-platform di progetti Java, mentre Maven opera a un maggiore livello di astrazione
"orchestrando" molti task Ant.
Tuttavia, a meno di qualche pecca di instabilità, Maven dovrebbe "rimpiazzare" Ant nella
gestione dei processi di grandi dimensioni. Si tratta tuttavia, come specificato poco sopra, di
una sostituzione molto particolare dal momento che i goal Maven possono invocare task Ant, e
tipicamente lo fanno. La tabella 9.3 fornisce un confronto tra le principali caratteristiche dei
due tool. Va notato che Maven 2.0 ha incrementato il livello di protezione del ciclo di vita del
build: la conseguenza è che l'integrazione di Maven con l'ambiente esterno è diventata più
complessa/costosa.
Concetto Ant Maven
Tempo per scaricare il prodotto 10 minuti 10 minuti

Installazione Semplice ed immediata Semplice ed immediata

2 0 minuti (5 riutilizzando le
Tempo per iniziare un nuovo progetto
5 minuti impostazioni di un precedente
(semplice)
progetto)

Molto, soprattutto con


Tempo per iniziare un nuovo progetto
progetti molto 30 minuti
(complesso)
complessi/articolati

Aggiungere nuovi target/goal (semplici) 10 minuti (target) 5 minuti (goal esistente)

Aggiungere nuovi target/goal (complessi) 10 minuti (target) 5 minuti (goal esistenti)

Implementazione di un nuovo target/goal


10 minuti (target) Ore
(semplice)

Implementazione di un nuovo target/goal


Ore Giorni
(complessi)

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.)

Non definita: ogni team può


organizzare il proprio
Predefinita. Tuttavia è possibile
progetto a piacimento.
Struttua standard del progetto apportare diverse variazioni anche
Significa però dover spiegare,
importanti
a ogni nuovo elemento del
team, struttura, script, etc.

Si, demandata però


Supporto dei progetti complessi basati all'implementazione di
Si, nativamente
su più protetto appositi script (è necessario
definire quello "master")

Disponibilità di tool/plug-in Elevata Elevata

Elevata (alcuni IDE basano il


Integrazione con I D E loro progetto direttamente su Elevata
script Ant)

Demandato al team e / o ai vari


Definizione del sito di documentazione Nativa
tool

Apertura della struttura e meccanismi


Elevata Ridotta
interni

Limitata e inconsistente (ma


Documentazione Abbondante ed accurata ultimamente esistono articoli e
libri)

Frequenti: a volte è necessario


Bugs/Problemi Quasi inesistenti
modificarli in prima persona o

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.

Struttura di progetti medio-grandi


Maven, come visto in precedenza, prevede una propria struttura standard, denominata
archetype, per organizzare i manufatti presenti nei progetti software (figura 9.2). Maven inoltre
fornisce appositi strumenti per personalizzare l'archetipo, sebbene ciò sia necessario in rarissi-
me occasioni. Tale struttura, tuttavia, è solo il punto di partenza. In effetti, da sola non sarebbe
sufficiente per organizzare progetti di medio grandi dimensioni. L'attenzione inoltre è comple-
tamente confinata ad aspetti prettamente implementativi, trascurando altre esigenze, come, per
esempio, quelle di memorizzare il modello dei requisiti.
L'obiettivo di questo paragrafo è fornire una serie di linee guidae per organizzare al meglio i
propri progetti software, tenendo presenti sia le tipiche esigenze dei progetti di medio-grandi
dimensioni, sia la necessità di memorizzare manufatti a carattere non strettamente
implementativo.
Si cominci con il considerare la struttura riportata in figura 9.11. Il punto di partenza è il file
system destinato ad ospitare i sorgenti di un'intera organizzazione. A dire il vero questo livello è
stato riportato prettamente a carattere rappresentativo: un livello di astrazione così alto non è
molto frequente. Tipicamente è possibile solo in organizzazioni di piccole dimensioni. Nella
maggior parte dei casi è disponibile un grado di visione al livello di dipartimento o
sottodipartimento (come per esempio mokabyte finance). Qui sono allocati i vari progetti (come
per esempio: globalfrontoffice) e, qualora i team di sviluppo siano dotati di buona volontà, librerie
e componenti di utilizzo generale (come per esempio un framework generico per implementare
le interfacce utente, una libreria con tutti i messaggi, incluse le corrispondenti rappresentazioni
ad oggetti ed i servizi di conversione, utilizzati all'interno del dipartimento). Purtroppo, non è
molto frequente il caso in cui librerie e componenti riutilizzabili riescano a raggiungere un livello
di adozione superiore a quello del dipartimento. E consigliabile che la struttura di ciascuna di
queste directory sia organizzata secondo l'archetipo di figura 9.2. Per quanto concerne invece i
progetti (come per esempio il nuovo sistema di front office globale di una banca di investimenti)
questi sono tipicamente costituiti da un insieme di sotto-sistemi specializzati nel fornire un ben
definito insieme di servizi. Pertanto è prassi comune redigere un POM genitore di tutti i sotto-
progetti da memorizzare a questo livello. Da questo pom poi ereditano tutti le librerie, compo-
nenti e sotto-progetti (come per esempio il price blotter, in componente per la negoziazione
automatica, etc.) appartenenti allo stesso programma. Come tali, questi componenti sono me-
morizzate in apposite sotto-directory del progetto, la cui struttura, come lecito attendersi, do-
vrebbe essere basata sull'archetipo di figura 9.2. Non è infrequente poi il caso in cui si riescano
o si debbano estrapolare librerie e componenti riutilizzabili solo al livello di progetto.
Il fine ultimo della stragrande maggioranza di progetti informatici consiste nel consegnare
codice eseguibile. Tuttavia, per giungere a tale risultato è necessario realizzare tutta una serie di
manufatti. Alcuni esempi, sono il modello dei requisiti, il disegno del sistema e così via. Come
lecito attendersi anche questi sono a loro modo dei sorgenti da dover preservare, di cui è necessa-
rio conservarne le diverse versioni e che quindi vanno affidati al sistema di gestione dei sorgenti.
B ;"nome organizzazione > B I mokabyte

E 1 "nome dipartimento > B I finance

E 1 "componente /libreria di uso generale > E I ui framework

!
i

E 1 "componente /libreria di uso generale > B finance_messagekit

E _J "nome progetto > E __J globalfrontoffice

B __J "componente /libreria di uso generale > E ^J basic blotter


I
i
B "componente /libreria di uso generale > E aulonegoliation

E "sotto-sistema 1> E I priceblotter


I
I
E <•sotto-sistema n> E I ecn_connect

Figura 9.11 - Struttura di un progetto di dimensioni medio-grandi.

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.

9.1.1 Non utilizzare strumenti quali make


Come detto, questi tool sono un pesante retaggio del passato. Il loro utilizzo era così problema-
tico da portare alla realizzazione di strumenti moderni come Ant e Maven.

- <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

Figura 9.12 - Struttura necessaria per i restanti manufatti.


9.1.2 Valutare l'utilizzo di Maven
L'avvento del tool Ant ha permesso di risolvere tanti dei problemi presentati nei paragrafici
predenti tipici degli script make. Tuttavia ha lasciato aperti una serie di problemi, come per
esempio, la mancanza dell'unificazione del processo di build, la necessità di realizzare comples-
si script in XML, l'assenza di un repository in cui pubblicare/prelevare i vari manufatti presen-
ti, il versionamento automatico dei manufatti, e così via. Problemi risolti da Maven, che, alme-
no in teoria, rappresenterebbe la scelta assolutamente consigliata, specialmente per progetti di
medio-grandi dimensioni. Tuttavia, per ora, questo consiglio rimane ancora sul livello teorico
soprattutto per via di una serie di problemi di stabilità, ahimè, esposti sia da Maven sia da
diversi plug-in di uso comune. Ciò nonostante, con un buona dote di pazienza e buona volontà,
spesso unita a colpi di debug, è sempre possibile ottenere ottimi risultati con l'utilizzo di Maven.

9.1.3 Eseguire il build al di fuori degli ambienti di sviluppo


Gli IDE moderni, come Eclipse, NetBeans e IntelliJ Idea, permettono di eseguire il build delle
varie classi con un paio di colpi di mouse. Lo sviluppo è molto semplice ed interattivo, si lancia
la compilazione, se ci sono errori questi sono prontamente riportati in un apposito tab e con i
soliti colpi di click del mouse è possibile venir automaticamente catapultati sulla linea di codice
imputata. Si tratta di una comodità ben accettata durante la fase di sviluppo, ma che può creare
problemi qualora questo sia l'unico "processo" di build considerato. Sebbene ciò possa sem-
brare ridicolo, esistono molti team il cui processo di build corrisponde a eseguire il famoso
click del mouse nell'ambiente IDE.
I primi problemi cominciano a presentarsi quando è necessario rilasciare l'applicazione per
ambienti diversi a quelli di sviluppo, come per esempio l'ambiente di QA o addirittura quello
di produzione. In questi casi non è di certo auspicabile fornire agli utenti l'ambienti IDE.
Inoltre, è necessario eseguire il processo di build al di fuori dell'ambiente di IDE per assicu-
rarsi che tutte le librerie siano disponibili, che tutti possano eseguire il processo di build, senza
gravi problemi e per debellare la famosa sindrome del "ma funziona sul mio PC!".
Ogni processo di build deve esporre almeno tre qualità irrinunciabili, quali:

• 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.

9.2 Organizzare correttamente il proprio progetto


Maven, pur con i vari limiti, sta diventando sempre più l'ambiente di build utilizzato in molti
progetti. Anche qualora si decidesse di non ricorrere a tale tool, si consiglia di organizzare il
progetto n base al relativo archetipo. Questo perché si tratta di una struttura frutto di tanti
studi, dell'integrazione di feeback ricevuti dal mondo del lavoro, offre un elevato livello di
standardizzazione che facilita anche il movimento delle risorse, il progetto è predisposto alla
migrazione in Maven, così via.
Qualora si utilizzi Maven è consigliabile cercare di utilizzare per quanto possibile la versione
standard. I tipici vantaggi derivanti dallo status di standard, sono:
1. presenza di automatismi. I vari servizi di Maven sono predisposti per funzionare con
questa struttura e quindi nessun ulteriore operazione è richiesta per farli funzionare.
2. uniformazione dei i vari progetti presenti all'interno di un'organizzazione.
3. semplificazione dell'apprendimento dei progetti e quindi agevolazione dell'allocazione
del personale.
4. niente discussioni relative alla struttura del progetto ("dove posizionare il file X?") nessuna
necessità di creare nuove directory per memorizzare determinati file, e così via.
5. maturità. Invece di cercare di inventarsi l'ennesima struttura, con il rischio peraltro di
ottenere risultati modesti o addirittura problematici, è decisamente più facile e
conveniente, beneficiare di un disegno frutto di una lunga esperienza che include anche
l'uso di plug-in e di svariati tool. Pertanto l'utilizzo della struttura standard elimina alla
radice il rischio di utilizzare un'organizzazione che poco si adatti all'utilizzo di importanti
tool di sviluppo/plug-in.

9.3 Automatizzare il processo di build


Nei progetti di dimensioni medio-grandi è prassi standard quella di disporre di tool atti a
verificare continuamente lo stato del build del sistema. Ciò è ottenuto eseguendo sia il build
automatico del sistema sia un prestabilito insieme di test (per esempio il processo di build di
Maven include automaticamente l'esecuzione dei test). Il più famoso di questi tool è sicura-
mente CruiseControl (http://cruisecontrol.sourceforge.net/), anche se non sempre si è distinto per il
livello di stabilità. Allo stato attuale ne esistono molteplici, alcuni a pagamento altri open source
(come per esempio luntbuilt, http://luntbuild.javaforge.com/). Ognuno di questi offre tutta una
serie di servizi, quali per esempio trasparenti integrazioni con script Ant e/o tool di build come
Maven, possibilità di accedere ai vari repositori. Inoltre le tipiche impostazioni previste sono
quelle relative alle modalità con cui eseguire il build automatico, come per esempio dopo ogni
build, a tempi predefiniti (tipicamente build notturni), una combinazioni delle precedenti e
così via. Ogni build è ovviamente seguito dal processo di test.
La presenza di strumenti per il continuo build del sistema è fondamentale nei progetti di
medio-grandi dimensioni caratterizzati da diversi gruppi di sviluppatori atti a lavorare sullo
stesso sistema. In questi contesti, infatti, è abbastanza frequente, specialmente dopo i primi
rilasci del sistema, che le modifiche di diversi sviluppatori possano sovrapporsi corrompendo
lo stesso build del sistema.

9.3.1 Automatizzare il processo di build appena possibile


Una brutta abitudini difficile da debellare nel mondo dei progetto software è quella di rinviare
fino a quando la situazione non sia più sostenibile l'implementazione di un adeguato processo
automatico di build. L'idea alla base sembrerebbe essere quella di tuffarsi nella scrittura del
codice per mostrare qualche codice eseguibile ai vari manager ed utenti, per poi sistemare le
cose in un secondo momento.
Ahimè raramente questo approccio porta a buoni risultati, senza contare il tempo perso
durante le fasi iniziali per via della mancanza di un processo formale ed affidabile di build. Le
conseguenze classiche dei last-minute-build includono la realizzazione di processi di build di
bassa qualità e mal congeniati, tenuti insieme con continui e costosi prodigi (i quali spesso e
volentieri sono veri e propri costi nascosti), spesso non conformi con le policy aziendali relative
al build e al deployment dei sistemi.
Pertanto è consigliabile di cominciare a redigere il processo di build da subito facendolo
evolvere a pari passo con il sistema.

9.3.2 Investire nei tool per il build continuo


La popolarità di questi tool è così evidente da non richiedere ulteriori commenti. Tuttavia, il
loro corretto utilizzo richiede di compiere alcuni passi preliminari, tra cui la completa
automatizzazione del processo di build (non sempre semplice per progetti di medio-grandi
dimensione) e la corretta configurazione dell'integrazione tra i vari tool. Ciò fa sì che in alcuni
casi, come visto nel precedente paragrafo, la messa in utilizzo del software di build continuo sia
ritardata. Tuttavia, esperienze empiriche insegnano che questo investimento iniziale di tempo è
ben ripagato: si ha sempre codice funzionato pronto per il processo di build, il rilascio di nuove
versioni del sistema non desta particolari preoccupazioni/frustrazioni, eventuali errori dovuti
da modifiche eseguite contemporaneamente o quasi sulle stesse parti del codice sono indivi-
duati immediatamente, etc.
Qualora invece non si utilizzi un software per il build continuo, questo deve essere eseguito
in modo differito prima del deployment di una nuova versione del sistema. L'esperienza inse-
gna che in questi scenari solo poco prima di rilasciare il sistema ci si accorge di tutta una serie
di problemi, in cui anche i più banali tendono a richiedere tempi di risoluzione non trascurabili
("Chi ha eseguito questa modifica?", "Perché?", ...). Il risultato finale è che si introduce una
grave alcatorietà nel processo di rilascio di nuove versioni del sistema: si sa quando si inizia e
non si sa mai quando si finisca. Il grande vantaggio dell'utilizzo di tool per il continuo build si
apprezzano solo dopo un loro utilizzo in progetti di medio-grandi dimensioni.

9.3.3 Attendere l'esito del tool di build continuo


dopo ogni integrazione del codice
Non è infrequente il caso in cui gli sviluppatori eseguano un certo insieme di modifiche, effet-
tuino i vari test sulla propria macchina e quindi procedano con l'integrazione delle proprie
modifiche nel sistema di gestione del codice. Questa pratica è spesso fonte di noiosi e inutili
problemi. Una pratica migliore, consiste nell'eseguire una serie di passi prima di effettuare
l'integrazione dei proprio sorgenti, come assicurarsi di aver sincronizzato il proprio codice con
l'ultima versione disponibile del sistema, ed assicurarsi di eseguire nuovamente i test un volta
integrate le proprie variazioni nel sistema di gestione dei sorgenti. Ciò è molto utile sia per
verificare che il processo di integrazione del proprio codice non abbia creato problemi sia per
evitare brutte figure.

9.3.4 Valutare la possibilità di ricorrere a sanity test


Come visto nel corso di questo capitolo, i test di unità si occupano di verificare che i singoli
elementi, considerati isolatamente, presentino il comportamento previsto. Pertanto, nulla di-
cono circa il comportamento del sistema nel suo insieme. A tal fine esistono i test di integra-
zione, oggetto di studio del prossimo capitolo. Il problema di questi è paradossalmente dato
dalla loro virtù: poiché sono in grado di verificare molti aspetti del sistema, tendono a richie-
dere elevati tempi di esecuzione. Pertanto, per ottenere alcuni vantaggi dei test di integrazio-
ne, senza dover attendere ore, una buona pratica consiste nel ricorrere a test di integrazione
più superficiali (appunti i sanity test, oggetto di studio del prossimo capitolo), da eseguire
ogni volta che un programmatore esegue l'integrazione nel sistema di gestione del codice
delle proprie modifiche.
9.5 Utilizzare i repository Maven
Al fine di comprendere appieno l'importanza dei repository, è necessario far mente locale alla
situazione tipica degli ambienti che non utilizzano Maven.
In questi casi, il processo standard di condivisione dei manufatti tra i diversi team di svilup-
po prevede, nella migliore delle ipotesi, la relativa memorizzazione nel SCM (Software
Configuation Management, Software per la gestione della configurazione. Es. CVS, Subversión,
IBM Rational ClearCase, etc) del progetto, mentre in casi più drammatici è addirittura possibi-
le assistere ad un continuo scambio di file jar via e-mail!
In ogni modo, questa pratica presenta una serie di problemi. In primo è di ordine concettua-
le: i manufatti risultanti del processo di compilazione, in quanto sempre riproducibili, non
dovrebbero essere memorizzati nel SCM. Oltre questo problema strettamente di carattere
filosofico, sono presenti altri decisamente più pratici. Per esempio, la continua memorizzazione
dei manufatti nel SCM può dar luogo ad un notevole occupazione di spazio su disco. Poi vi è
il problema della versione. Qualora non sia presente, è naturale avere a che fare con tutta una
serie di problemi di allineamento, di controllo della sincronizzazione, etc. Vi dice nulla il se-
guente scambio di battute?
Sviluppatore Uno: "La libreria che avete rilasciato non funziona!"
Sviluppatore Due: "Quale versione stai utilizzando?"
Sviluppatore Uno: "Come si fa vedere la versione?"
...e via discorrendo...
Quando invece è gestita, potrebbe rendersi necessario un continuo aggiornamento delle
dipendenze del proprio progetto (è il caso in cui al nome dei i vari JAR sia aggiunta l'indicazio-
ne della versione) oppure ricadere in un caso simile alla mancanza di gestione della versione,
qualora questa sia memorizzata all'interno dei vari file. Inoltre, spesso, la loro condivisione
richiede tutta una serie di passi manuali. Poi di questi file, essendo binari (correttamente), non
può fare il merge. Infine, la presenza del repository rende il processo di check out del progetto
più veloce: non è necessario eseguire il check-out di file binari di grandi dimensioni. Questi
sono scaricati, secondo un meccanismo di lazy loading, una volta soltanto attraverso il repository.

9.5.1 Valutare la possibilità di avere un repository remoto


interno all'organizzazione
Qualora si decida di utilizzare Maven, è importante porre attenzione a come organizzare i vari
repository. In primo luogo, come visto nell'apposito paragrafo, i repository più famosi soffrono
di grandi problemi di performance. Pertanto è fortemente consigliato alle organizzazioni, so-
prattutto di medio-grandi dimensioni, di gestire repository "remoti interni" ad uso e consumo
dei propri team di sviluppo. Ciò fornisce un grande aiuto per sopperire alle inefficienze dei
repository globali e dei relativi mirror.

9.6 Distinguere i processi build e di personalizzazione


L'implementazione di sistemi non banali richiede la presenza di una serie di parametri memo-
rizzati in appositi file di configurazione incapsulati nella distribuzione. Una pratica spesso uti-
lizzata consiste nell'impostare questi valori direttamente durante il processo di build. Sebbene
ciò offra il grande vantaggio di eliminare un passaggio e di semplificare il processo, allo stesso
tempo genera tutta una serie di problemi, come quello di avere dei manufatti (file jar, war, ear)
customizzati per uno specifico ambiente e non facilmente riutilizzabili per altri. Ciò crea pro-
blemi non solo in presenza di diversi ambienti, come per esempio IST, QA e UAT, ma anche
qualora sia necessario disporre di diverse istanze dello stesso sistema.
JavaDoc
Introduzione
Questa appendice è dedicata alla presentazione dell'utility Java per la produzione automatica
della documentazione: JavaDoc e il relativo insieme di etichette (Tag). Per maggiori informa-
zioni, consultare [WRJDOC] e [JDOCHP].

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.

' Ttiis ¡< t l H c i i n i n i n n base class o l ali Java >angu.iqe e n i i m c i a t i o n lypcs

' ' « a n t i m i Josli Bloch


' « a u l h o r Neal Gafter
' « v e r s i o n 1 12 06 08 04
' ^silice 1.5

public abstract class Enum<E extends E n u m < E » i m p l e m e n l s Comparable<E>, Serializable I

I commenti doc sono di tue tipi:

• 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:

/ • * @deprecated As of release 1.3, replaced by l@link #getPreferredSize()l 7

@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:

' 4 p a r a m <E> Type of element slored in a list

public interface List<E> extends Collection<E> (


I

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.

Tag Eltetto nella documentazione


© s e e javaJang.String Siring

© s e e java.lang.String The String class The Strina class

@see String String

@see String#equals(Objecl) Strino.eauals(Obiect)

© s e e String#equals Strino.eaualstrava.lana Obiectl

@see java.lang.Objecl#wait(long) java.lana. Object, wait(lona)

@see C h a r a c l e r # M A X _ R A D I X Character.MAX RADIX

© s e e <a hrel='spec.hlml">Java Spec</a> Java Spec

@see "The Java Programming Language" 'The Java P r o g r a m m i n g Language'

Tabella A.l - Esempi di utilizzo del tag @see.


@serialField
Utilizzo: (@serialField <nome_campo> <tipo_campo> <descrizione_campo>)
J D K : 1.2
Questo tag è utilizzato per documentare ogni ObjectStreamField (java.io) incluso nel relativo
array (serialPersistentFields) utilizzato per descrivere, ed eventualmente ridefinire, il defualt dei
campi serializzabili, incapsulati in una classe, ovviamente, serializzabile. Questo array può es-
sere reperito utilizzando la classe ObjectStreamClass.
Per esempio, la classe java.math.Biglnteger definisce i seguenti campi serializzabili come si vede
dal listato 2 che mostra la dichiarazione dell'array ObjectStreamField nella classe java. math. BigDecimal.

r"
' Serializable fields for Biglnteger.

" @serialField s i g n u m int s i g n u m of this Biglnteger.


" tì'serialField magnitude int[] m a g n i t u d e array of this Biglnteger.
' ©serialField bitCount int n u m b e r of bits in this Biglnteger
' rfserialField bitLength int the n u m b e r of bits in the m i n i m a l t w o ' s - c o m p l e m e n t
representation of this Biglnteger
' WserialField lowestSetBit int lowest set bit in the t w o s c o m p l e m e n t representation

private static final ObjectStreamField!] serialPersistentFields = I


new ObjectStreamFieldfsignum", Integer.TYPE),
new ObjectStreamFieldfmagnitude", byte[J.class),
new ObjectStreamFieldfbitCount", Integer.TYPE),
new ObjectStreamFieldfbitLength", Integer.TYPE),
new ObjectStreamFieldffirstNonzeroByteNum", Integer.TYPE),
new ObjectStreamFieldflowestSetBit", Integer.TYPE)
I;

@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.

&gt
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:

' ì h c t r a n x l a l i o n n ' s n l l s in nn offset of & l t : ? . 0 -2 O&gt:.

&lt
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:

' Ti«' I r a n s i a i i o n is-siilK in .in offset of & l t : 2 . 0 . -2 O&gt:

&nbsp
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.

* < p > < b > N o t e : It is rarely a p p r o p r i a t e to use t h i s c o n s t r u c t o r .


" U n l e s s a < i > n e w < / i > Instance Is required, the static f a c t o r y
* K<?link # v a l u e O f ( b o o l e a n ) l is generally a better c h o i c e . It is
* likely to yield s i g n i f i c a n t l y better s p a c e a n d t i m e p e r f o r m a n c e . < / b >

<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:

' A t h r e a d state. A t h r e a d can be in one of the f o l l o w i n g states:


" <ul>
* <li>|@link #l\IEW|<br>
' A t h r e a d t h a t has not yet started Is in t h i s state.

<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:

" User interfaces a n d o p e r a t i n g s y s t e m s use s y s t e m - d e p e n d e n t < e m > p a l h n a m e


' s t r i n g s < / e m > to n a m e tiles a n d d i r e c t o r i e s . This class p r e s e n t s an
' abstract, s y s t e m - i n d e p e n d e n t v i e w of hierarchical p a t h n a m e s . A n
* < e m > a b s t r a c t p a t h n a m e < / e m > has t w o c o m p o n e n t s :

<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:

" <h3>Memory Synchronization</li3>


' <p>AII < c o d e > L o c k < / c o d e > i m p l e m e n t a t i o n s < e m > m u s t < / e m > e n f o r c e the
' s a m e m e m o r y s y n c h r o n i z a t i o n s e m a n t i c s as p r o v i d e d by the b u i l t - i n
' m o n i t o r lock:

<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
&nbsp:.&nbsp;.&nbsp:.
* </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:

' <p> For a given a b s t r a c t p a t h n a m e < i > f < / i > it is g u a r a n t e e d that

<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:

' Returns a s y n c h r o n i z e d ( t h r e a d - s a f e ) c o l l e c t i o n b a c k e d by the s p e c i f i e d


c o l l e c t i o n In o r d e r to g u a r a n t e e serial access, it is critical that
' < s t r o n g > a l l < / s f r o n g > a c c e s s to the b a c k i n g c o l l e c t i o n is a c c o m p l i s h e d
' t h r o u g h the r e t u r n e d c o l l e c t i o n . < p >

<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.).

' Creates a n e w <tt>File</tt> Instance by c o n v e r t i n g the given


* <tt>file:</tt> URI into an abstract p a t h n a m e .

<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

h(x) -» N per ogni XG U

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:

h: U 10,1,2, ..., m-11

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

h(x) = ^ 31 * h(x) + x.char(i)


¡=o ('1>

dove h(x) (0) =0

Non a caso il verbo hash significa "sminuzzare" e, per estensione, "pasticciare", "creare
contusione". Ed ecco Implementazione della funzione hash nella classe String.

public int hashCodeQ {


// l'attributo hash è utilizzato per effettuare II cash del valore hash
int h = hash;

it (h == 0) I
int off = offset;
char val[] = value;
int len = count;

for (int i = 0; i < len; i++) {


h = 31 * h + val[off++];

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:

(e.hash == f.hash ) & & ( !e.key.equals(f.key) )

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);

Gestione interna alla tabella


L'idea alla base di questa politica di gestione delle collisioni è molto semplice e consiste nell'uti-
lizzare uno stesso vettore, di dimensioni finite, per memorizzare tutti gli elementi, indipenden-
temente dal fatto che generino o meno conflitti. Pertanto, qualora l'indirizzo assegnato a un
elemento (detto posizione home, casa) sia già occupato, ossia si abbia un conflitto, l'algoritmo
di hash tenta di individuare una posizione libera tra quelle successive alla posizione inizialmen-
te assegnata (home). Questa strategia, pertanto, non richiede l'utilizzo di apposite strutture
esterne per memorizzare elementi che hanno dato luogo a un conflitto: si ha un solo array di
dimensioni finite.
Un algoritmo molto utilizzato per determinare la posizione successiva disponibile (detto
comunemente linear prohing, "esplorazione lineare") consiste nell'aggiungere un fattore fisso
(tipicamente 1) all'indirizzo inizialmente assegnato.
L'algoritmo utilizzato è riportato nel listato che vediamo di seguito, con lo pseudocodice
dell'inserimento di un elemento in una tabella hash le cui collisioni sono gestite internamente.

1. genera valore hash h (posizione home);


2. accedi alla posizione home. v[h]\
3. assegna i = 0,
4. finché la posizione non è libera (v[h+i'] == nuli} e non si è giunti alla fine del vettore (h+i < m);
4.1. verifica che l'elemento non sia uguale a quello di inserire ! v[h+1].equals(x);
4.2. se è uguale allora termina

Francesco
Roberto

> Hash function

Chiave x = Luca Antonio


Luca

)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.

Gestione esterna alla tabella


La gestione esterna, come suggerisce il nome, prevede che la struttura dati utilizzata per me-
morizzare gli elementi non sia più un semplice vettore, bensì una lista di liste in cui ogni ele-
mento della lista principale contiene il riferimento in memoria ad una lista di elementi accomu-
nati dallo stesso valore hash della chiave (queste liste sono tipicamente denominate liste dei

\ „ ^ \
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

• {transient} table: Enlryf]


«interlace» count: int
• threshold: int
Cloneable
loadFactor: float
• {transient) modCount : int
• {transient,volatile) keySet Sel<K>
«Àtteri a c e »
• {transient,volatile) entrySet Set<Map.Enlry<K,V>>
Serializable
- {transient,volatile) values Collection<V>

h Hasblablei)
i- Hashtable(initialCapacity: int, loadFactor: float)
«tfilerface» [ K,V
k Hashtable(initialCapacity: int)
Map L"

£ T i- Hashtable(Map<? extends K, ? extends V> l)


h {synchronized) sizeQ : int
h {synchronized) isEmptyO : boolean
i- {synchronized) keys(): Enumeration<K>
h {synchronized) elements(): Enumeration<V>
k {synchronized) contains(value: Object): boolean
i- containsValue(vatue : Object): boolean
K,V i- {synchronized) containsKey(key: Object): boolean
«interface»
h (synchronized) get(key: Object): V
Entry
1 t rehash()
k (synchronized) put(key: K, value : V ) : V
h (synchronized) remove(Object key): V
v (synchronized) putAII(Map<? extends K, ? extends V> t)
h (synchronized) clear()
h (synchronized) clone(): Object
h (synchronized) toStringO: Siring
h keySet(): Set<K>

t;

0 1 J, 1 ;
T
K,V
Enlry KeySet ValueColleclion Enumerator
1
- hash : ini
• key : K
- value : V EntrySet Empty Enumerator Emptylterator

Figura C.4 - Diagramma delle classi della classe java.util.Hashtable.

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:

1 public synchronized V put(K key, V value) I


2 // Make sure the value is n o i nuli
3 it (value == nuli) I
4 throw n e w NullPointerException();
5

6 / ' Makes sure the key is not already in the hashtable.


Entry tab[] = table;
8 int hash = key.hashCodef);
9 int index = (hash & 0x7FFFFFFF) % tab.length;
10 lor (Entry<K,V> e = t a b [ i n d e x ] ; e 1= n u l l ; e = e.next) I
11 if ( ( e . h a s h == hash) & & e.key.equals(key)) {
12 V old = e.value;
13 e.value = value;
14 return old;
15
16

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

24 // Creates the new entry.


25 Entry<K,V> e = tab[index];
26 tab[index] = new Entry<K,V>(hash, key, value, e);
27 count++;
28 RETURN NULL;

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.

public synchronized V get(0bject key) I


Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

for (Entry<K,V> e = tab[index]; e != n u l l ; e = e.next) I


if ( ( e . h a s h == hash) & & e.key.equals(key)) I
return e.value;

return null;

Ed ecco gli attributi della classe Entry.

private static class Entry<K,V> i m p l e m e n t s Map.Entry<K,V> I


int hash;
K key;
V value:
Entry<K,V> next;

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.

Funzioni hash e numeri primi


Nell'analizzare la realizzazione di una serie di funzioni hash è possibile notare la presenza di
numeri quali: ..., 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, ... e soprattutto 31 e 37 utilizzati come
fattori moltiplicativi. Questi sono particolari numeri primi. Per quanto non ci sia una prova
matematica certa in grado di dimostrare l'efficacia dei numeri primi nella generazione (calcolo)
di numeri psudorandomici, studi empirici mostrano che il loro utilizzo permette di generare
migliori distribuzioni di numeri randomici.
Pertanto utilizzare numeri primi per la realizzazione delle funzioni hash permette di raggiun-
gere migliori risultati. 1 numeri primi selezionati, però, non devono essere eccessivamente gran-
di, altrimenti si corre il rischio che moltiplicando i vari numeri si finisca con l'azzerare i bit
meno significativi del valore hash calcolato. Questi bit giocano un ruolo molto importanti per
via dell'operazione di modulo necessaria per normalizzare il valore intero calcolato alla dimen-
sione òeWhash tahle.

Alcuni esempi di algoritmi di hash


Saranno adesso presentati alcuni algoritmi hash particolarmente interessanti. L'idea non è quella
di fornire una panoramica esaustiva degli algoritmi di hash esistenti, ma di presentarne alcuni
particolarmente interessanti utilizzati per le strutture dati. Questi, oltre a fornire un riscontro
pratico alla teoria presentata sin qui, dovrebbero offrire interessanti spunti per ulteriori appro-
fondimenti demandati però al lettore.
Per semplicità di trattazione, si è deciso di considerare esclusivamente funzioni hash che
operano sul dominio delle stringhe ( hash(string) ). La loro estensione agli altri tipi di dati è
immediata.

Caratteristiche di una buona funzione hash


Prima di procedere con la disamina dell'insieme di funzioni hash selezionato, è importante
ricordare brevemente quali siano le caratteristiche di una buona funzione hash.

Capacità di evitare collisioni


Sebbene questa possa sembrare una caratteristica più teorica che pratica, e quindi non facil-
mente raggiungibile a meno di specifiche informazioni circa il dominio delle chiavi, non è infre-
quente il caso in cui sia possibile evidenziare, da una semplice ispezione, funzioni disegnate
non accuratamente e quindi destinate a generare un gran numero di conflitti.

Capacità di generare dei valori uniformemente distribuiti nel sottoinsieme


dei numeri naturali considerato (dimensione dell'array)
Indicando con p/ la probabilità che h(x) = /,' una funzione hash che distribuisce uniformemente
i valori ha la proprietà che 0 < i < M, p/= 1/M (i valori hash sono uniformemente distribuiti).
Purtroppo, per poter eseguire delle valutazioni sulla distribuzione dei valori di hash, è necessa-
rio conoscere alcune informazioni circa la distribuzione delle chiavi. In assenza di queste infor-
mazioni, si può assumere che le chiavi siano equiprobabili. Indicando con K/ l'insieme delle
chiavi che generano un medesimo valore / : h(Ki) = /, il requisito di distribuire uniformemente i
valori calcolati, implica che |K/| = |K|/|M|, ossia che un ugual numero di chiavi si riferisca a ciascun
elemento dell'array.

Efficienza di calcolo (ridotta complessità)


Si tratta di un'ulteriore, importante caratteristica di una funzione hash. Passiamo ora ad analiz-
zare diversi esempi di algoritmi di hash.

Additive Hash (hash additivo)


Uno dei primissimi algoritmi di hash elaborato è il cosiddetto hash additivo. Il nome è dovuto
alla sua idea di base che prevede semplicemente di addizionare il valore ASCII dei caratteri che
costituiscono la stringa. Del risultato ottenuto viene eseguito il modulo per la dimensione del
vettore, per la quale, come spesso avviene, è consigliabile selezionare un numero primo. Valori
che invece devono essere evitati sono potenze di due. In questo caso l'operazione di modulo
restituisce gli ultimi bit del numero calcolato ignorando completamente i restanti. Per evitare
questo fenomeno, in generale è necessario evitare valori tali che siano divisibili per un valore
del tipo rk ± a, dove a e k sono due numeri, è la radice dell'insieme dei caratteri. Pertanto la
soluzione più semplice consiste nel selezionare un numero primo.

public ini additiveHash(Str¡ng so urce, int size) I

il ( (source == nuli) || (source.length() == 0) ) I


throw new NulIPointExceptionQ;
I
long hash = source.Iength();
for (int ¡=0; i < source.Iength(); i++) I
hash += source.charAt(i);
I

RETURN ( I N T ) (HASH % SIZE);

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.

public ini rotativeHash(String source, int size) I

il ( (source == null) || (source.Iength() == 0) ) (


throw n e w NullPointException();
I
long hash = source.Iength();
lor (int i=0; i < source.Iength(); i++) (

hash = (hash « 4 ) A (hash » 2 8 ) A source.charAt(i);


I

return (int) (Math.abs(hash) % size);


I

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.

public int bernsteinHash(String source, int size) {

il ( (source == null) || (source.Iength() == 0) ) (


throw new NullPointException();
I
long hash = 0;
for (int i=0; i < source.length)); i++) (
hash += hash * 6 5 + source.charAt(i);

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.

public int cbuHash(String source, int size) {

if ( (source == null) || (source.lengthQ == 0) ) I


throw new NullPointException();
I
long hash = 0;

for (ini i=0; i < source.Iength(); i++) I

hash = (hash « 2) + source.charAt(i);

return (int) (Math.abs(hash) % size);


I

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.

public int pkpHash(String source, int size) I

il ( (source == nuli) || (source.Iength() == 0) ) I


throw new NullPointException{)

pTab[] - 1 3 , 1 2 , 1 7 6 , 34, ... // 2 5 6 elementi

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

• generare 8 pTabs di valori casuali, sempre compresi tra 1 e 256;


• consideare 8 hash parziali;
• utilizzare il codice ASCII di ciascun carattere per aggiornare gli 8 hash di cui sopra;
• effettuare il merge degli 8 hash parziali in un unico hash finale.

Ed ecco, nel listato seguente, un esempio di PKP hash esteso.

public class PKPHashFunction I


/ / t h i s table is used to generate the 8 per 2 5 6 p s e u d o - r a n d o m n u m b e r s
private intpTabs[J[J = null;
private static int numBytes = 8;

' Default c o n s t r u c t o r

public PKPHashFunction() (
initPTabs();
I

/ "

' Initialises the table titled with r a n d o m n u m b e r


' Each row contains the n u m b e r t r o n i 1 to 2 5 6 stored in a r a n d o m order
*/
private void initPTabs() I
pTabs = new lnt[numBytes][255];
for (int ind = 0; Ind < pTabs.length; lnd++) {
pTabs[lnd] = generateRandomByteArray(256);

' Generales a p s e u d o - r a n d o m array filled w i t h n u m b e r


' f r o m 1 to size, stored in a r a n d o m order
' feparam size vector d i m e n s i o n
* © r e t u r n int array of size d i m e n s i o n w i t h n u m b e r f o r m 1 to size r a n d o m l y stored

private int[] generateRandomByteArray(int size) I


int[] randomArray = new int[size];
// initialise the array
for (int ind = 0; ind < size; ind++) I
randomArray[ind] = ind;

(
// 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;

" Generates the PKP Hash value


* @)param source String to use for the hash generation
' @ p a r a m size address space size
* © r e t u r n the PKP hash value
7
public int pkpHash(String source, int size) I
if ( ( s o u r c e == null) || (source.length)) == 0 ) ) I
throw n e w NullPointException();
I
int hash[] = new int[numBytes];
for (int i=0; i < source.lengthf); i++) I
for (int j=0; j < numBytes; j++) I
hash[j] = pTabs[j][ h a s h [ j ] A source.charAt(i)];
I
I
// a c c u m u l a t e the different c o n t r i b u t i o n s
int resultHash = 0;
for (int j=0; j < numBytes; j++) (
resultHash += ( hash[j] « (8*j));
)
return (int) (Math.abs(resultHash) % size);
Come si può notare, questo algoritmo presenta qualche problema di complessità. In partico-
lare è necessario allocare 8 tabelle di 256 caratteri e quindi eseguire ogni passo 8 volte. Comun-
que, i risultati ottenuti sono del tutto apprezzabili.

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 con nominativi celebri (228 elementi)


private static String e l e m e n t N a m e s [ ] = (
"Curtís Jackson", "Vincent Furnier", "Alizée Jocotet", "Anastacia Newkirk", "Sasmi Anggun Cipta", " J i m m y
McShane", "Barry Pinkus", "Hansen Beck", "William Michael Albert Broad", "Bjòrk Gudmundsdottir", "Robert
Z i m m e r m a n n " , "Paul Hewson", "George Alan O ' D o w d " , "Steven Demetre Georgiou", "Charles Aznavourian",
"Cherilyn Sarkasian La Pierre", "Francisco Repilando", "Olga De Souza", "Deadrie Crozie", "David Robert Hayward
Jones", "Dino Paul Crocetti", "Dido Armstrong", " R a y m o n d Donnez", "Adrian Donna Gaines", "Andre Young",
"Edith Giovanna Gassion", "Reginald Kenneth Dwight", "Declan Patrick M c M a n u s " , "Marshall Bruce Mathers III",
"Eithne Ni Bhraonain", "Derek William Dick", "Michael Balzary", " M a m m o l a Sandon", "Frank Sinatra", "Farookh
Pluto Bulsara", "Luis Romero", "Georgios Kyriacos Panayiotou", "Geraldine Estelle Halliwell", "José Angel Hevia
Velasco", "O'Shea Jackson", "James Newell Osterberg", "John Robert Cocker", "John Francis Bongiovi", "Jean
Philippe Smet", "Henry W a y n e Casey", "Keith Richards", "Robert J a m e s Ritchie", "Christopher Hamill", "Richard
Wayne Penniman", "Steve Van Zandt", "Lewis Firbank Reed", "Louise Veronica Ciccone", "Oscar Tramor", "Maria
Sofia Kalogeropoulos", "Brian Warner", "Melanie Brown", "Melanie Jayne Chrisholm", "Michael Bolotin", "Miguel
Dominguin", "Richard Melville Hall", "Achinoam Nini", "Noelia Lorenzo M o n g e " , "Winston Hubert Mclntosh",
"Alicia Moore", "Roger Jouret", "Prince Roger Nelson", "Sean Combs", "Richard Bresch", "Enrique Martin
Morales", "George Albert Tabett", "Richard Starkey", "Helen Folasade Adu", "Terence Trent D'Arby", "Sandra Ann
Goodrich", "Sandra Lauer Cretu", "Orville Richard Burrell", "Shakira Ripoli Mebarak", " N o r m a n David Shapiro",
"Deborah Ann Dyer", "Steven Tallarico", "Steveland Hardaway Judkins", "Gordon M a t t h e w Sumner", "David
Evans", "Anna M a e Bullock", " T h o m a s Beecher", " T h o m a s Jones W o o d w a r d " , "Antonio De La Cuesta", "Tracy
Louise Freeman", "George Ivan Morrison", "Wesley Johnson", "Ivo Livi", "Salvatore Adamo", "Albano Carrisi",
"Aleandro Civai", "Alessia Aquilani", "Carla Bissi", "Roberta Mogliotti", "Andrea Fumagalli", "Anna Hoxha",
"Antonio Muraccioli", "Nicola Balestri", "Roberto Gatti", "Domenico Di Graci", "Antonio Calò", "Mariano Rapetti",
"Carla Quadraccia", "Davide Civaschi", "Carlo Marchino", "Alfredo Rapetti", "Cristiano Rossi", "Claudia Moroni",
"Claudio Pica", "Yolande Christina Giglioni", "Stefano Zanchi", "Eugenio Zambelli", "Vito Luca P e r i n i " , "Donato
Battaglia", "Aldo Caponi", "Giuliano llliani", "Giampiero Anelli", "Stefano Belisari", "Elisa Toffoli", "Vincenzo
Jannaccl", " T o m m a s o Zanello", "Nicola Fasani", "Paolo Panigada", "Marina Fiordaliso", "Francesco Di Gesù",
"Roberto Antoni", "Alfredo Buongusto", "Gabriele Ponte", "Renato Abate", "Luigi Giovanni Maria Panceri", "Paul
Mazzolini", "Giampiero Scalamogna", "Federico Renzulli", "Dante Luca", "Giorgia Todrani", "Giorgio Gaberschick",
"Giovanna Nocetti", "Giuseppina Romeo", "Federico Monti Arduini", "Carmela lannetti", "Ivan Lenin Grazlani",
"Alessandro Aleotti", "Gianfranco Randone", "Giovanna Bersola", "Enrico Sbricolli", "Giovanna Coletti", "Edoardo
Bennato", "Giorgio Guidi", "Lorenzo Cherubini", " M a s s i m o Jovine", "Marco Messina", "Lorena Biolcati", "Annalisa
Panetta", "Antonio Ciacci", "Paul Bradley Couling", "Giuseppe M a n g o " , " M a r c o Armenise", "Marina Restuccia",
"Mario Macciocco", "Giovanni Calone", "Maurizio Lobina", "Maria DI Donna", "Marcello M o d u g n o " , "Domenica
Berte", "Gian Michele Maisano", "Daniela Miglietta", "Christian Meyer", "Maria Uva Biolcati", "Anna Maria Mazzini",
"Beniamino Reitano", "Mirella Fregni", "Giulio Rapetti", " U m b e r t o Giardini", "Monica Bragato", "Marco Castoldi",
"Nada Malanima", "Natale Codognotto", "Giovanni Pellino", "Angela Cacciola", "Filippo Neviani", "Nico Tirone",
"Domenico Colarossi", "Michele S c o m m e g n a " , "Adionilla Pizzi", "Agostino Ferrari", "Ombretta Comelli", Orietta
Galimberti", "Pierpaolo Pelandi", "Paola lezzi", "Chiara lezzi", "Nicoletta Strambelli", "Giuseppe Faiella", "Barbara
D'Alessandro", "Enzo Ghinazzi", "Raffaele Riefoli", "Bruno Canzian", "Renato Brioschi", "Renato Ranucci", "Renato
Fiacchini", "Riccardo Sanna", "Roberto Concina", "Roberto Loreti", "Roberto Castiglione", "Camillo Facchinetti",
"Sergio Conforti", "Rosalino Cellamare", "Fabio Staccotti", "Giovanni Scialpi", "Gaetano Scardicchio", "Ivana
Spagna", "Stefano Rossi", "Susanna Quattrocchi", "Cecilia Cipressi", "Giovanni Giacobetti", "Ferruccio Ricordi",
"Antonio Lardera", "Elio Cesari", "Tiziana Donati", "Salvatore Cutugno", "Guido Lamberti", "Monica Zucchi",
"Virginia Minetti", "Giuditta Guizzetti", "Adelmo Fornaciari", "Luca Persico" I;

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 " |;

Vettore con un insieme di codici di compagnie aeree (198 elementi)


private static String elementsAirlineCodes[] = I
"JP", "AEA", "RE", "El", " W " , "SU", "AR", "4L", "EUK", "BT", "AB", "AC", "CA", "AF", "Al", "JM", "UL", "KM",
"MK", "SW", "NZ", "A7", "AIRS", "GRE", " H M " , " W O W " , "TS", "JY", "PB", "6G", " U M " , "AZ", "NH", "4J", "AA",
"OZ", "5W", "AEU", "RC", "GR", "AO", "OS", "J2", "BD", "LZ", "PG", "B2", "BG", " W W " , "BA", "BNW", "BW",
"8B", "ZQ", "CX", "KX", "CO", " M U " , "C9", "CF", "WX", "X9", "CO", "SS", "OU", "OK", "CT2", "CU", "CY", " D W T " ,
"DL", "T3", "U2", "EZ", "DS", " M S " , "LY", "JEM", "EK", "EOS", "OV", "ET", "EY", "VE", "EUR", " 3 W " , "EA", "BR",
"JN", "AY", "DP", "BE", "GC", "GA", "4U", "B4", "JN2", "GF", "ZU", "T4", "2L", "8H", "X3", "IB", "TY", "FI", "IC",
"IR", "JL", " 9 W " , "LS", "JFK", "KQ", "YK", "KL", "KE", "KU", "LI", "LN", "TE", "LO", "LH", "LG", " D M " , " W 5 " ,
" M H " , "MA", " M Y " , "IG", "ME", "ZB2", "ZB", " M Y T " , "VZ", "CE", "HG", " N W " , "DY", "OL", "OA", "WY", "OHY",
"PK", "PMA", "9R", "NI", "QF", "OR", "ROC", "AT", "Bl", "RJ", "FR", "S4", "SV", "SK", "SKY", "CB", " M I " , "SQ",
"5P", "RU", "NE", "JZ", "ALXX", "OS", "SN", "SA", "JK", "SD", "WV", "LX", "RB", "DT", "TP", "RO", "TG", "MT",
"TOM", "BY", "HV", "5A", "TU", "TK", "T5", "T7", "UA", "US", "HY", "RG", "VIK", "VS", "VG", "WF", " W M " , " W 6 " ,
"IY", "Z4" |;

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

• considerare i primi 40 elementi dell'array dei nomi e studiarne la distribuzione su una


tabella di 43 elementi (tabella C.4 e figura C.5);
• considerare i primi 40 elementi dell'array dei codici dei vettori aerei e di studiarne la
distribuzione su una tabella di 89 elementi (Figura C.5).

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

servizio (daemon). Questi sono particolari T c h e o p e r a n o in b a c k g r o u n d , rispetto al n o s t r o

p r o g r a m m a , e sono necessari p e r s u p p o r t a r e l'ambiente di esecuzione (run-time). Il classico

esempio è il (ìarbage Collector (T incaricato di liberare lo spazio heap o c c u p a t o da oggetti non

più referenziati). Questi T finiscono per c o n c o r r e r e con il nostro p r o g r a m m a per aggiudicarsi

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

presenza è del tutto trasparente. I T daemon non influenzano la terminazione di un programma

Java: infatti questo conclude la propria esecuzione quando tutti i thread non-dacmon finiscono la

propria esecuzione. In tale evenienza, la J V M si fa carico di b l o c c a r e tutti i thread daemon e

quindi esegue lo shut-down.

Per comprendere appieno la nozione di thread è necessario introdurre un altro concetto:


quello di "processo". I T sono spesso indicati con il termine di light-weight processes (LWPs,
"processi leggeri"), giacché presentano forti analogie con i processi che operano a livello di
sistema operativo (OS, Operative System)-, spesso ed erroneamente vengono confusi proprio
con tali processi, anche se sono decisamente più leggeri.
Un processo essenzialmente è un programma in corso di esecuzione, e tipicamente è costitu-
ito dalle seguenti cinque parti fondamentali:

1. codice;
2. dati;
3. stack;
4. file di I/O;
5. tabelle dei segnali.

I processi, in contrapposizione ai T, sono definiti heavy-weight processes (HWPs, "processi


pesanti); pesanti, in quanto il relativo cambio di contesto (switch context, necessario per rimuo-
vere un processo dallo stato di esecuzione e quindi assegnare la CPU a un altro processo), dà
luogo ad un considerevole overhead. Ciò è dovuto, tra l'altro, alla necessità di salvare tutte le
tabelle del processo uscente e di ripristinare (o creare) quelle del processo entrante. La situa-
zione non migliora granché neanche quando un processo dà luogo a un altro figlio ( f o r k Q )
giacché l'unica parte condivisa è il codice.
I T, d'altro canto sono in grado di ridurre l'overhead poiché possono condividere diverse
parti (figura D.2) e quindi il cambio di contesto può avvenire molto più rapidamente. Inoltre, è
possibile sfruttare i cosiddetti T di livello utente (user-level). Questo modello prevede che i task
si occupino anche di definire le routine necessarie per gestire l'alternarsi dei T allo stato di
esecuzione, intervenendo, quasi esclusivamente, sullo stack pointer e quindi evitando invocazioni
a livello di kernel dello OS. I T a differenza dei processi, condividendo diversi spazi di memoria
(figura D.2) devono preoccuparsi dell'accesso alle variabili comuni.
Da quanto appena scritto, è chiaro che è necessario fare attenzione quando si parla di multi-
processo e MT. Il primo è la caratteristica che permette ai sistemi operativi di eseguire (più o
meno simultaneamente) due o più processi concorrentemente. Per esempio qualora si lanci la
compilazione di un programma Java, mentre contemporaneamente si utilizza un editor di testi
e si ascolta la musica generata da un'applicazione lettore MP3, si ha un esempio di tre processi
in esecuzione concorrente (a dire il vero, c'è sempre da fare i conti anche con i processi del
sistema operativo, relativi all'hardware, etc.). Inoltre, è possibile lanciare diverse volte una stes-
sa applicazione, per esempio un editor di testi, e far sì che ciascuna istanza carichi un diverso
insieme di documenti. Questo è possibile giacché ciascuna istanza dell'editor viene eseguita in
un opportuno processo che, come visto in precedenza, dispone di una propria area di memoria
non condivisa con altri processi. Pertanto, ciascun processo dispone delle proprie aree di me-
moria privata. La logica conseguenza di ciò è che il programmatore non deve preoccuparsi di
problemi relativi all'accesso condiviso alle variabili, e proprio per questo, però, la situazione
diviene più complessa qualora i diversi processi dovessero aver necessità di comunicare, magari

Figura D.2 - Schematizzazione concettuale di programmi multi-threaded.


per sincronizzare la propria esecuzione, o per fornire, a un processo controllore, i risultati della
propria esecuzione. In questo caso la comunicazione tra diversi processi richiede soluzioni
abbastanza complesse che tipicamente prevedono l'utilizzo di meccanismi di networking.
Uno stesso programma può poi essere organizzato in un insieme di T in grado di eseguire
(più o meno simultaneamente) diversi compiti. Per esempio, un editor di testi può utilizzare
diversi T per formattare quanto digitato, controllare la sintassi delle parole immesse, visualizzare
il testo digitato corredato da eventuali correzioni, tutto in "tempo reale".
Spesso la confusione tra processi e thread è dovuta al fatto che, al fine di supportare la
programmazione MT, la JVM è costretta a implementare, internamente, funzionalità tipiche
degli OS, come per esempio lo scheduler, facendo condividere ai vari thread le risorse messe a
disposizione del processo.

Mapping tra thread al livello di programma utente


e i corrispondenti al livello kernel
Q u a n t o esposto di seguito presenta un livello di complessità molto avanzato per via dei molteplici

prerequisiti richiesti. Tuttavia, è possibile continuare nella lettura del presente capitolo anche

s a l t a n d o il p r e s e n t e p a r a g r a f o , p e r q u a n t o una sua lettura p e r m e t t a di c o m p r e n d e r e più

rapidamente molti concetti introdotti successivamente.

Un interessante argomento, che purtroppo non è possibile trattare in dettaglio in questo


contesto ma che vale la pena introdurre, si riferisce al mapping tra T a livello di programma
utente e i corrispondenti al livello kernel.
Le alternative per il mapping tra thread al livello di applicazione e i corrispondenti al livello
kernel sono tre e sono caratterizzate dalla cardinalità di tale mapping (figura D.3):

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 •

kernel kernel kernel

Figura D.3 - Schematizzazione concettuale del mapping tra thread delle applicazioni Java e dei
corrispondenti al livello di kernel.

Aree di memoria gestite dalla JVM e metodi rientranti


La JVM, come illustrato in precedenza (figura D.2), gestisce diverse aree di memoria, rispettiva-
mente:

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.

public static void shuffle(List<?> list, Random rnd) I


int size = list.sizef);
if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) (
for (int i=size; i>1; i — )
swap(list, i-1, rnd.nextlnt(i));
) else I
Object arr[] = list.toArray();

/,/ Shuttle array


(or (int i=size; ¡>1; i — )
swap(arr, i-1, rnd.nextlnt(i));

// D u m p array back into list


Listlterator it = list. listlterator();
for (int i=0; karr.length; i++) I
it.next();
it.set(arr[i]);

public static void swap(List<?> list, int i, int j) {


final List I = list;
l.setfi, l.set(j, l.get(i)));
I

private static void swap(Object[] arr, int i, int j) I


Object t m p = arr[i];
arr[i] =arr[j];
arr[j] = tmp;

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.

Modello MT della JVM


Poiché la programmazione multithreading, come suggerisce il nome, è una tecnica di disegno
e programmazione, ne segue che l'implementazione di applicazioni MT presenta forti
interdipendenze con il linguaggio di programmazione selezionato.
Una prima importante limitazione da tener presente quando si progettano programmi MT in
Java è che, in questo caso, Java non è completamente indipendente dalla piattaforma di esecu-
zione. In altre parole, nel caso della programmazione MT, il linguaggio Java, così come tutti gli
altri eseguibili dalla JVM, non sono completamente platform independent. Chiaramente, questa
caratteristica fondamentale è ancora ottenibile, a spese però di un disegno che contempli, in
maniera ridondante, i modelli di funzionamento delle differenti piattaforme (platform aware).
Il problema nodale è che la programmazione MT presenta dipendenze strutturali dal sistema
operativo, le quali possono essere minimizzate ma, non eliminate completamente.
Prima di procedere oltre, è necessario ricapitolare concisamente alcuni concetti propedeutici
al MT. In primo luogo è necessario comprendere chiaramente la differenza tra parallelismo e
concorrenza: nozioni troppo frequentemente fraintese. Il concetto di parallelismo è abbastanza
intuitivo: due o più T sono eseguiti in parallelo quando la relativa computazione avviene effet-
tivamente simultaneamente (figura D.4). Il parallelismo (comunque sempre parziale) potrebbe
essere raggiunto in presenza di architetture hardware multi-processore dotate di sistemi opera-
tivi in grado di tranne pieno vantaggio. Da tener presente che server multi-processore sono
situazione normalissima in ambienti professionali. Per esempio, la macchina Sun Narnia dispo-
ne di 32 CPU. Tuttavia, la situazione tipica, invece, consiste oggi nel disporre di architetture
con un paio di CPU, eventualmente corredate da processori dedicati alla gestione di servizi ben
definiti (operazioni di I/O, gestione della grafica, etc.), in cui le CPU sono ripartite temporane-
amente tra più T secondo opportune politiche. In questo caso quindi, il parallelismo è apparen-
te, simulato. La concorrenza si ottiene facendo sì che l'esecuzione di ciascun T sia divisa in
sequenze di frazioni più piccole che si alternano, con parti di altri T e processi, nell'assegnazio-
ne della CPU. Chiaramente, la suddivisione dei T è solo logica (non si hanno parti fisiche di
dimensione prefissata) ma intervalli temporali di esecuzione che quindi, logicamente, finiscono
per frammentare i T in parti più piccole. Un frammento pertanto è dato dalla sequenza di
istruzioni eseguite dalla CPU durante l'intervallo di tempo assegnato al relativo T.
Una prima considerazione riguarda il fatto che applicazioni MT, tipicamente eseguite in
maniera concorrente, richiedono un maggiore intervento dello OS e dello scheduler della JVM.
Inoltre, presentano problematiche completamente nuove, come per esempio: sincronizzazio-
ne, protezione di aree ad accesso condiviso e scambio di dati tra diversi T. Pertanto, potenzial-
mente, applicazioni MT potrebbero richiedere un tempo complessivo di esecuzione superiore
rispetto a quello richiesto dalle equivalenti implementazioni tradizionali (single-threaded). In
realtà, se le applicazioni MT sono ben disegnate, questo scenario non è frequente. In effetti,
l'esecuzione della quasi totalità delle applicazioni presentano un elevato numero di tempi di
"attesa", che un programma MT ben disegnato è in grado di sfruttare. Ad esempio, ogniqualvolta
un T in stato esecuzione esegue un'operazione di I/O, questo viene immediatamente forzato a
rilasciare la CPU ed a transitare nello stato di attesa (figura D.7). Questa tecnica permette di
sfruttare al meglio la preziosa risorsa CPU: le operazioni di I/O, infatti, richiedono tempi di
esecuzione decisamente superiori a quelli della CPU poiché, tipicamente, coinvolgono opera-
zioni meccaniche, come letture dal disco rigido, che ovviamente sono relativamente lente. Il
coordinamento di tali attività, nelle architettura moderne, è assegnato ad appositi processori. E
opportuno quindi, che T richiedenti operazioni di I/O attendano che queste siano completate
rilasciando la CPU che, invece di attendere inutilmente, può essere assegnata ad altre attività.
Lo sfruttamento di queste fasi "morte" consente alle applicazioni MT di fornire tempi di esecu-
zione inferiori, anche di molto, rispetto ai corrispondenti single-threaded.

\
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

Figura D.4 - Rappresentazione grafica concettuale delle nozioni di parallelismo e concorrenza. Il


grafico di sinistra mostra una serie diT eseguiti simultaneamente e quindi in parallelo. Il grafico di
destra invece mostra diversi T che concorrono, in modo completamente indeterministico, per
l'acquisizione della CPU. Questi diagrammi sono notevolmente semplificati. Trascurano una serie
di fattori, come per esempio il continuo intervento del OS/'Scheduler, l'accesso a risorse di I/O, etc.
Le ragioni del MT non si limitano alla massimizzazione del throughput, ma vanno ricercate
anche in altri fattori. Alcuni problemi sono intrinsecamente MT ma è più naturale pensare
alcune applicazioni in termini di T separati (si considerino, per esempio, la gestione delle con-
nessioni a servizi offerti in Internet); inoltre sempre più frequentemente i sistemi moderni,
anche a basso costo, dispongono di molte CPU.
Un reale parallelismo, come accennato in precedenza, è ottenibile solo in rarissimi casi, men-
tre lo scenario più tipico è quello di un insieme di n T che concorrono per poter essere eseguiti
su m CPU, dove n » m. A questo punto le politiche di gestione della concorrenza (modelli
MT) giocano un ruolo fondamentale e la dipendenza dei linguaggi di programmazione dai
sottostanti OS diviene evidente. In particolare, esistono due grandi tipologie di modelli MT,
ereditati dal mondo del multiprocesso, rispettivamente: cooperativo e preventivo (preemptive).
Il modello cooperativo prevede che un T mantenga il controllo della CPU (rimanga in esecu-
zione) fino al momento in cui, esplicitamente, decida di rilasciarne il possesso. Cosa che po-
trebbe anche non avvenire mai. Ciò determina, nella migliore delle ipotesi, il degradamento
dell'applicazione a mono-tbreaded (tutti i T, eccetto quello in esecuzione, starve, "soffrono la
fame", non avendo mai l'opportunità di essere eseguiti) o, caso peggiore, la transizione dell'ap-
plicazione in uno stato di dead-lock.
Nel modello cooperativo, pertanto, la concorrenza è (quasi) completamente demandata all'ap-
plicazione, quindi al programmatore, con i ben noti prò e contro. Questo modello, assumendo
che le applicazioni MT siano perfettamente disegnate e implementate, offre il grande vantaggio
delle performance: il programmatore dichiara esplicitamente quale siano i momenti più opportu-
ni in cui un T debba cedere il controllo, inoltre, potenzialmente, il passaggio del controllo tra
diversi T, può avvenire al livello di user-mode subroutine, evitando il coinvolgimento di servizi
kernel del OS, i quali possono richiedere fino a diverse centinaia di cicli macchina! Un altro
grande vantaggio è dato dalla semplicità del modello: non ci si deve preoccupare di problemi
come l'atomicità delle operazioni (operazioni la cui esecuzione non può essere interrotta), sincro-
nizzazioni, del fatto che un T possa essere interrotto in qualsiasi punto, e così via. Questa sempli-
cità però genera anche degli inconvenienti, come per esempio la strategia in cui suddividere T
complessi e/o pesanti in parti più piccole è completamente demandata al programmatore.
Oggetti Java di tipo Thread, in stato di esecuzione, possono richiedere esplicitamente di tran-
sitare nello stato di pausa, rilasciando quindi la CPU, invocando direttamente il metodo yield()
(metodo della classe java.lang.Thread).
Una volta rilasciata la CPU questa è disponibile per essere assegnata ad altri T, permettendo-
gli di entrare in esecuzione. La strategia utilizzata dallo scheduler per selezionare il successivo T
da mandare in esecuzione, tipicamente, è basato sulla priorità: il T a più alta priorità, tra quelli
in stato di attesa, è selezionato per essere mandato in esecuzione. Come vedremo a breve, anche
questo è un problema per l'indipendenza del linguaggio Java dalla piattaforma di esecuzione.
Il modello alternativo, largamente utilizzato, è il cosiddetto preventivo o a partizione di
tempo (time-slice). In questo caso la CPU è assegnata ai T per intervalli di tempo {slice) ben
definiti, al termine dei quali, il T è forzato a rilasciare la CPU. In questo caso è 1' OS e lo
scheduler (non c'è modo che ciò possa avvenire senza il relativo supporto) che si fa interamente
carico di gestire la concorrenza (non a caso le applicazioni tendono ad una maggiore affidabilità)
e quindi l'applicazione ha minori possibilità di sfruttare al massimo il MT. Un grande vantag-
gio, tuttavia, è che solo questo modello, in condizioni ottimali, rende possibile il parallelismo: è
la coppia OS/Scheduler che si occupa di assegnare e rimuovere i T e non regole "cementate"
nel codice stesso. Disegnare applicazioni MT per questo modello è un po' più complesso, in
quanto, per esempio, è necessario prevedere esplicite sincronizzazioni, disegnare il programma
pensando che un T possa essere interrotto in pressoché qualsiasi punto (Java definisce pochis-
sime operazioni atomiche).
A questo punto i lettori potrebbero interrogarsi su quale sia il modello MT adottato da Java.
La risposta è che il linguaggio Java non prevede alcun modello MT di riferimento; questo è
"ereditato" dalla piattaforma di esecuzione. Ciò si traduce in un po' di mal di testa per i pro-
grammatori; in effetti, se si desidera scrivere programmi MT in grado di essere eseguiti su
qualsiasi piattaforma e quindi per qualsiasi modello MT, questi devono contenere i meccanismi
di funzionamento di entrambi i modelli (sincronizzazione e cessione, yield).
A questo punto dovrebbe essere finalmente chiaro il perché il linguaggio Java non sia com-
pletamente platform independent, e che ciò sia ottenibile esclusivamente includendo nel codice
meccanismi MT "ridondanti" per i diversi modelli di MT.
Il disegno di una versione del linguaggio Java completamente indipendente dalla piattafor-
ma, anche nel dominio MT, avrebbe richiesto l'implementazione all'interno della stessa mac-
china virtuale di una serie di servizi tipici dei sistemi operativi che, avrebbero potuto creare
problemi e non trarre il massimo del beneficio dal sottostante hardware. Questo da un lato
avrebbe consentito di raggiungere l'obiettivo prefisso: J V M completamente indipendenti dal
sottostante OS. Dall'altro però avrebbe finito per minare definitivamente la possibilità di dar
luogo a un vero parallelismo, anche in condizioni favorevoli. Questo perché lo OS, probabil-
mente, avrebbe finito per considerare e quindi eseguire, la J V M come un normale programma
sequenziale [single-threaded). Quindi, anche in presenza di architetture multiprocessore, la J V M
sarebbe sempre assegnata a un solo processore. Ovviamente, si sarebbe sempre potuto eseguire
diverse instanze della J V M su diversi processori. Organizzazioni di questo tipo però, non sono
automaticamente MT (è più appropriato parlare di multiprocesso) ed inoltre pongono una
serie di problemi aggiuntivi relativi alla programmazione distribuita (comportamenti client/
server, protocolli distribuiti, etc.) In sintesi, efficaci meccanismi MT si possono ottenere esclu-
sivamente facendo sì che thread Java siano associati a diversi thread dello OS. In passato ciò
non è sempre avvenuto creando molteplici problemi, soprattutto al livello di performance.
Un altro problema legato alla dipendenza di Java dallo OS è relativo alla gestione delle priorità
degli oggetti di tipo Thread. Il linguaggio Java definisce dieci diversi livelli di priorità: la classe
java.lang.Thread definisce le seguenti costanti (attributi statici):

• public static final int MAX_PRIORITY = 10;


• public static final int MIN_PRIORITY = 1;
• public static final int N0RM_PRI0RITY = 5

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.

Race conditions (condizioni di competizione)


Si tratta di un problema abbastanza ricorrente e si genera quando due o più T tentano di
aggiornare contemporaneamente una medesima struttura di dati (oggetto) e, che, per via di
un'implementazione non disegnata per un ambiente MT (tecnicamente non thread-safe), o non
accurata, la lascino in uno stato inconsistente, ossia in cui i dati non sono coerenti. Un esempio
classico è quello di due T che aggiornano il valore di una medesimo contatore incapsulato in un
apposito oggetto. Lo scenario tipico, in presenza di un codice non esattamente ben implemen-
tato, prevede che il primo T acquisisca il valore del contatore (per esempio 1003) e contem-
poraneamente o quasi, anche il secondo T legga il medesimo valore (ancora 1003). A questo
punto ciascun T dopo aver provveduto ad incrementare il contatore, memorizzi il nuovo valore
nel contatore originario. Sebbene questo abbia subito due incrementi, il valore finale sarà co-
munque 1004 e pertanto un incremento viene perso. Si immaginino gli effetti di tale situazione
qualora il contatore rappresenti un sedile di aeroplano, un posto al teatro o il prezzo e/o la
disponibilità di un determinato prodotto finanziario.

Deadlock (abbraccio mortale)


Una tecnica spesso utilizzata per evitare problemi come quelli illustrati precedentemente (race
conditions) consiste nel ricorrere ad appositi costrutti atti a far sì che l'accesso a determinate
risorse condivise avvenga in mutua-esclusione. Pertanto, se un T accede ad una di queste risor-
se, gli altri automaticamente sono posti in attesa fintanto che il T stesso non rilasci la risorsa.
Ciò fa sì che in presenza di una serie di strutture dati condivise da parte di più T, il relativo
accesso sia regolato. Una volta che a tali strutture sia stato fatto accesso, automaticamente esse
divengono inaccessibili ad altri T. Qualora poi uno stesso T debba modificare diverse strutture,
può accadere che, per via di un'implementazione non furbissima, questo inizi a bloccare le
varie risorse e ne trovi una o più bloccate da un altro T che, a sua volta, per evolvere abbia
bisogno delle risorse bloccate dal primo T. A questo punto l'abbraccio mortale è generato con
una serie di T che si bloccano vicendevolmente non riuscendo più a evolvere. Sebbene lo
scenario presentato sia abbastanza semplice, in applicazioni complesse è possibile rigenerarlo
in maniera abbastanza subdola, senza rendersene facilmente conto.

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.

Non deterministic behaviour (comportamenti non deterministici)


Con questa definizione ci si riferisce genericamente a una serie di comportamenti inaspettati,
anche molto seri, da parte di un'applicazione MT di cui però non si riesce ad identificarne
l'esatto motivo. Le race conditions sono chiaramente un esempio di comportamento non
deterministico: ad un certo punto una o più variabili presentano dei valori errati. Se si conside-
ra che spesso il valore di determinate variabili influenza il flusso del programma, si comprende
quali conseguenze possano generarsi in applicazioni fortemente MT. Comportamenti non
deterministici possono includere la produzione, in circostanze non chiare e quindi non facil-
mente riproducibili, di risultati inaspettati, calcoli errati, etc. Si pensi agli effetti che un com-
portamento non deterministico può avere in un sistema di calcolo automatico dei prezzi di
offerta di determinate azioni finanziarie, oppure in un sistema di controllo del traffico della
metro, o in quello delle torri di controllo. Chiaramente, un buon disegno e una buona
implementazione costituiscono sempre il modo migliore per evitare problemi.

Programmazione MT Java originaria


Classe Thread e interfaccia Runnable
Tutti coloro che hanno seguito l'evoluzione del linguaggio Java fin dalla sua genesi (fin dalle
prime Applet), ricordano l'enfasi straordinaria che veniva conferita alla programmazione MT.
Ciò si evince da una serie di elementi nativi del linguaggio, quali la presenza di specifiche classi
e interfacce disegnate appositamente per supportare il multithreading e la disponibilità di de-
terminate parole chiave e metodi dedicati alla programmazione MT. Per esempio, affinché i
metodi (delle istanze) di una classe siano eseguiti all'interno di un apposito T dedicato, è suffi-
ciente far sì che la classe implementi l'interfaccia Runnable (e quindi definisca l'apposito metodo
run()), oppure, in alternativa, è possibile estendere direttamente la classe java.lang.Thread. Ciò
rende possibile definire T "paralleli", la cui esecuzione concorre, o compete, in funzione della
piattaforma di produzione, con quella del metodo principale del programma. La JVM, natural-
mente, funziona sempre in modalità MT: oltre all'applicazione utente esistono altri T (daemon),
come per esempio il famoso garbage collector, il gestore della coda degli eventi grafici, etc.
Come primo esempio si considerino i due listati presentati di seguito. In queso, c'è un sem-
plice esempio di applicazione MT. La classe ThreadExample implementa l'interfaccia Runnable.

public class ThreadExample i m p l e m e n t s Runnable {


/ " * used to c o u n t the n u m b e r of executions 7
private long count = 0;

public ThreadExampleO I

public void run() I


count = 0;

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

System.ouf.println("—> thread:" + Thread.currentThread().ge\\6() + " stopped- Count:" + count);


I

public static void main(String[] args) I


ThreadExample[] runnables = I
new ThreadExampleO, new ThreadExampleO, new ThreadExampleO K
Thread[] threads = I
new Thread(runnables[0]), n e w Thread(runnables[1 ]), n e w Thread(runnables[2]) I;

tor (int ind = 0; ind < threads.length; ind++) I


threads[ind].start();

try I
Th read. s/eep(5000) ;
) catch (InterruptedException e) I
e.printStackTracef); // ignore this exception
)

for (int ind = 0; ind < threads.length; ind++) I


threads[ind].interrupt();
Il funzionamento del programma è abbastanza semplice. La classe ThreadExample dispone di
un solo costruttore e del metodo run() (si tratta dell'unico metodo definito dall'interfaccia
java.lang.Runnable) che viene eseguito da un apposito thread. Questo non fa altro che eseguire un
ciclo finché il relativo thread non viene interrotto. Il ciclo del metodo si limita a stampare
l'identificatore dell'oggetto seguito con il numero di volte in cui il ciclo stesso è stato eseguito.
Ogni ciclo è intervallato da una pausa di mezzo secondo (500 millisecondi) reso possibile dal-
l'istruzione Thread.sleep(500).
La parte interessante è ciò che avviene nel metodo mairi. In primo luogo sono generate tre
istanze della classe ThreadExample i cui riferimenti sono memorizzati in un apposito array. Que-
sta classe è di tipo Runnable e pertanto il relativo metodo run può essere eseguito da un apposito
T. A questo punto (linee 37/38) sono creati i T necessari per eseguire le istanze testé generate.
Il costruttore utilizzato prevede come parametro un'istanza di tipo Runnable. Una volta creati gli
oggetti di tipo T questi sono eseguiti tramite l'invocazione del metodo Start, che a sua volta si
occupa di eseguire il metodo run dell'oggetto Runnable assegnato. Avviati i tre T, il programma
continua la propria esecuzione, questo perché il main è eseguito da un apposito T, che prevede
una pausa di cinque secondi, dopodiché tutti i T vengono terminati in modo pulito (gracefully).
In particolare, il main si occupa di invocare il metodo interrupt su ciascun thread. Il programma
quindi termina, perché sia il main sia gli altri T terminano la loro esecuzione e quindi non ci
sono più T utente in stato di esecuzione.
Nelle righe successive è mostrato un esempio dell'ultimo frammento dell'output prodotto:

— > 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 class ThreadExample extends Thread I

r " used to c o u n t the n u m b e r of executions ' /


private long count = 0;

public ThreadExampleO I
I

public void run() {


counl = 0;

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");
)

S y s t e m . o u f . p r i n t l n f — > thread:" + Thread.currentThread().get\d() + " stopped- Count:" + count);

public static void main(String[] args) I


ThreadExample[] threads = I
new ThreadExampleO, new ThreadExampleO, new ThreadExampleO I;

for (int ind = 0; ind < threads.length; ind++) (


threads[ind].start();
I

try I
Thread.s/eep(5000);
I catch (InterrupledExcepllon e) {
e.printStackTrace(); / / ignore this exception

lor (ini ind = 0; ind < threads.length; ind++) I


threads[ind].interrupt();

Come si può notare ci sono due differenze:

1. ThreadExample estende la classe java.lang.Thread;


2. non c'e' bisogno di creare appositi T, perché la classe stessa è un T.

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

Figura D.5 - Ciclo di vita concettuale dei thread java.


Figura D.6 - Rappresentazione concettuale dei concetti di monitor e lock.

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.

public class SeatsRepository {


// CONSTANTS SECTION
/* * available sign "/'
private static String AVAIL_SIGN ="*";
// ATTRIBUTES SECTION
/ ' * seat row ids "/
public char[] rowlds = null;
/'** c o l u m n ids 7
public lnt[] collds = null;
/ ' " h a s h m a p with the available seats 7
private HashMap<String,String> seatsAvailable = new HashMap<String,String>();
/ ' ' h a s h m a p w i t h the seats b o o k i n g a t t e m p t s 7
private HashMap<String,lnteger> bookingAttempts =
new HashMap<String,lnteger>();
/" * this flag states w h e t h e r or not r u n n i n g in a thread-safe e n v i r o n m e n t 7
private boolean threadSafety = true;
// M E T H O D S SECTION
public SeatsRepository(char[] rowlds, int[] collds) I
if ( ( r o w l d s == null) || (collds == n u l l ) ) {
throw n e w HlegalArgumentExceptionfRow ids or column ids null");
I
this.rowlds = rowlds;
this.collds = collds;
// initialise the seat situation
tor (inl i=0; krowlds.length; i++) {
lor (int j=0; j<collds.length; j++) (
String currSeatld = getSeatld(rowlds[i],j);
sealsAvailable.putfcurrSeatld, AVAIL_SIGN );
bookingAttempts.putfcurrSeatld, 0 );
I
I
I

public boolean isThreadSafety() I


return threadSafety;

public void setThreadSafety(boolean threadSafety) {


this.threadSafety = threadSafety;

public String getSeatld(char row, int col) I


return ""+row+(col+1);
I

public String getSeatld(int row, int col) I


return ""+rowlds[row]+(col+1);
]

public int getColumnsLength() I


return (collds == null ? -1 : collds.length );
public int getRowsLength() (
return (rowlds == nuli ? -1 : rowlds.length );

public boolean bookSeat(String seatld, String owner)


throws UlegalArgumentException I
boolean success = true;

il (threadSafety) I
synchronized (this) I
success = internalSeatBooking(seatld, owner);
I
I else I
success = internalSeatBooking(seatld, owner);
I
return success;

privale boolean InternalSeatBookingfString seatld, String owner) {

boolean success = true;


Integer attempts = bookingAttempts.get(seatld);

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;

public String toString() I


StringBuilder state = new StringBuilderfSeats :");
String LF = System.getProperty("line.separator");
String allSeats[] = seatsAvailable.keySet().toArray(new String[0]);

lor (inl ind=0; ind<allSeats.length; ind++)l


state.append(LF).append(" Seat ").append(allSeats[ind]);
state.appendf' booked: ").append(seatsAvailable.get(allSeats[ind]) );
state.appendf attempts:").append(bookingAttempts.get(allSeats[ind]) );
1
return state.toStringQ;

A questo punto si consideri un semplice esempio costituito da un insieme di T che si conten-


dono la prenotazione di una serie di poltrone di un teatro o magari di un volo. Ciascuna di
queste è identificata da un codice univoco formato da una lettera indicante la riga e da un
numero che ne indica invece la colonna. I riferimenti a queste poltrone sono incapsulati nella
classe SeatsRepository riportata nel listato visto poco sopra. Questa utilizza due HashMap, una
(seatsAvailable) per memorizzare lo stato delle poltrone ed una (bookingAttempts) per memoriz-
zarne i tentativi di prenotazione. In particolare, la convenzione utilizzata dalla prima collezione
prevede che un carattere asterisco indichi la disponibilità della poltrona, mentre una diversa
stringa ne indichi l'avvenuta prenotazione, e in particolare il codice dell'oggetto riuscito a pre-
notare la relativa poltrona. Il metodo costruttore della classe SeatsRepository si occupa di:

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.

Il metodo interessante è bookSeat il quale invoca il metodo internalSeatBooking. A seconda se


l'oggetto è stato o meno impostato in modalità tread-safe l'invocazione avviene o meno all'inter-
no di un blocco sincronizzato. Questo è molto utile per mostrare gli effetti di permettere acces-
si non sincronizzati ad oggetti che possono essere modificati simultaneamente da diversi T (race
conditions). Il metodo internalSeatBooking si occupa di:

1. incrementare il contatore di tentativi di prenotazione effettuati nei confronti di una stes-


sa poltrona (bookingAttempts.put(seatld, ++attempts), come si vede anche in questo caso è
possibile incrementare l'oggetto Integer attempts come se fosse una variabile primitiva;
2. di prenotare la poltrona in caso questa sia disponibile. In sostanza, se la stringa associata
al relativo identificativo è un asterisco, allora la poltrona è libera e quindi può essere
prenotata sostituendo tale asterisco con il codice dell'oggetto prenotante.

Il metodo internalSeatBooking contiene diversi punti in cui l'esecuzione in un ambiente MT deve


avvenire in maniera sincronizzata: per esempio l'incremento del contatore di tentativi di preno-
tazione e la prenotazione stessa. La mancanza di sincronizzazione può portare a varie conseguen-
ze. Primo caso: qualche incremento viene perso; può accadere che un T, dopo aver acquisito il
valore del contatore, venga sospeso e che uno o più T acquisiscano lo stesso valore e lo incremen-
tino. Cosi, quando poi il T originario viene nuovamente eseguito, finirà per aggiornare il contato-
re con il valore iniziale senza tener conto degli aggiornamenti avvenuti nel tempo in cui era
sospeso. In un'architettura multi CPU, poi, può accadere che un T i legga il valore e, dopo un
nanosecondo, altri T leggano lo stesso valore e quindi diversi aggiornamenti vadano persi.
Il caso di race condition sulla prenotazione è del tutto analogo al precedente, può accadere
infatti che un T verifichi lo stato di una determinata poltrona e la trovi libera e, che prima di
segnare la medesima poltrona come prenotata sia sospeso e che nel frattempo un altro T trovi
la stessa poltrona libera e la prenoti. Quando poi il T originario viene riawiato, accade che
questo porti a termine la prenotazione senza accorgersi che nel frattempo un altro T aveva
terminato la prenotazione della stessa poltrona. Anche il caso delle architetture multi CPU
mantiene la propria validità. Sebbene questi casi di race conditions possano sembrare poco
probabili, di seguito viene mostrato come condizioni di questo tipo accadano e, ancor peggio,
come sia difficile ricreare il problema. Un aspetto importante che è bene tenere sempre in
mente è che spesso race conditions possano risultare difficilmente riscontrabili. Può capitare
infatti, di trovarsi di fronte a situazione in cui in una particolare e non ben identificata occasio-
ne un programma esibisca un comportamento imprevisto, come per esempio, fornisca un valo-
re di un prezzo inaspettato, oppure che permetta che una medesima risorsa sia prenotata da più
soggetti, etc. Il problema è che individuare problemi di questo tipo, specie quando il sistema è
in produzione, può essere un'attività veramente difficile. Ed ecco l'implementazione della clas-
se BookingThread. N.B.: molti commenti non sono presenti per esigenze editoriali di spazio.

public class SeatsRepository I


// CONSTANTS SECTION
/ " available sign 7
privale static String AVAIL_SIGN ="*";
// ATTRIBUTES SECTION
/ " seat row ids 7
public char[] rowlds - null;
,/*" c o l u m n ids 7
public int[] collds = null;
/" * h a s h m a p w i t h the available seats 7
HashMap<String,Strings seatsAvailable = new HashMap<String,String>();
/ " " h a s h m a p with the seats b o o k i n g a t t e m p t s 7
HashMap<String,Integer» bookingAttempts = new HashMap<String,lnteger>();
/ " " this flag states w h e a l h e r or noi r u n n i n g in a thread-safe e n v i r o n m e n t 7
private boolean threadSafety = true;
// METHODS SECTION
public SeatsRepository(char[] rowlds, int[] collds) I

if ( (rowlds == null) || (collds == null) ) I


throw new NlegalArgumentExceptionfRow ids or column ids null");
I

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

public boolean IsThreadSafetyO I


return threadSafety;
I

public void setThreadSafety(boolean threadSafety) I


this.threadSafety = threadSafety;
I

public String getSeatld(char row, int col) I


return ""+row+(col+1);
)

public String getSeatld(int row, int col) I


return ""+rowlds[row]+(col+1);

public int getColumnsLength() I


return (collds == nuli ? -1 : collds.length );

public int getRowsLength() I


return (rowlds == nuli ? -1 : rowlds.length );
)

public Boolean bookSeat(String seatld, String owner)


throws NlegalArgumentException {

boolean success = true;

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) {

boolean success = true;


Integer attempts = bookingAttempts.get(seatld);
it (attempts == null) I
throw new NlegalArgumentExceptionfSeat not valid ("+seatld+")");

System.out.printlnf'Thread :"+owner+" requested seat:"+seatld);

bookingAttempts.putfseatld, ++attempts);

String booked = seatsAvailable.get(seatld);

if (AVAIL_SIGN.equals(booked)) (
seatsAvailable.put(seatld, owner);
) else I
success = false;
)

return success;
I

public String toString() I


StrlngBuilder state = new StringBuilderf'Seats :");
// implementazione r i m o s s a per esigenze t i p o g r a f i c h e ! ! !
return state.toString();
I

public class BookingThread i m p l e m e n t s Runnable I


// CONSTANTS SECTION
// ATTRIBUTES SECTION
/ " * this flag is responsible to gracefully s t o p this thread "/
private volatile boolean keepRunning = true;
/ * * reference to the seats r e p o s i t o r y 7
private SeatsRepository seatsRepository = null;
I " delay in millisec between t w o consecutive iterations 7
private long delay = 0;
/ * * thread s y m b o l i c name 7
private String name;
/ * * list of booked seats 7
privale ArrayList<String> bookedSeats = new ArrayList<String>();
// M E T H O D S SECTION
public BookingThread( SeatsRepository aSeatsRepository, String symbName, long aDelay) I
name = symbName;
delay = aDelay;
seatsRepository = aSeatsReposltory;

public void run() I


System.out.printf Thread ("+name+") starting . . . " ) ;

w h i l e (keepRunning) I
Random randomGen = n e w Random();

String seatld = seatsRepository.getSeatld(


randomGen.nextlnt( seatsRepository.getRowsLength() ),
randomGen.nextlnt( seatsRepository.getColumnsLength()) );
System.out.println(" Thread ("+name+") try to book: -"+seatld+"-");

boolean booked = seatsRepository.bookSeat(seatld, name);


System.out.println( " Thread ("+name+") try to book: -"+seatld+"- Result :"+booked);

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());

public void start() I


Thread aThread = new Thread(this);
aThread.start();
I

public void stopRunningQ I


keepRunning = false;
I

public String toString() I


StringBuilder strBuilder = new StringBuilderf'BookingThread: ");
// i m p l e m e n t a z i o n e r i m o s s a per esigenze t i p o g r a f i c h e ! ! !
return strBuilder.toString();

public static void main(String[] args) I


System.out.printlnfApplication starting...");
char rowlds[] = I 'A', 'B', 'C'l;
int collds[] = I O , 1 , 2 , 3,41;

SeatsRepository seatsRepository = new SeatsRepository(rowlds, collds);


seatsRepository.setThreadSafety(true);

BookingThread bookingThreads[] = new BookingThread[9];

// creates all b o o k i n g threads


lor (int indx=0; indx<bookingThreads.length; indx++) I
bookingThreads[indx] = new BookingThread(seatsRepository, ("T"+indx), 10);
I

// starts all b o o k i n g threads


lor (int indx=0; indx<bookingThreads.length; indx++) I
bookingThreads[indx].start();
I
// wait 6 s e c o n d s before s t o p p i n g the application
try I
Thread.sleep(6*1000);
I catch (InterruptedException e) I
// ignore this exception
e.printStackTrace();
I

// stops all b o o k i n g threads


lor (int indx=0; indx<bookingThreads.length; indx++) (
bookingThreads[indx].stopRunning();

System.out.println(seatsRepository.toStringO);
System.out.println("Application stopped...");
I
I

Si consideri l'implementazione della classe BookingThread. Il metodo Run si occupa di generare


due numeri randomici indicanti, rispettivamente, la riga e la colonna della poltrona desiderata,
quindi ne ottiene l'identificativo univoco con cui tenta di effettuare la prenotazione (booked =
seatsRepository.bookSeat(seatld, name)). Se quest'ultima ha esito positivo, memorizza l'identificativo
della poltrona nell'oggetto ArrayList riportante tutte le poltrone prenotate (bookedSeats.add(seatld)).
Al termine della propria esecuzione, ciascun T stampa la lista delle poltrone prenotate.
Il programma è stato eseguito molteplici volte in due modalità diverse:

1. oggetto SeatsRepository impostato ad un funzionamento MT;


2. oggetto SeatsRepository impostato a un funzionamento non M T (seatsRepository.
setTh read Safety(false)).

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

Come si può notare, la poltrona G Ì è stata prenotata da due T: T8 e T3.


Per evidenziare ancor una volta quanto subdoli possano essere i problemi di race conditions,
basti pensare che l'applicazione è stata eseguita ripetutamente per oltre 10 minuti, prima che
un caso di race conditions si manifestasse.
A questo punto verrebbe da chiedersi: visto che la sincronizzazione può risolvere molti pro-
blemi, perché non inserirla ovunque? La risposta è molto semplice ed è legata alla riduzione di
performance, sia in termini di latenza (latency, tempo impiegato per svolgere un determinato
lavoro), sia in termini di throughput (numero di lavori eseguito nel medesimo arco temporale).
Nelle versioni iniziali della, JVM, alcune statistiche evidenziavano che l'impatto di aree sincro-
nizzate poteva giungere fino a un fattore 50. Le cose sono molto migliorare con le versioni più
recenti. Resta tuttavia il fatto che, oltre a ridurre le performance, la sincronizzazione a tutto
spiano può generare colli di bottiglia e avere un effetto negativo sulla scalabilità dell'intera
applicazione. Pertanto, è necessario utilizzare la sincronizzazione solo quando sia veramente
necessario.

La perdita di p r e s t a / i o n i dovute all'utilizzo della parola chiave S y n c h r o n i z e d è 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 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

(ingresso in un'area sincronizzata), e c h e riporti tutte le modifiche effettuate nella m e m o r i a

principale, appena prima di rilasciare il lock (uscita dall'area sincronizzata). Pertanto, è facile

c o m p r e n d e r e c o m e ripetute richieste di sincronizzazione da memoria principale verso la cache

del thread e viceversa portano a una riduzione delle performance.

Il protocollo attesa e notifica (wait / notify)


Dopo avere investigato il problema della sincronizzazione dell'accesso di diversi T ad un me-
desimo oggetto è giunto il momento di vedere come sia possibile coordinare l'evoluzione di
diversi T. A tal fine, Java mette a disposizione delle primitive per inviare ed attendere l'arrivo di
determinati segnali. Il metodo wait, come lecito attendersi, permette ad un T di attendere un
determinato evento. Questo è definito nella classe antenata di tutte le classi java: java.lang.Object
e quindi implicitamente presente in tutte le classi. Di questo metodo sono disponibili le seguen-
ti versioni (overloading) di cui l'ultima è stata introdotta con la versione JDK5:

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:

• un altro T esegue il metodo notify o il metodo notityAll sull'oggetto di cui il T ha eseguito


il wait;
• un altro T lo interrompe (questo è ottenibile utilizzando le primitive stop ed interrupt,
considerando però che la prima è stata giustamente deprecata perché interrompe bru-
scamente i T non permettendo loro di concludere programmaticamente le attività intra-
prese);
• trascorre un intervallo di tempo almeno uguale a quanto specificato nell'invocazione del
metodo wait;

Il metodo wait può lanciare le seguenti eccezioni:

• IHegalArgumentException nel caso in cui i parametri forniti non siano validi;


• UlegalMonitorStateException qualora il T che esegue il metodo non abbia acquisito il relati-
vo lock\
• InterruptedException nel caso in cui un T sia interrotto da un altro.

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.

Ciclo di vita di un thread


A questo punto è possibile presentare il diagramma a stati finiti (figura D.7) che descrive il
ciclo di vita dei thread. In particolare, ciascun thread può trovarsi in uno dei seguenti stati:

• NEW: il T è stato creato ma non ancora avviato.


• READY-TO RUN: il T è stato avviato (ha eseguito il metodo Start) e sebbene non sia in attesa
dell'esecuzione di alcuna operazione di I/O, non abbia eseguito alcun wait, non si trova
in esecuzione in quanto in attesa che lo scheduler gli assegnati la CPU.
• RUNNING: il T finalmente ottiene la CPU, tipicamente per un ben definito intervallo di
tempo, e quindi prosegue l'esecuzione delle istruzioni appartenenti, direttamente o meno,
al metodo run.
• SLEEPING: il T ha seguito dell'esecuzione del metodo Sleep, e lo scheduler lo sposta in
questo stato per la durata del tempo di "sonno", al termine del quale transita nello stato
ready to run.
• SUSPENDED: l'esecuzione del T viene esplicitamente sospesa per mezzo dell'esecuzione
del metodo deprecato suspend. Questo metodo è stato opportunamente deprecato, in
quanto porta naturalmente alla generazione di situazioni di deadlock. Ciò è dovuto al
fatto che la sospensione non forza il T a rilasciare eventuali lock acquisiti. Il T poi abban-
dona questo stato quando un altro T esegue il metodo di résumé. In tal caso lo scheduler
si occupa di far transitare il T nello stato ready-to-run.
• BLOCKED: un T viene bloccato quando esegue un'istruzione di I/O oppure tenta di ac-
quisire un lock non disponibile (tenta di accedere a una sezione sincronizzata di un
oggetto). Poiché queste operazioni possono richiedere diverso tempo, lo scheduler forza
il T a rilasciare la CPU. Una volta terminata poi l'istruzione di I/O o una volta acquisito
il lock, lo scheduler si occupa di far transitare il T nello stato ready to run.
• WAITING: un T transita nello stato di attesa a seguito dell'invocazione del metodo wait su
un determinato oggetto. Una volta terminato il tempo di attesa (wait(timeout)), o ricevuto
un opportuno evento di notifica, lo scheduler si occupa di far transitare il T sospeso
nello stato ready to run.
• DEAD: un T transita nello stato di "morto", o a seguito dell'esecuzione del metodo depre-
cato di Stop, oppure al termine dell'esecuzione del proprio metodo run. Una volta che un
T passa allo stato di ready-to-run può essere terminato attraverso l'esecuzione del meto-
do Stop indipendentemente dal codice che stava eseguento o dallo stato in cui si trovata.

new Thread I)

New

End of sleeping time _ ^ Ready-to-run


<object> jiotify), <object-notifyAUO

yield« Assegnata assegna


ad un altro T CPU

Thread sleep 0 Running «object. .waitO

supend () Richiesta di I/O,


Richiesta monitor

^ Sleeping J ^ Suspended ^ ^ Blocked ^ f" Waiting

Operazione I / O eseguita ,Monitor ottenuto

stopi).
termine del metodo run ()

r D e a d J

(?)

Figura D.7 - Ciclo di vita di un Thread.


Produttore / consumatore
Si consideri ora uno scenario classico della programmazione MT noto con il nome di produtto-
re/consumatore. In particolare, si consideri il caso costituito da un T che, in qualche modo,
produce dei dati e un altro che li utilizza per fornire un qualche servizio. Si consideri inoltre il
caso molto frequente in cui i due T funzionino con velocità molto diverse. Per esempio, il
produttore sia molto veloce e il consumatore sia lento, o viceversa.
Situazioni del genere sono molto frequenti nei casi pratici. Si pensi, per esempio, ai flussi di
informazioni relative ai prezzi di prodotti finanziari provenienti dai mercati elettronici, oppure
alla gestione di eventi in un framework grafico. Ogni qualvolta vi è un problema di produttori/
consumatori a diverse velocità, la soluzione consiste nell'introdurre un meccanismo, ossia un
buffer o una coda, in grado di disaccoppiare le diverse velocità di funzionamento (figura D.8).
Si inizi con il considerare la classe che implementa la coda.

public class SimpleOueue {


// ATTRIBUTES SECTION
l " queue Id 7
private String id = null;
/ * " this object is used to restrict the access to critical area 7
private Object aLock = new Object();
/ " Indicates whether or not the whole process has been stopped 7
private volatile boolean endOtProcessing = false;
/ " queue 7
private ArrayList queue = n e w ArrayList();
// METHODS SECTION

public SimpleQueue() I
I

public SimpleQueue(String id) I


lhis();
this.id = id;
I

public String getldQ I


return id;

public void setld(String id) I


this.id = id;
I
// QUEUE SPECIFIC METHODS

public int getSize() I


return queue.size();
^^^^^^^ ; •) '^m ' ^
Figura D.8 - Schematizzazione del produttore / consumatore.

public boolean isEmpty() {


return queue.IsEmptyO;
)

public void addElement(Object dataObject) I


synchronized (aLock) I
// add the given element to the queue
queue.add(dataObject);
/'/ send an alert to threads that m i g h t be blocked on this object
aLock.notifyAIIO;
I
// request explicitily to assign the CPU to another thread
Thread.yield();
I

public Object getFirstElementFromQueue() I

Object ret = null;


synchronized (aLock) I

while ((queue.isEmptyO) & & (lendOfProcessing)) {

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

public void notifyEndOfProcessing() I


endOfProcessing = true;

synchronized (aLock) (
// send an alert t o threads that m i g h t be blocked on this object
aLock.notifyAII();

public boolean isEndOfProcessingQ I


return endOfProcessing;
I

public String toString() I


StringBuffer state = n e w StringBuffer( getBasicStatus() );
// r i m o s s o per esigenze di spazio
return state.toStringQ;

I // — end of class —

I metodi interessanti di questa classe sono: addElement e getFirstElementFromQueue. In particola-


re, il primo metodo si occupa di acquisire il lock definito su un apposito oggetto (aLock di tipo
Object). L'acquisizione del lock garantisce che nessun altro T possa modificare contemporanea-
mente la coda (tutte le operazioni di manipolazione della coda sono soggette all'acquisizione di
tale lock). Una volta entrato nella sezione protetta, il T aggiunge l'oggetto nella coda
(queue.add(dataObject)) e quindi invia una notifica (aLock.notifyAII()). Questa istruzione risveglia
tutti i T che si erano posti in stato di wait in attesa che la coda ricevesse altri dati. Il T una volta
uscito dalla sezione sincronizzata cede spontaneamente la CPU (Thread.yield()) per favorire l'en-
trata in stato di esecuzione di eventuali T consumatori.
I I metodo getFirstElementFromQueue, analogomente a quanto visto in precedenza, si occupa di
acquisire il lock sull'oggetto aLock. Fatto ciò esegue un loop che viene ripetuto fintanto che la
coda è vuota e non è stata richiesta la terminazione dell'applicazione. L'unica istruzione presen-
te nel ciclo è quella di attesa. Pertanto il T attende di essere risvegliato. Ciò potrebbe essere
causato dalla richiesta di terminare programmaticamente la coda (notifyEndOfProcessing) o dal-
l'inserimento di nuovi dati. Tuttavia, anche qualora siano presente nuovi dati, non significa che
il T riesca a consumarli prima di altri T. Usciti dal loop, l'algoritmo verifica che la coda non sia
vuota, e quindi preleva il primo elemento che viene restituito come parametro di ritorno. Come
si può notare dal codice, si è deciso di non sincronizzare i metodi getSize e isEmpty. Ciò significa
che l'acquisizione di tali valori è relativamente veloce, sebbene possano non essere aggiornatissimi.
Questa tecnica è nota con il nome di cheap read e non è implementabile in tutti i contesti. Il
codice non è stato implementato utilizzando i Generics per non complicare la comprensione da
parte di lettori non esperti. Mentre il pubblico esperto non dovrebbe avere alcun problema a
trasformare il codice nella più elegante versione basata sui Generics.

public class SimpleProducer extends Thread {


// ATTRIBUTES SECTION
/'" " queue to send tasks t o p r o d u c e r s 7
private SlmpleQueue queue = null;
/ " pause time 7
private long pauseTime = 0;
/ ' * iteration counter 7
private ini iterationsCounter = 0;

// METHODS SECTION
public SimpleProducer(SimpleQueue queue) (
this.queue = queue;
I

public SimpleProducer(SimpleQueue queue, long pauseTime) I


this(queue);
this.pauseTime = pauseTime;
I

public void run() I

iterationsCounter = 0;

System. ou/.println("PRODUCER RUNNING...");

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

System. oi/f.printlnf'PRODUCER SHUTTING DOWN...");


public void stopRunning() I
interrupt();
I
I

Il compito di questa classe è piuttosto semplice e consiste nell'implementare un T che esegue


un loop finché non ne viene richiesta l'esplicita terminazione (esecuzione del metodo StopRunning).
Il corpo del loop si occupa di generare una stringa (element = "PRODUCER COUNTER
:"+iterationsCounter) inserita successivamente nella coda. Il riferimento di tale code è fornito al-
l'oggetto attraverso il metodo costruttore. Dopo la generazione di un nuovo elemento, il T
dorme per un determinato intervallo di tempo (Thread.sleep(pauseTime)). Questa istruzione è
stata inserita per simulare produttore e consumatore funzionanti a diverse velocità.

public class SimpleConsumer extends Thread I


// ATTRIBUTES SECTION
/* * queue to send tasks to p r o d u c e r s ' /
private SimpleQueue queue = null;
I " pause t i m e 7
private long pauseTime = 0;
// METHODS SECTION
public SimpleConsumer(SimpleQueue queue) I
this.queue = queue;
I

public SimpleConsumer(SimpleQueue queue, long pauseTime) {


this(queue);
this.pauseTime = pauseTime;
I

public void run() {


System.ouf.printlnfCONSUMER RUNNING...");

try I

boolean endOfProcessing = false;

while ( (!islnterrupted()) & & (lendOfProcessing) ) I


String element = (String) queue.getFirstElementFromQueuef);
System. ouf.printlnf'CONSUMER ELEMENT RETRIEVED: >"+element+"<");

il ( (queue.isEndOfProcessingO) & & (queue.isEmptyf)) ) I


endOfProcessing = true;
I else (
Thread. s/eep(pauseTime);
I catch (InterruptedException e) I
// Ignore this exception
// n o t h i n g to do
I

System.ouf.prlntln("CONSUMER SHUTTING DOWN...");


I

public void stopRunning() {


interrupt();
I
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:

public class SimpleMain I

public static void main(String[] args) I

SimpleQueue aQueue = new SimpleQueuef'QUEUE 1");

SimpleConsumer consumer = n e w SimpleConsumerfaQueue, 100);


SlmpleProducer producer = new SimpleProducer(aQueue, 75);

consumer.start();
producer.start();

tryl
Thread.sleep(5000);
) catch (InterruptedException e) I
// ignore this excetion
e.printStackTrace();
I

producer.stopRunning();
aQueue.notifyEndOtProcessing();

System.out.printlnf END OF EXAMPLE — " ) ;


I

Anche l'implementazione di questa classe non dovrebbe destare sorprese. In particolare, il


metodo main si occupa di generare l'oggetto coda. Il relativo riferimento è quindi fornito come
parametro ai metodi costruttori dell'oggetto produttore e di quello consumatore. Una volta
creati anche questi, i relativi T sono avviati (invocazione dei rispettivi metodi Start). A questo
punto il T che esegue il main viene messo a riposo per cinque secondi, mentre il produttore e
consumatore continuano ad alternarsi nella CPU. Trascorso questo intervallo il produttore
viene bloccato e la medesima segnalazione è fornita all'oggetto coda. Quest'ultimo si occupa di
propagare tale segnalazione ai vari oggetti consumatore (in questo caso 1).
Il listato seguente mostra il frammento finale dell'output prodotto dall'esecuzione dell'esem-
pio produttore/consumatore mostrato in precedenza

P R O D U C E R A D D E D ELEMENT: > P R 0 D U C E R C O U N T E R :62<


C O N S U M E R E L E M E N T RETRIEVED: > P R 0 D U C E R C O U N T E R :47<
P R O D U C E R A D D E D ELEMENT: P R O D U C E R COUNTER : 6 3 <
P R O D U C E R A D D E D ELEMENT: > P R 0 D U C E R COUNTER :64<
C O N S U M E R E L E M E N T RETRIEVED: P R O D U C E R C O U N T E R :48<
P R O D U C E R A D D E D ELEMENT: P R O D U C E R C O U N T E R :65<
C O N S U M E R E L E M E N T RETRIEVED: > P R 0 D U C E R C O U N T E R :49<
P R O D U C E R A D D E D ELEMENT: > P R 0 D U C E R C O U N T E R :66<
C O N S U M E R E L E M E N T RETRIEVED: > P R 0 D U C E R COUNTER :50<
P R O D U C E R A D D E D ELEMENT: P R O D U C E R COUNTER :67<
END OF E X A M P L E —
PRODUCER SHUTTING DOWN...
C O N S U M E R E L E M E N T RETRIEVED: > P R 0 D U C E R COUNTER :51<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :52<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R C O U N T E R :53<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :54<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :55<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :56<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R C O U N T E R :57<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R C O U N T E R :58<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :59<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :60<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :61<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :62<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R C O U N T E R :63<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :64<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R COUNTER :65<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R C O U N T E R :66<
C O N S U M E R E L E M E N T RETRIEVED: » P R O D U C E R C O U N T E R :67<
CONSUMER SHUTTING DOWN...

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).

/ "

" R e m o v e s the element at the specified position in this list.


' Shifts any subsequent elements to Ihe left (subtracts one f r o m their
' indices).
' (<?param Index the index of the element to removed.
" © r e t u r n the element that was r e m o v e d f r o m the list.
" ©throws I n d e x O u t O f B o u n d s E x c e p t l o n if index out of range <tt>(index
* &!t: 0 || Index & g t ; = slze())</tt>.

public E remove(int index) I


RangeCheck(index);

modCount++;
E oldValue = elementData[index];

int numMoved = size - index - 1 ;


if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[—size] = null; // Let gc do Its w o r k

return oldValue;
I

Problemi di programmazione MT in Java


Introduzione
Nonostante la presenza nativa dei meccanismi per il supporto alla programmazione MT visti in
precedenza, la pratica ha evidenziato come la progettazione e realizzazione di applicazioni Java
concorrenti, scalabili e ad alte prestazioni, fosse tradizionalmente un'attività estremamente
complessa, in cui molti sviluppatori finivano per insabbiarsi. Quante volte è capitato di essere
chiamati a risolvere i problemi di un programma MT errato in Java?
Il problema è che la programmazione MT è intrinsecamente più complessa rispetto a quella
sequenziale: è necessario abbandonare lo schema mentale sicuro di un'unica sequenza
deterministica di esecuzione e iniziare a visualizzare il sistema in termini di una serie di thread
(logicamente) paralleli che evolvono più o meno indipendentemente, eventualmente sincroniz-
zandosi, tutto assolutamente in maniera indeterministica.
Il multi threading, quindi, introduce nuove problematiche nel disegno delle applicazioni ed
enfatizza quelle esistenti. Il disegno di applicazioni avanzate MT richiede uno studio attento di
fattori quali accesso a risorse condivise, comunicazione tra diversi flussi di esecuzione, perfor-
mance, throughtput, utilizzo parsimonioso della memoria e delle risorse in generale,
minimizzazione degli oggetti utilizzati, e così via.
Inoltre, la progettazione di applicazioni MT Java, basate esclusivamente sulle primitive
succitate può risultare non immediata e macchinosa al punto di indurre gli sviluppatori a cade-
re in una serie di insidie. Il problema di fondo è che, tipicamente, quando si progettano com-
plesse applicazioni concorrenti, scalabili e ad alte prestazioni, raramente si tende a pensare il
sistema in termini di metodi quali wait() e notify(); primitive probabilmente troppo a basso livello. Più
naturale è ragionare in termini di pool di thread, semafori, mutex e barriere. Logica conseguenza
è che, in passato, il disegno di applicazioni multithread Java avanzate ha richiesto lo sviluppo
preliminare e ad hoc di sofisticati framework, atti a fornire un elevato livello di astrazione.
L'obiettivo consisteva nel realizzare blocchi base, più o meno complessi, per il supporto della
programmazione parallela, con i soliti problemi derivati dalla cronica carenza di tempo:
framework mai completamente compiuti, non sempre flessibili e riutilizzabili, faticosamente
scalabili, carenti di documentazione, e così via. La situazione è ovviamente cambiata con l'av-
vento del J D K 5 e in particolare con l'introduzione del package dedicato alla concorrenza.

Aspetti da tenere in considerazione


quando si disegnano applicazioni MT
Ogni qualvolta si disegna un'applicazione multithread, le principali caratteristiche da conside-
rare attentamente sono: atomicità, visibilità e ordine di esecuzione.

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.

1. Qualora l'elemento oggetto di variazioni sia protetto da un accesso esclusivo, ne segue


che solo quando il T che ha acquisito il relativo lock e ha eseguito delle operazioni
rilascia il lock (o esce dalla zona synchronized), gli aggiornamenti diventano visibili ad
altri T che acquisiscono il medesimo lock. In sostanza i T utilizzano una sorta di cache
in cui vengono memorizzati gli elementi utilizzati da un T per eseguire le proprie azio-
ni. Pertanto, il rilascio di un lock forza la persistenza delle variabili memorizzate nella
cache del T nella memoria condivisa. Mentre l'acquisizione di un lock forza il T ad
aggiornare le variabili nella propria cache. Questo spiega perché è opportuno non abusa-
re l'utilizzo dei lock. Pertanto lock e synchronized definiscono sia zone ad accesso condi-
viso sia le regole per gestire la memoria condivisa necessarie per mantenere sincroniz-
zati i valori delle varie variabili condivise con le copie locali ai vari T.
2. Qualora un campo sia dichiarato volatile, ogni aggiornamento dello stesso eseguito da
un determinato T viene copiato e reso visibile agli altri prima che il T che ha effettuato
le modifiche esegua un'altra operazione in memoria. Pertanto, tutti i T devono aggior-
nare il valore memorizzato nella propria cache contenuto in un campo volatile prima
di accedervi.
3. La prima volta che un T accede a un determinato attributo di un oggetto, ne legge o il
valore dell'attributo all'atto dell'avvio del T (copia locale) o un nuovo valore scritto da
un altro T. L'avvio di un T (invocazione del metodo Start) ha lo stesso effetto del rilascio
di un lock e dell'acquisizione dello stesso da parte del T avviato. Questo fa sì, per
esempio, che se si avvia un T il quale esegue alcune operazioni sui campi di un oggetto
inizializzato dopo l'avvio dello stesso T, i valori aggiornati potrebbero non essere visibi-
li al T a meno di sincronizzare i metodi di accesso ai relativi elementi.
4. Quando un T termina la propria esecuzione, tutte le copie delle variabili vengono co-
piate nella memoria condivisa. Pertanto se un T sincronizza il proprio avvio con la
terminazione di un altro T (metodo join) questo ha la garanzia di accedere al valore
aggiornato delle variabili.

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.

Innovazioni del JDK 5 per il MT


Genesi
Fin dalle prime versioni del linguaggio Java, sono iniziati ad apparire framework, più o meno
validi, per il supporto alla programmazione avanzata MT. Tra le varie persone che si sono
cimentate nella progettazione e implementazione di tali framework figura anche Doug Lea
(autore del libro [CNPRGJ] e di altre pubblicazioni), professore universitario della State
University of New York Oswego, il quale, in tre anni di lavoro, è riuscito a mettere a punto un
brillante framework Java per il supporto alla programmazione MT. Tale framework ha dimo-
strato livelli di performance e scalabilità così elevati da meritare l'inclusione nella piattaforma
Java 2 versione 5.0 ("Tiger"). Lo stesso Doug Lea ha assolto il ruolo di specification leader della
JSR1661JSR166] incaricata appunto di includere il suo framework nel package java.Util.concurren!

Principi base del disegno del package


Fin dalle analisi iniziali del package java.util.concurrent, è facile constatare come questo sia stato
disegnato adottando best-practice e pattern propri del disegno OO. In particolare, vi è una
parte interna (core), non facilmente modificabile, e una serie di interfacce di estensione, tipi-
camente corredate da opportune implementazioni, le quali permettono di risolvere elegante-
mente molteplici problematiche relative al MT. La potenza del framework è quindi trasparen-
te agli sviluppatori, i quali, attraverso una serie di semplici meccanismi di estensione, sono in
grado di implementare sofisticate applicazioni MT. La mancanza di meccanismi di questo
tipo ha tradizionalmente generato non pochi problemi alla comunità dei programmatori Java,
spesso costretti a implementare codici intricati che diventano accessibili e/o manutenibili con
grandi difficoltà.
3
U
C:
o
to
3
§

<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.

Nuovi elementi complementari


Le nuove eccezioni
L'introduzione della nuova libreria, come lecito attendersi, ha generato la necessità di disporre di
nuove classi di eccezioni atte a segnalare l'occorrenza di inedite situazioni anomale (figura D.9),
spiegate in dettaglio nei paragrafi successivi. Le eccezioni del package java.util.concurrent sono:

• BrokenBarrierException. Questa viene lanciata quando un T tenta di porsi in stato di attesa


su una barriera che si trova nello stato di broken ("rottura") o quando la barriera transita
in tale condizione con il T in stato attesa. La rottura di una barriera avviene quando uno
o più T lasciano prematuramente la barriera per via di un'interruzione, un timeout, il
presentarsi di un problema, etc.
• IHegalStateException. Questa eccezione permette di segnalare che l'applicazione Java si
trova in uno stato che non permette l'esecuzione dell'operazione invocata.
• CancellationException. Eccezione utilizzata per segnalare la circostanza in cui il risultato
prodotto da un task di produzione di un valore (value-producing task), come per
esempio FutureTask, non possa essere recuperato in quanto 0 relativo T è stato cancellato.
• ExecutionException. Questa viene lanciata quando si esegue un tentativo di recuperare il
risultato di un task la cui esecuzione è stata terminata prematuramente per via dell'insor-
gere di una grave anomalia, a sua volta prontamente segnalata da apposita eccezione.
• RejectedExecutionException. Eccezione scatenata da un'istanza della classe Executor al fine
di indicare la situazione in cui un determinato task non possa essere accettato per la
relativa esecuzione.
• TimeoutException. Questa eccezione indica che il tempo massimo di permanenza nello
stato attesa, impostato per lo specifico T su un determinato oggetto (il cui tipo
appartiene al package della concorrenza), è trascorso.

TimeUnit. il nuovo tipo enumeratore.


Altra entità particolarmente ricorrente nella libreria che gestisce la concorrenza è
java.util.concurrent.Timellnit. Si tratta di un tipo enumeratore utile per rappresentare intervalli di
tempo specificati in una determinata unità di misura (NANOSECONDS(O), MICROSECONDS(I),
MILUSEC0NDS(2) e SEC0NDS(3)). Inoltre fornisce metodi utili per la conversione tra diverse uni-
tà di tempo (convert(long duration, TimeUnit unit), toSecondsQ, toMillis(), toMicros() e toNanos()), non-
ché importantissimi metodi temporizzati e per la gestione di tempi di ritardo (sleep(long timeout),
timedJoin(Thread thread, long timeout) e timedWait(Object obj, long timeout)). I seguenti sono due esem-
pi di utilizzo:

myLock.tryLock(50L, TimeUnit.MILLISECONDS)

myLock.tryLock(50L, T i m e U n i t . S E C O N D S )

Metodo per l'accesso ai nanosecondi


Un'altra importante novità introdotta con la versione JDK5, particolarmente utile nel contesto
MT, è rappresentata dal metodo System.nanoTime. Questo, come lecito attendersi, consente di
accedere a una misurazione temporale espressa in termini di IO-9 sec. Si tratta di un metodo
particolarmente utile per eseguire misurazioni relative del tempo ed è utilizzata (indirettamen-
te) per l'implementazione di metodi che prevedono come parametro un valore di tempo massi-
mo di attesa (timeout), come per esempio: BlockingQueue.offer, BlockingQueue.poll, Lock.tryLock,
Condition.await, etc. Questo metodo fornisce una precisione dell'ordine dei nanosecondi, ma
non necessariamente un'equivalente accuratezza. La frequenza di aggiornamento del valore
non è garantita e la stessa precisione è dipendente dalla piattaforma.

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:

• schedulazione e gestione dell'esecuzione di task;


• strutture di dati "coda" e code bloccanti;
• nuove collezioni concorrenti (ottimizzate per il multithreading);
• blocchi (locks)\
• sincronizzatori (semafori, barriere e variabili atomiche).

La libreria della concorrenza è organizzata in tre package: java.Util.concurrent e i due sotto-


package: locks e atomic.

Variabili atomiche (atomic irarìables).


Sono un insieme di classi (AtomicBoolean, Atomiclnteger, AtomicIntegerArray, etc.) che permettono il
trattamento atomico (non interrompibile) e, preferibilmente, non bloccante (il cui consegui-
mento dipende dai servizi offerti dalla piattaforma di esecuzione) delle variabili incapsulate
(figura D.10). Si tratta di un concetto simile alle classi Java "wrapper" che incapsulano le varia-
bili primitive Java (Boolean, Integer, etc.), anche se non le sostituiscono. In questo caso l'obiettivo
è disporre di classi ottimizzate per l'utilizzo in ambienti MT, e quindi in grado di offrire perfor-
mance superiori a quelle che si otterrebbero con il ricorso al costrutto bloccante synchronized.
Le specifiche ufficiali consigliano un'implementazione dei relativi metodi, da parte della JVM,
che sfrutti le istruzioni atomiche a livello di processore, esposte attraverso le nuove primitive
compare and swap delle implementazioni J D K 5 della JVM. Pertanto, per quanto l'essere non
bloccante non possa essere sempre garantito, perché dipende dalle istruzioni macchina delle
specifiche piattaforme, le performance risultano comunque superiori.
Queste nuove istruzioni del tipo: boolean compareAndSet(expectedValue, updateValue), la cui
implementazione è delegata a classi dotate di metodi nativi di collegamento con istruzioni a livello
macchina (come sun.misc.Unsafe), corrispondono all'implementazione, a basso livello, della stra-
tegia dell'optmistic locking: l'aggiornamento di un attributo avviene solo se il valore del para-
metro expectedValue coincide con quello reale. Qualora ciò si verifichi, 0 metodo ritorna il valore
true, altrimenti false. Pertanto, questa strategia è diametralmente opposta a quella implementa-
ta per mezzo della parola chiave synchronized che rappresenta una tecnica di pessimistic locking.
L'implementazione del metodo getAndlncrement(), utilizzato per ottenere il valore successivo di una
istanza della classe Atomiclnteger, mostra l'implementazione della strategia delYoptimistic locking
da parte delle classi atomic. Ecco il codice che implementa il metodo.

public linai int getAndlncrement() I


lor (;;) I
int current = get();
int next = current + 1 ;
if (compareAndSet(current, next))
return current;
I
l

In particolare, si ottiene l'ultimo valore (get()) della variabile incapsulata dall'oggetto, lo si


incrementa localmente, e quindi si tenta di impostare tale incremento. Si assume, quindi (otti-
misticamente), che nessun altro T nel frattempo modifichi il valore originario. Se, invece, ciò
non accade, e quindi durante l'esecuzione delle ultime due istruzioni, un altro T interviene a
modificare il valore originario, il metodo compareAndSet fallisce e quindi il ciclo (infinito) esegue
una nuova iterazione. Quindi, il valore false non permette di uscire (return current) dal metodo e
si continua a iterare per via del ciclo infinito (for (;;)). Come si può notare, si ottiene un codice
thread-safe ma non bloccante grazie alla disponibilità dell'istruzioni del tipo "compare and set"
fornite direttamente a livello di CPU.
Si consideri il listato presentato di seguito. In questo caso la classe AtomicLong è utilizzata per
generare numeri sequenziali.

public class SequenceGenerator I

private AtomicLong sequenceNumber = nuli;

public SequenceGenerator(long base) I


sequenceNumber = new AtomicLong(base);
I

public long next() {


return sequencel\lumber.getAndlncrement();
I
I

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:

• la possibilità di interrompere un T in attesa di uno specifico lock\


• la dichiarazione del tempo massimo di attesa di un thread per uno specifico lock\
• la generazione di insiemi di meccanismi "wait-notify" (chiamati condition) associati ad
uno stesso lock.

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;

public void accessCriticalSection() {


myLock.lock(); // un thread alla volta
try {
// accesso alla sezione critica
I finally I
myLock.unlock(); // rilascio del lock
I
I
)

Il J D K 5 fornisce un numero di implementazioni del concetto di lock caratterizzate da una


diversa semantica (figura D . l l ) . Le più semplici prevedono un accesso esclusivo: solo un T alla
volta può accedere alla risorsa condivisa. Pertanto, il primo T che acquisisce il lock ottiene il
15
o

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.

Sincronizzatori (Semaphore, Barrier, Latche Exchangei)


Tutti coloro che hanno studiato la programmazione concorrente, ricorderanno il concetto dei
semafori di Dijkstra. Questi, a lungo ufficialmente assenti nel mondo multithread Java, sono
stati finalmente introdotti con il package della concorrenza, con la classe Semaphore. Si tratta di
un ennesimo meccanismo utilizzato per limitare l'accesso a risorse condivise (tipicamente ad un
pool), logiche e/o fisiche che siano, da parte di un certo numero di thread. I semafori, detti
anche semafori contatori, consistono in un insieme di permessi che possono essere ottenuti
(acquireQ) finché ce ne è uno disponibile (il contatore interno è maggiore di 0). La loro restituzio-
ne (releaseQ) genera l'incremento del contatore e, potenzialmente, l'uscita dallo stato di attesa da
parte di un T che precedentemente aveva richiesto di eseguire l'acquisizione di un permesso. La
dichiarazione dei semafori richiede di specificare la relativa dimensione, che, eventualmente può
essere unitaria. In questo caso si ottiene un comportamento simile al lock, con la grande differen-
za però che nei semafori non esiste il concetto di possesso e quindi è possibile che un T sia in
grado di "sbloccare" un altro che, precedentemente, aveva ottenuto l'accesso.
Il concetto della barriera, introdotto per mezzo della classe CyclicBarrier, fornisce un meccani-
smo molto comodo per la sincronizzazione di un insieme di T. In particolare, come suggerisce
0 nome, offre la possibilità di specificare nel codice dei punti (myBarrier.await()) in cui i vari T
sono forzati ad attendere che i restanti giungano a quel medesimo punto, prima di poter prose-
guire oltre. Se, per qualche ragione, un T in attesa lascia prematuramente la barriera stessa (per
esempio scade il relativo tempo massimo di attesa, timeout), l'oggetto barriera transita in uno
stato di rottura (broken) che forza i restanti T a procedere oltre. Il tutto accompagnato da una
serie di eccezioni. Il nome ciclico deriva dal fatto che un medesimo oggetto barriera può essere
utilizzato diverse volte (per esempio all'interno di un ciclo). Il riutilizzo dell'oggetto barriera
avviene tramite l'invocazione del metodo reset(). La classe CyclicBarrier, inoltre, fornisce la possi-
bilità di specificare (nel costruttore) un comando, fornito attraverso un oggetto di tipo Runnable,
la cui esecuzione avviene quando tutti i thread hanno raggiunto la barriera, ma prima che questi
siano rilasciati (CyclicBarrier(int parties, Runnable barrierAction)). Il meccanismo delle barriere è par-
ticolarmente utile per decomporre calcoli particolarmente complessi in un insieme di sottocalcoli
di complessità inferiore e gestibili con più facilità (disegno divide et impera).
Altro meccanismo simile alla barriera è costituito dalla classe CountDownLatch (serratura a
scatto a decremento), la quale fa sì che un T rimanga in attesa dei risultati forniti da opportune
operazioni la cui completa esecuzione è assegnata ad altri T. Istanze di questo tipo prevedono
come parametro del metodo costruttore un valore di inizializzazione del contatore interno
(CountDownLatch myLatch = new CountDownLatch(OPER_COUNTER)). Il corrispondente metodo di
attesa (myLatch.await()) blocca il T chiamante finché il contatore interno raggiunga il valore 0. Il
decremento avviene quando gli altri T, ai quali, tipicamente, va passato il riferimento dell'istan-
za CountDownLatch (in questo caso myLatch), invocano esplicitamente l'operazione di decremento
(myLatch.countdownQ). Gli oggetti latch ("serrature a scatto"), al contrario delle barriere, non
prevedono un'operazione di reset che ne consenta il riutilizzo: sono a singolo colpo. Anche il
meccanismo delle "serrature a scatto" si presta ad un certo numero di interessanti utilizzi. Per
esempio, impostando il contatore a 1, è possibile creare un meccanismo on-off (aperto-chiuso)
denominato gate (cancello): tutti i T che invocano l'operazione di await, su un determinato
latch, attendono che il cancello venga aperto da parte di un altro T eseguendo l'operazione di
COuntDown sul medesimo latch. Impostando invece il contatore a un valore intero maggiore di
uno, è possibile far in modo che un T attenda finché una determinata operazione sia eseguita
un certo numero di volte o finché n T eseguano specifici comandi.
L'ultima classe presentata, appartenente a questa famiglia, è Exchanger<V>. Si tratta di un
potente meccanismo che consente di definire punti di incontro (rendezvous) tra coppie di T,
con esplicito scambio di dati. In particolare, la primitiva exchange fa sì che il T invocante atten-
da e sia bloccato nell'esecuzione del metodo, finché un altro T giunge al medesimo punto e
quindi fornisca il riferimento all'oggetto di scambio dichiarato. Anche questo metodo prevede
la possibilità di definire il tempo massimo di attesa di un T.
Qo
e=)
o
co

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 ^

O ConcurrentLinkedQueue (Zi java:iititAbstractQueue


1i"—E , _ J!

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.

Scheduling ed esecuzione di task


Si tratta di una delle aree più controverse del multithreading, nonché di una delle più frequen-
temente affrontate dagli sviluppatori Java. In effetti, la stragrande maggioranza di applicazioni
I w

.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:

• esecutori (Executor, ExecutorService, ScheduledExecutorService, etc.);


• esecutori basati su pool di thread (ScheduledThreadPoolExecutor e ThreadPoolExecutor);
• operazioni a termine (Future e FutureTask).

In particolare, il framework degli esecutori fornisce una serie di meccanismi decisamente


flessibili per la gestione dei task. Questi sono rappresentati da classi che implementano l'interfaccia
Runnable, che quindi definiscono il corpo del metodo run(), eseguibile da opportuni T.
Il package dispone di meccanismi che permettono di effettuare l'esecuzione asincrona, oltre
che ovviamente quella sincrona, la richiesta di informazioni, il controllo dell'esecuzione ed
eventualmente la cancellazione di task, la cui gestione avviene in funzione di specifiche politi-
che, dichiarabili separatamente. Il package dispone poi di una serie di pool di T assolutamente
flessibili, i quali possono essere generati sia:

• direttamente (classi ThreadPoolExecutor, ScheduledThreadPoolExectutor, la quale permette di


gestire task i cui comandi possono essere eseguiti dopo un determinato intervallo di
tempo e/o periodicamente);
• utilizzando appositi meccanismi di factory (Executors.newCachedThreadPool(),
Executors.newFixedThreadPool(int) ed Executors.newSingleThreadExecutor()).

Le diverse implementazioni sono sempre isolate da opportune interfacce, assolutamente con-


sistenti, le quali, permettono tra l'altro di definire meccanismi standard per la terminazione
(.shut-down) controllata degli esecutori e per la gestione della cancellazione di lavori non ancora
eseguiti.

Thread Pooling in breve


Tutti coloro che hanno realizzano applicazioni MT in Java, con buona probabilità, si sono
trovati di fronte al problema di dover disegnare e realizzare servizi di Thread Pooling (TP).
Sebbene il disegno di questi sistemi da tempo non presenti più grandissime problematiche,
tanto che in Internet è possibile trovarne svariati esempi, nella pratica la trasformazione di
questi esempi in efficienti meccanismi di pooling si è dimostrata un'attività non sempre ele-
mentare: sono sempre presenti importanti scenari non considerati.
L'inclusione del package java.util.concurrent nel JDK5, chiaramente, ha archiviato anche que-
sta problematica, fornendo meccanismi di TP, eleganti, ad alte prestazioni, supportati e ben
documentati.
L'idea alla base del meccanismo di pooling risiede nel disporre di un certo numero di risorse
(in questo contesto, thread) pronte ad essere utilizzate non appena necessario. Pertanto le ri-
sorse sono create, opportunamente inizializzate e poste in stato di attesa, in un periodo prece-
dente alla relativa richiesta. Ciò fa sì che nel momento in cui si vi è la necessità di utilizzarle, si
prelevi la prima disponibile e quindi, al termine dell'utilizzo, la si restituisca al pool.
Nel conteso MT, ciò equivale a dire che ogniqualvolta sia necessario eseguire un nuovo lavo-
ro, questo sia assegnato al primo T disponibile nel pool, evitando di dover attendere il tempo
necessario per eseguire le operazioni necessarie per ¡stanziare e inizializzare un'apposita risor-
sa. A seguito del completamento dell'esecuzione del lavoro assegnato, il T ritorna a disposizio-
ne del pool in uno stato di attesa e quindi pronto per il prossimo utilizzo.
Altro vantaggio delle tecniche di pooling sta nel riuscire a riutilizzare un numero finito di
risorse: gli oggetti di tipo T. Contrariamente a quanto si possa essere portati a pensare, l'obiet-
tivo delle tecniche di pooling non è sempre e solo l'aumento delle performance: in alcuni scena-
ri si tratta di una scelta obbligata: per esempio quando sia abbia disposizione un numero limi-
tato di risorse (limitate connessioni al database; in alcune particolari implementazioni, anche il
numero di T generabili è limitato).
L'esistenza di un numero limitato di risorse, tipicamente, genera lo scenario in cui all'atto
della richiesta dell'assegnazione di un T, il pool risulti temporaneamente vuoto (tutti i T del
pool sono attualmente impegnati nell'esecuzione di appositi lavori). In questo caso è necessario
attendere che le risorse siano riconsegnate al pool affinché questo sia in grado di poterle asse-
gnare all'esecuzione di altri lavori. Ciò, anche se può sembrare limitante, paradossalmente spesso
diviene un vantaggio. In effetti, non è infrequente il caso in cui sistemi non basati sul meccani-
smo dei pool (e quindi basati sull'approccio di generare nuovi T ad ogni richiesta), diano luogo
a sistemi con pessime prestazioni (eccessivo overhead per la gestione del gran numero di risor-
se) o, peggio ancora, diano luogo ad antipatiche eccezioni di esaurimento delle risorse.
Il problema ancora più serio è che tante volte sistemi così concepiti tendono a celare le loro
grosse limitazioni fino all'esecuzione dei test di carico (troppo spesso omessi) o, peggio ancora,
fino al momento della messa in opera del sistema. Questi sistemi, pertanto, se verificati con
pochi T tendono a presentare ottime prestazioni, le quali, però, degradano spaventosamente
quando il numero di questi T comincia a raggiungere il valore di soglia in cui, l'overhead della
relativa sincronizzazione e della gestione del relativo ciclo di vita diviene superiore al tempo di
esecuzione dei T stessi. Ecco quindi che risulta più opportuno disporre di un numero fisso di T
e accettare eventuali tempi di attesa quando il sistema è sotto carico, piuttosto che continuare a
generare altri T ad ogni richiesta. Ricapitolando, si ricorre ai meccanismi di thread pooling per:

• aumentare le prestazioni. In particolare, si evita di continuare a sprecare risorse nel-


l'eseguire il processo di creazione e inizializzazione delle risorse gestite dal pool. Questa
dispersione, in molte applicazioni, può risultare non compatibile con i requisiti non
funzionali (performance) del sistema che si sta realizzando. Pertanto risulta conveniente
pagare questo scotto una sola volta (all'atto dell'avvio del sistema) e quindi riciclare
risorse a sistema a regime. Inoltre, il riciclo degli oggetti riduce il volume di lavoro a
carico del Garbage Collector e quindi diminuisce il tempo di CPU assegnato a questo
daemon thread.
• migliorare il controllo del numero massimo di risorse create. In alcuni scenari, il nume-
ro delle risorse disponibili è limitato e quindi questa tecnica permette di controllare
agevolmente il numero di quelle generate.
• perfezionare il disegno del sistema. L'utilizzo del pool fa sì che vi sia un solo componen-
te delegato alla gestione delle risorse e del relativo ciclo di vita. Inoltre, ciò spesso per-
mette di risolvere elegantemente il problema dell'allocazione delle risorse, annullando
alla base problemi di dead-locking. Per esempio, è possibile creare oggetti con già allocate
tutte le risorse (come connessioni al database) di cui avranno bisogno per eseguire i
lavori a loro assegnati.

La tecnica dei pool, progettata opportunamente e con la possibilità di specificare il valore di


appositi parametri runtime (come il numero minimo e massimo di T) in funzione del sistema in
cui il programma è eseguito, è praticamente capace di produrre sempre un buon effetto sul
sistema.

Le interfacce degli esecutori


In questo paragrafo si analizza il framework degli esecutori partendo dalla sua gerarchia delle
interfacce (figura D.15).
L'interfaccia antenata di tutti gli esecutori è, come il nome lascia presupporre, Executor, che
definisce il comportamento di "semplici" oggetti in grado di eseguire generici task. L'interfaccia
è dotata di un solo metodo (execute) utilizzato per eseguire, in un tempo futuro non definito o
eventualmente immediatamente, il comando specificato fornito attraverso un opportuno ogget-
to di tipo java.lang.Runnable (la relativa classe che implementa tale interfaccia). Ciò impone di
definire il metodo run(), in grado di essere eseguito da un opportuno T. L'interfaccia Executor è un
semplice meccanismo disegnato per disaccoppiare la proposizione di un task dalla sua esecuzio-
ne. Questa interfaccia, un po' troppo basilare, non prevede alcuna implementazione diretta.
Il successivo livello della gerarchia è occupato dall'interfaccia, decisamente più estensiva,
ExecutorService, la cui peculiarità consiste nel definire una serie di metodi atti alla gestione della
terminazione del servizio (awaitTermination, shutdown, shutdownNow, etc.) e alla fornitura di og-
getti di tipo Future (invokeAII, invokeAny e Submit) utili per seguire l'evoluzione di una serie di
task asincroni (il meccanismo Future è descritto di seguito). I lettori con esperienza in C++
noteranno una certa somiglianza con la libreria Roguewave utilizzata per la programmazione
MT in C++.
L'esecuzione del metodo shutdown semplice di un oggetto ExecutorService fa sì che l'oggetto
stesso termini l'accettazione di nuovi task da eseguire e awii quindi la terminazione controllata
di sé stesso. Ciò avviene quando non ci sono più task "in esecuzione" o in attesa di essere
eseguiti. Qualora invece sia necessario eseguire uno shutdown immediato e quindi brusco, è
possibile invocare il metodo shutdownNow, il quale interrompe bruscamente sia l'esecuzione dei
task attivi, sia il processo di gestione di quelli in attesa e quindi termina restituendo la lista di
tutti i task interrotti o accettati ma non eseguiti.
I metodi submit sono un'estensione del metodo execute dell'interfaccia genitore. Questi forni-
scono una serie di servizi aggiuntivi, come la restituzione di un oggetto di tipo Future utilizza-
bile per seguire, ed eventualmente cancellare, l'esecuzione del task sottoposto.
Altro servizio aggiunto consiste nel poter specificare l'oggetto che conterrà il risultato del-
l'esecuzione del task. Infine, i metodi invokeAny e invokeAII permettono di eseguire insiemi di
task. In particolare, il primo ritorna l'oggetto risultato del task completato correttamente (sen-
za generare eccezioni) qualora questa situazione si verifichi. Una variante (overaloading) del
metodo invokeAny permette di specificare il lasso di tempo massimo di attesa che un lavoro sia
completato. Il metodo invokeAII, invece, tenta di eseguire tutti i task sottoposti, ritornando una
lista di oggetti Future, utilizzabili per seguire l'evoluzione dei rispettivi task e, ad esecuzione
terminata, per incapsularne i risultati prodotti.
IDI IDI , •> m c
.0

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

o o 1£3 . clì . ci .li


u u u u
c
.«3

_ 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

• Tli r e a d Poo l Exe cutor(co re Pool Size, m a x i m u m P o o l S i z e , k e e p A l l v e T i m ^ timeUnit, workQueue)


• T t i r e a d P o o l E x e c u t o r f c o r e P o o l S i z e , m a x i m u m P o o l S i z e , k e e pA l i v e T i m s timeUnit workQueue, rejectedExecutionHandler)
•ThreadPoolExecutorfcorePoolSize, maximumPoolSize, keepAliveTime t i m e U n i t w o r k Q u e u e , t h r e a d Factory)
*ThreadPoolEaecutor(corePoo(Slze, m a x i m u m P o o l S i z e , k e e p A l i v e T i m « timeUnil workQueue, threadFactory .rejectedExecutionHandler)
• awaitTermination ( long timeogtTimeUnit u n i t ) : boolean
• isShutdown ( ) : boolean
»isTerminatedO :boolean
«shutdown ( ) : void
»shutiJownNow ( ) : List

O
ScheduledExecutorService +ScheduledThreadPoolEKecutor (int corePoolSize)
•ScheduledThreadPoolExecutor (int corePoolSize,RejectedExecutionHandler handler)
ScheduledThreadPoolExeculor

•ScheduledThreadPoolExecutor (int corePoolSize, ThreadFactory threadFactory )


• ScheduledThreadPoolExecutor (Int corePoolSize,ThreadFactory threadFactory , RejectedExecutionHandler handler)
»schedule (Callable callable, long d e l a y , TimeUnit unit) : ScheduledFuture
• s c h e d u l e ( R u n n a b l e c o m m a n d , long delay . T i m e U n i t unit) : S c h e d u l e d F u t u r e
»scheduleAtFixedRate (Runnable c o m m a n d , long initialDelay, long period, TimeUnit unit) : ScheduledFuture
• s c h e d u l e W i t h F i i e d D e l a y (Runnable c o m m a n d , long initialDelay, long d e l a y , TimeUnit unit) : ScheduledFuture

Figura D.16 - Diagramma (semplificato) delle classi implementanti le interfacce Executor


precedente esecuzione. La differenza tra i due metodi, quindi, risiede nel momento in cui viene
considerato il tempo di ritardo (nel primo caso: dal momento in cui scade il precedente conta-
tore; nel secondo caso: al termine dalla precedente esecuzione). Qualora non si voglia specifica-
re il ritardo iniziale, è possibile fornire come parametro un valore uguale o minore di zero, il che
equivale ad una richiesta di esecuzione immediata. La stessa tecnica, ovviamente, non è consen-
tita per il parametro relativo al ritardo. L'interfaccia ScheduledExecutorService prevede la sola
classe implementante: ScheduledThreadPoolExecutor (figura D.16).

Le principali classi concrete


Dopo aver presentato le interfacce di tipo Executor, di seguito sono introdotte le due classi
concrete che le implementano (figura D.17): ThreadPoolExecutor e ScheduledExecutorService.
Come visto nel paragrado precedente, entrambe forniscono l'implementazione di un thread
pooling, con la differenza che la seconda fornisce servizi aggiuntivi atti a eseguire i lavori forniti
dopo un determinato intervallo e/o periodicamente. Qualora non si abbia necessità di utilizza-
re tali servizi (caso assai più frequente), l'utilizzo di un thread pool si riduce nel creare un'istan-
za della classe ThreadPoolExecutor, opportunamente inizializzata, e quindi nel fornire le istanze
delle classi che rappresentano i lavori (task) che il pool dovrà eseguire. I relativi parametri base,
presenti in tutte le versioni del metodo costruttore, sono: corePoolSize, maximumPoolSize,
keepAliveTime, Timellnit. I primi due sono utilizzati per regolare il numero di T che costituiscono
il pool. In particolare è possibile distinguere le seguenti casistiche:

• 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:

• ThreadPoolExecutor.CallerRunsPolicy: fa sì che lo stesso T che ha invocato il metodo di execute


esegua il task (sempre a patto che il pool non abbia eseguito lo shutdown, nel qua! caso
il nuovo task viene scartato);
• ThreadPoolExecutor.DiscardPolicy: non fa altro che ignorare "silenziosamente" il nuovo la-
voro proposto.
• ThreadPoolExecutor.DiscardOldestPolicy fa sì che, qualora il pool non abbia concluso intera-
mente lo shutdown, il task in testa alla workQueue sia sostituito con il nuovo e quindi che
l'intera esecuzione sia reiterata.

Chiaramente è sempre possibile definire proprie implementazioni dell'interfaccia.


La trattazione della classe ThreadPoolExecutor termina con la descrizione dei metodi di hook.
In particolare, questi permettono di definire il comportamento eseguibile prima e dopo l'ese-
cuzione di ogni task e prima della completa terminazione dell'esecutore. Questi metodi sono,
rispettivamente, beforeExecute, afterExecute e terminated, tipicamente utilizzati per generare dati
statici, eseguire il debugging del sistema, aggiungere informazioni nel file di log, eseguire op-
portune azioni prima della completa terminazione del pool e così via. L'unico elemento che
bisogna considerare è che si tratta di metodi protetti ma non final e di cui quindi è possibile
eseguire l'overriding.
Esaminata la classe che fornisce l'implementazione dei TP, si consideri lo schedulatore dei
lavori (ScheduledThreadPoolExecutor, figura D.17), che permette di eseguire determinati task allo
scadere di un determinato lasso temporale e/o di ripeterne l'esecuzione a intervalli di tempo
prestabilito. Anche in questo caso, dettagli di basso livello relativi alla gestione MT sono com-
pletamente risolti dal package, quindi sono trasparenti al programmatore. Pertanto le respon-
sabilità degli sviluppatori sono circoscritte alla definizione dei vari task e alla relativa sottomis-
sione a un'istanza, opportunamente inizializzata, dello schedulatore.
Per quanto riguarda i metodi costruttori, non ci sono novità di rilievo, come lecito attendersi,
considerata la relazione di ereditarietà che lega la classe schedulatrice di TP alla classe genitore
esecutore di T (c/r. figura D.16). In ogni modo, la versione più completa del metodo costruttore
prevede la seguente firma:

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

compareTo (T o) : int +cancel(boolean m a y l n t e r n j p t l f R u n n i n g ) : boolean


•get() : V
+ get (long t i m e o u t , T i m e U n i t unii) :V
+ isCancelled() : boolean
• isDone () : boolean
Delayed

getDelay (TimeUnit unit) : long

• irlU'üüf^-
ScheduledFuture

Figura D.17 - Gerarchia semplificata delle interfacce Future.

Un'elaborazione asincrona può essere fatta terminare prematuramente tramite l'invocazio-


ne del metodo cancelQ. Chiaramente, ciò non è possibile a elaborazione conclusa. I restanti due
metodi, isCancelledQ e isDone(), permettono di verificare se, rispettivamente, l'elaborazione sia
terminata prematuramente tramite invocazione del rispettivo metodo, e se l'elaborazione sia
conclusa, indipendentemente dalla relativa causa: invocazione del metodo cancel, verificarsi di
un errore e quindi relativa eccezione o elaborazione terminata spontaneamente.
L'interfaccia Future prevede un'unica classe implementazione: FutureTask ed è estesa
dall'interfaccia ScheduledFuture, tipicamente utilizzata per monitorare l'evoluzione dei task la
cui esecuzione è posticipata e/o ripetuta. In altre parole, è utilizzata congiuntamente con le
classi (attualmente una) che implementano ScheduledExecutorService.
Dall'analisi del package è possibile notare che non esiste un'apposita classe implementazione
di questa interfaccia (per esempio ScheduledFutureTask), come invece accade per l'interfaccia
Future. Il mistero è presto risolto accedendo al codice della classe ScheduledThreadPoolExecutor: al
suo interno infatti è presente una classe annidata che appunto implementa l'interfaccia:

private class S c h e d u l e d F u t u r e T a s k < V > extends FutureTask<V>

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]

F R A N K A R M O U R , G R A N V I I . I . I : M I I . L K R , Advanced Use Case Modeling. Software Systems, Addison Wesley

[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]

BRL'CR: E<:KI-:I„ 1'hinking in Java, Prentice Hall Ptr

[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]

JOSHUA BLOCH, Effective Java. Programming Language Guide, Sun MicroSystems

[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]

VINCENT MASSOL, TED HUSTED, JUnit in Action, Manning Publications Co.

[JUNPCK]

KENT BECK, JUnit Pocket Guide, O'Reilly

[GDLPRF]

HERNEST NAGEL, JAMES R. NEWMAN, Godel's proof, New York University Press, 2 0 0 1

[APCL4J]

SAMUNDRA GUPTA, ProApache Log4J, 2" J Edition, June, 2 0 0 5

[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:

PROGRAMMARE IN JAVASCRIPT HTML, XHTML E CSS


Shelley P o w e r s Elizabeth Castro
• Brossura • 17x24 cm • 456 pagine
• Brossura • ! 7x23.5 cm • 352 pagine
ISBN: 978-88-481-1914-6 • 34,90 €
ISBN: 978-88-481-2058-6 • 29.90 €

ENTERPRISE JAVABEANS APPLICAZIONI WEB DATABASE


IV EDIZIONE
CON PHP E MYSQL
Il EDIZIONE
Richard M o n s o n - H a e f e l
H u g h Williams, D a v i d L a n e
• Brossura • 17x23,5 cm • 800 pagine
• Brossura • 17x23,5 cm • 864 pagine
ISBN: 978-88-481-1752-4 • 49.90 €
ISBN: 978-88-481-1722-7 • 49,90 €

JAVA E OPEN SOURCE MYSQL GUIDA AVANZATA


Massimiliano Bigatti TECNICHE E STRUMENTI AVANZATI
• Brossura • 17x23.5 cm • 352 pagine PER AMMINISTRATORI MYSQL
ISBN: 978-88-481-1718-0 • 29.90 € J e r e m y D. Z a w o d n y , D e r e k J. Balling
• Brossura • 17x23,5 cm • 312 pagine
ISBN: 978-88-481-1696-1 • 29,90 €
JAVA SERVER PAGES
III EDIZIONE
Bergsten H a n s MYSQL GUIDA POCKET
" Brossura ' 17x23,5 cm • 752 pagine G e o r g e Reese

ISBN: 978-88-481-1651-0 • 49.90 € • Brossura • 11x18 cm • 144 pagine


ISBN: 978-88-481-2087-6 • 9.90 €

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 €

HTML & XHTML GUIDA POCKET


PROGRAMMARE IN PHP E MYSQL GUIDA POCKET
Michele Davis, J o n Phillips Jennifer N i e d e r s t R o b b i n s

• Brossura • 17x23,5 cm • 422 pagine • Brossura • 11x18 cm • 108 pagine


ISBN: 978-88-481-2088-3 • 32,90 € ISBN: 978-88-481-1978-8 » 8,90 €

JAVA GUIDA POCKET PROGRAMMARE IN ASP.NET AJAX


Robert Liguori, Patricia Liguori Christian Wenz

• Brossura • 11x18 cm • 176 /tagine • Brossura • 17x23,5 cm • 488 pagine


ISBN: 978-88-481-2222-1 • 13,90 € ISBN: 978-88-481-2085-2 • 39,90 €

Potrebbero piacerti anche