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 ([email protected], 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
[email protected]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 ([email protected]),
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 < e >. La stessa sorte spetta al carattere & che va sostituito con la stringa &,
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<?></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 esplicit