Sei sulla pagina 1di 777

JAVA

Pellegrino Principe
Apogeo - IF - Idee editoriali Feltrinelli s.r.l.
Socio Unico Giangiacomo Feltrinelli Editore s.r.l.

ISBN edizione cartacea: 9788850333080

Il presente file pu essere usato esclusivamente per finalit di carattere personale. Tutti i
contenuti sono protetti dalla Legge sul diritto dautore.
Nomi e marchi citati nel testo sono generalmente depositati o registrati dalle rispettive
case produttrici.
Ledizione cartacea in vendita nelle migliori librerie.
~
Sito web: www.apogeonline.com
Scopri le novit di Apogeo su Facebook.
Seguici su Twitter @apogeonline
Introduzione

Questo libro ha come obiettivo quello di insegnare la programmazione in Java in


modo semplice e immediato, andando direttamente al sodo e cercando di concentrarsi
sullessenziale.
Dire le cose essenziali, tuttavia, non comporta tralasciare informazioni importanti,
e infatti per ogni argomento si cercato di mostrare i punti salienti, corredandoli di
esempi e listati.
Il lettore tenga presente, quindi, che questo non un libro leggero o superficiale,
poich affronta tutti gli argomenti che una guida completa si prefigge di trattare a cui
sono associati innumerevoli listati e snippet di codice da studiare, compilare e
provare.
Organizzazione del libro
Il libro organizzato nei capitoli elencati di seguito.

Capitolo 1, Introduzione al linguaggio: introduciamo il lettore a una


panoramica del linguaggio e spieghiamo i concetti propedeutici per compilare
ed eseguire un programma Java.
Capitolo 2, Variabili, costanti, letterali e tipi: mostriamo come dichiarare e
definire i dati di un programma e come attribuire a essi un tipo di appartenenza.
Capitolo 3, Array: spieghiamo la struttura di dati array e come manipolarla.
Mostriamo altres la gestione degli array multidimensionali.
Capitolo 4, Operatori: mostriamo tutti gli operatori che il linguaggio mette a
disposizione per manipolare i dati. Si passa dallillustrazione dei semplici
operatori aritmetici a quella dei complessi operatori bitwise.
Capitolo 5, Strutture di controllo: vediamo come gestire lesecuzione del
flusso di un programma attraverso le strutture di iterazione e di selezione.
Capitolo 6, Metodi: mostriamo come progettare i metodi, ovvero le funzioni
basilari di un programma che eseguono determinate operazioni.

NOTA
Nellorganizzazione dei capitoli si preferito introdurre i metodi prima del costrutto di classe
poich sono strutture sintattiche pi semplici, in modo da affrontare i concetti essenziali del
linguaggio in ordine crescente di complessit. Filosoficamente, questa scelta si sposa bene con
linquadramento del paradigma ad oggetti (basato sulle classi) inteso come estensione del
paradigma procedurale (basato sulle procedure). Non a caso, le strutture di controllo della
programmazione ad oggetti, che vengono poi utilizzate allinterno dei metodi, sono le stesse del
paradigma procedurale.

Capitolo 7, Programmazione basata sugli oggetti: vediamo come si crea un


nuovo tipo di dato attraverso il costrutto di classe. Illustriamo che cosa sono i
membri di una classe e la loro visibilit (information hiding) e come si possono
definire al suo interno. Analizziamo i metodi costruttori, la keyword this e la
progettazione di classi annidate. Infine, trattiamo i tipi enumerati.
Capitolo 8, Programmazione orientata agli oggetti: descriviamo come si
creano gerarchie di classi (ereditariet) e che cos il polimorfismo. Parliamo,
inoltre, di classi astratte e interfacce. Chiudiamo il capitolo con lillustrazione
delle classi anonime.
Capitolo 9, Programmazione generica: spieghiamo come si creano metodi e
classi generiche, ovvero come si effettua una programmazione che manipola tipi
parametrizzati.
Capitolo 10, Programmazione funzionale: introduciamo il lettore allo studio
del paradigma della programmazione funzionale attraverso un percorso di
apprendimento graduale e completo, che parte dallanalisi dei concetti
propedeutici quali il lambda calcolo, limmutabilit dello stato, le closure e cos
via, e termina con un dettaglio di come i progettisti di Java hanno introdotto i
pattern propri di tale paradigma nel linguaggio. Tra gli argomenti: le interfacce
funzionali, i metodi di default, le lambda expression, il target typing.

NOTA
In alcuni listati, laddove necessario, si preferito lasciare limplementazione delle interfacce
funzionali piuttosto che scrivere lequivalente lambda expression. Ci per due ragioni: la prima di
carattere teorico-didattica, legata a garantire al lettore, in questa fase di studio preliminare, una
maggiore comprensione di quello che effettivamente avviene con lutilizzo delle interfacce
funzionali; la seconda di carattere pratico-didattica, connessa a permettere al lettore di esercitarsi
a cambiare, in quelle parti di codice dove presente uninterfaccia funzionale, lequivalente
lambda expression. Quanto detto ha una sola eccezione nel Capitolo 17, dove viene trattata
lastrazione stream, che stata pensata proprio per supportare nativamente le lambda
expression; pertanto in quel contesto, nei listati relativi non sono state usate le interfacce
funzionali ma direttamente tali lambda expression.

Capitolo 11, Errori software: illustriamo come si intercettano e gestiscono gli


errori software grazie allutilizzo di un meccanismo definito gestione delle
eccezioni.
Capitolo 12, Package: vediamo come si creano librerie di tipi correlati e come
si rendono disponibili (deployment) in un ambiente di sviluppo o di produzione.
Capitolo 13, Annotazioni: analizziamo come si creano e utilizzano dei
metadati che annotano gli elementi (variabili, metodi e cos via) del linguaggio
Java.
Capitolo 14, Documentazione del codice sorgente: mostriamo come utilizzare
dei tag speciali che consentono di formattare il codice sorgente in modo che
sia possibile generare per esso unadeguata documentazione.
Capitolo 15, Caratteri e stringhe: impariamo a utilizzare le classi principali
che consentono di manipolare i caratteri e le stringhe (Character, String,
StringBuilder e cos via).
Capitolo 16, Espressioni regolari: spieghiamo cosa sono e come impiegare le
regex per costruire dei sofisticati pattern per la ricerca di corrispondenze di
caratteri.
Capitolo 17, Collezioni: studiamo come utilizzare le collezioni, o contenitori
(Collection, List, Map e cos via), costituite da gruppi di elementi tra loro
naturalmente collegati che possono subire operazioni di manipolazione.
Introduciamo, infine, allo studio dellastrazione stream, che rappresenta una
sequenza di elementi su cui possibile compiere operazioni aggregate, di tipo
filter-map-reduce, sia sequenziali sia parallele.
Capitolo 18, Programmazione concorrente: trattiamo i concetti di processo e
thread e di come scrivere in Java programmi che sono in grado di eseguire pi
operazioni in parallelo.
Capitolo 19, Input/Output: stream e file: illustriamo come effettuare le comuni
operazioni di input e output dei dati (stream, file e cos via).
Capitolo 20, Progettazione di interfacce utente: impariamo a scrivere
programmi Java dotati di una GUI (Graphical User Interface) mediante
lutilizzo del potente framework Swing.
Capitolo 21, Programmazione di rete: studiamo cos una rete (suite TCP/IP)
e le API che Java mette a disposizione per la programmazione dei socket e dei
datagram.
Capitolo 22, Programmazione dei database: analizziamo i concetti
propedeutici dei database relazionali come le tabelle e le relazioni tra di esse,
diamo uno sguardo al linguaggio SQL e studiamo larchitettura software JDBC,
che consente di scrivere applicazioni che si interfacciano con una base di dati in
modo semplice, consistente e indipendente dal database usato.
Capitolo 23, Sviluppo di applicazioni web: impariamo a utilizzare la
piattaforma JEE 7 (Java Enterprise Edition) per progettare e sviluppare
applicazioni per il web (servlet, JavaServer Pages, JavaServer Faces e cos via).
Appendice A, Installazione e configurazione della piattaforma JSE: vediamo
come installare e configurare la piattaforma standard di Java in ambiente
Windows e in ambiente GNU/Linux.
Appendice B, Installazione e configurazione della piattaforma JEE: vediamo
come installare e configurare la piattaforma di Java destinata alla progettazione
e allo sviluppo di applicazioni web.
Appendice C, Installazione e configurazione di MySQL: illustriamo come
installare e configurare il database server MySQL sia per sistemi Windows sia
per sistemi GNU/Linux. Analizziamo, infine, come installare e configurare
Connector/J, il driver JDBC ufficiale per il server MySQL.
Appendice D, Installazione e utilizzo di NetBeans: mostriamo come installare
e utilizzare lIDE (Integrated Development Environment) NetBeans per la
creazione di applicazioni Java per le piattaforme JSE e JEE.
Appendice E, Applet: vediamo come scrivere dei programmi Java denominati
applet, che girano allinterno di un browser web.
Struttura del libro e convenzioni
Gli argomenti del libro sono, ovviamente, organizzati in capitoli. Ogni capitolo
numerato in ordine progressivo e denominato significativamente nel suo obiettivo
didattico (per esempio, Capitolo 2, Variabili, costanti, letterali e tipi). I capitoli
sono poi suddivisi in paragrafi di pertinenza.
Allinterno dei paragrafi possiamo avere dei blocchi di testo o di grafica, a
supporto alla teoria, denominati come segue:

Listato NrCapitolo.NrProgressivo Descrizione per i listati del codice sorgente;


Decompilato NrCapitolo.NrProgressivo Descrizione per i listati dei file .class
decompilati;
Sintassi NrCapitolo.NrProgressivo Descrizione per la sintassi di un costrutto
del linguaggio;
Snippet NrCapitolo.NrProgressivo Descrizione per un frammento di codice
sorgente;
Shell NrCapitolo.NrProgressivo Descrizione per un comando di shell;
Warning NrCapitolo.NrProgressivo Descrizione per un avviso (warning) del
compilatore;
Errore NrCapitolo.NrProgressivo Descrizione per un errore di compilazione o
di esecuzione;
Output NrCapitolo.NrProgressivo Descrizione per loutput di un programma;
Figura NrCapitolo.NrProgressivo Descrizione per una figura;
Tabella NrCapitolo.NrProgressivo Descrizione per una tabella.

Per esempio, il blocco denominato Listato 6.2 Classe ArgomentoImmodificabile


indica il listato di codice numero 2 del Capitolo 6 avente come descrizione la classe
ArgomentoImmodificabile.

Per quanto attiene ai listati, abbiamo adottato la seguente convenzione: i puntini di


sospensione () eventualmente presenti indicano che in quel punto sono state omesse
alcune parti del listato. Ovviamente, le medesime parti sono presenti nei relativi file
.java allegati al libro. Gli stessi caratteri possono talvolta trovarsi anche negli output

di un programma eccessivamente lungo.


Codice sorgente e classi
Allindirizzo http://www.apogeonline.com/libri/9788850317042/scheda possibile scaricare
un archivio ZIP che contiene tante cartelle quanti sono i capitoli del libro. Ciascuna
cartella, denominata Cap01, Cap02 e cos via, contiene a sua volta due sottocartelle,
denominate Listati e Snippets.

Allinterno della cartella Listati sono presenti, in modo indipendente, sia tutti i file
sorgente .java del capitolo di pertinenza con le eventuali risorse complementari, sia
altre sottocartelle direttamente correlate a specifici progetti caricabili con lIDE
NetBeans, che ricalcano come denominazione, laddove opportuno, i rispettivi file
sorgenti.
Allinterno della cartella Snippets si trovano invece dei file .txt che contengono gli
snippet di codice eventualmente presenti nel capitolo di pertinenza.

Utilizzo dei comandi javac e java per compilare ed


eseguire i programmi Java
Per rendere agevole la compilazione e lesecuzione dei programmi Java,
indipendentemente dallIDE, riteniamo utili i consigli riportati di seguito.
Se si utilizza Windows, creare le seguenti strutture di directory:

per i sorgenti, C:\MY_JAVA_SOURCES;


per le classi, C:\MY_JAVA_CLASSES;
per i package, C:\MY_JAVA_PACKAGES;
per gli archivi JAR, C:\MY_JAVA_JARS;
per la documentazione, C:\MY_JAVA_DOCUMENTATION.

Se si utilizza GNU/Linux, creare le seguenti strutture di directory:

per i sorgenti, /opt/MY_JAVA_SOURCES;


per le classi, /opt/MY_JAVA_CLASSES;
per i package, /opt/MY_JAVA_PACKAGES;
per gli archivi JAR, /opt/MY_JAVA_JARS;
per la documentazione, /opt/MY_JAVA_DOCUMENTATION.
Copiare il listato da compilare nella cartella MY_JAVA_SOURCES. Compilare il listato
utilizzando il comando javac con il flag -d che indica il percorso dove generare i file
.class. Per esempio, per compilare il listato UsoDiChar.java invocare, dalla directory
MY_JAVA_SOURCES, il compilatore, per esempio javac -d C:\MY_JAVA_CLASSES UsoDiChar.java per
il sistema operativo Window e javac -d /opt/MY_JAVA_CLASSES UsoDiChar.java per il sistema
operativo Gnu/Linux.
Inoltre, se vi sono pi classi da compilare, magari perch una classe utilizza i
servizi di unaltra classe, si pu lanciare il comando di compilazione, come javac -d
oppure javac -d /opt/MY_JAVA_CLASSES Time.java
c:\MY_JAVA_CLASSES Time.java Time_Client.java

Time_Client.java .

Procedere poi come segue.

1. Eseguire il programma dalla cartella MY_JAVA_CLASSES, ricordandosi di anteporre il


nome del package com.pellegrinoprincipe al nome della classe che lo rappresenta
(tranne dove diversamente indicato).
2. Creare i package nella cartella MY_JAVA_PACKAGES.
3. Creare gli archivi nella cartella MY_JAVA_JARS.

In conclusione, sottolineiamo quanto segue.

Quando negli esempi del libro si invocheranno i comandi di compilazione ed


esecuzione del codice (o altri comandi, come per esempio jar), daremo per
assodato il rispetto dei percorsi di directory precedentemente indicati. Ci
significa che, se per esempio invocheremo il comando di compilazione,
presupporremo che abbiate gi cambiato la corrente directory in MY_JAVA_SOURCES e
che in tale percorso si trover il file sorgente indicato.
Gli eventuali comandi di shell presentati sono indicati secondo le convenzioni
del sistema Windows; per i sistemi GNU/Linux occorre attenersi alle regole
specifiche di questo sistema operativo.

Utilizzo dellIDE NetBeans per compilare ed


eseguire i programmi Java
Se lo si desidera, possibile utilizzare lIDE NetBeans per editare, compilare,
eseguire e debuggare il codice sorgente presente nel libro. A tal fine consultare
lAppendice D per una spiegazione in merito allutilizzo introduttivo di tale IDE e
alla creazione e allimpiego dei progetti.
Infine, importante precisare che tutti i progetti qui presentati presuppongono
uninstallazione di default del JDK nel percorso C:\Program Files (x86)\Java\jdk1.8.0 per
Windows e /opt/jdk1.8.0 per GNU/Linux.
Capitolo 1
Introduzione al linguaggio

Java un moderno linguaggio di programmazione le cui origini risalgono al 1991,


quando presso Sun Microsystems un team di programmatori, formato principalmente
da James Gosling, Patrick Naughton, Chris Warth, Ed Frank e Mike Sheridan, lavor
al suo sviluppo. Inizialmente il linguaggio fu chiamato OAK (in onore della quercia
che Gosling vedeva dalla finestra del suo ufficio, infatti oak in inglese significa
quercia) e fu sviluppato in seno a un progetto chiamato Green Project.
Lo scopo del progetto era quello di dotare svariati dispositivi elettronici di
consumo di un meccanismo tramite il quale fosse possibile far eseguire, in modo
indipendente dalla loro differente architettura, i programmi scritti per essi.
Questo meccanismo si sarebbe dovuto concretizzare nella scrittura di un
componente software, denominato macchina virtuale, da implementare per il
particolare hardware del dispositivo ma che, tuttavia, sarebbe stato in grado di far
girare il codice intermedio, denominato bytecode, generato da un compilatore del
linguaggio Java.
Sostanzialmente lobiettivo era quello di permettere agli sviluppatori di scrivere i
programmi una sola volta e con la certezza che sarebbe stato possibile eseguirli
dappertutto senza alcuna modifica sullhardware destinatario (da qui lo slogan
WORA, Write Once Run Anywhere).
Nel maggio del 1995 fu completato lo sviluppo di OAK, con lannuncio alla
conferenza Sun World 95. Con loccasione, visto che il nome OAK apparteneva gi
a un altro linguaggio di programmazione, si decise di cambiare il nome del
linguaggio di SUN in Java.
CURIOSIT
Probabilmente il nome Java fu deciso durante un incontro tra gli sviluppatori in un bar mentre
bevevano caff americano; infatti java un termine utilizzato nello slang per indicare una
bevanda fatta con miscele di chicchi di caff.

Successivamente, nel 1996, alla prima JavaOne Developer Conference, venne


rilasciata la versione 1.0 del JDK (Java Development Kit). A partire da quel momento
inizi una capillare e profonda diffusione di Java che diventato un linguaggio di
programmazione ampiamente diffuso e utilizzato per programmare ogni tipo di
dispositivi (dai cellulari ai computer, agli elettrodomestici e cos via) dotati di una
macchina virtuale.
Il successo di Java aument notevolmente con lesplosione di Internet e del Web
dove, allepoca, non esistevano programmi che permettessero di fornire contenuto
dinamico alle pagine HTML (se si escludevano gli script CGI). Con Java infatti fu
possibile, attraverso programmi detti applet, gestire nel Web contenuti dinamici e allo
stesso tempo indipendenti dal sistema operativo e dal browser sottostante. Dopo di
ci il successo di Java fu inarrestabile e attorno al linguaggio furono create molteplici
tecnologie, per esempio quelle per laccesso indipendente ai database (JDBC), per lo
sviluppo lato server del Web (Servlet/JSP/JSF) e cos via. Al momento in cui
scriviamo la versione ufficiale del linguaggio la 1.8 (chiamata 8.0 per motivi di
marketing), disponibile a partire dalla primavera del 2014.
NOTA STORICA
Sun Microsystems era una societ multinazionale, produttrice di software e di hardware, fondata
nel 1982 da tre studenti delluniversit di Stanford: Vinod Khosla, Andy Bechtolsheim e Scott
McNealy. Il nome infatti lacronimo di Stanford University Network. Nel gennaio del 2010 Sun
stata acquistata dal colosso informatico Oracle Corporation per la considerevole cifra di 7,4
miliardi di dollari. Tra i prodotti software ricordiamo il sistema operativo Solaris e il filesystem di
rete NFS, mentre tra i prodotti hardware le workstation e i server basati sui processori RISC
SPARC.
Paradigmi di programmazione
Un paradigma, o stile di programmazione, indica un determinato modello
concettuale e metodologico, offerto in termini concreti da un linguaggio di
programmazione, al quale fa riferimento un programmatore per la progettazione e
scrittura di un programma informatico e dunque per la risoluzione del suo particolare
problema algoritmico. Si conoscono molti differenti paradigmi di programmazione,
ma quelli che seguono ne rappresentano i pi comuni.

Il paradigma procedurale, dove lunit principale di programmazione , per


lappunto, la procedura o la funzione che ha lo scopo di manipolare i dati del
programma. Questo paradigma talune volte indicato anche come imperativo
perch consente di costruire un programma indicando dei comandi (assegna,
chiama una procedura, esegui un loop e cos via) che esplicitano quali azioni si
devono eseguire, e in quale ordine, per risolvere un determinato compito.
Questo paradigma si basa dunque su due aspetti di rilevo: il primo riferito al
cambiamento di stato del programma che causa delle istruzioni eseguite (si
pensi al cambiamento del valore di una variabile in un determinato tempo
durante lesecuzione del programma); il secondo inerente allo stile di
programmazione adottato che orientato al come fare o come risolvere
piuttosto che al cosa si desidera ottenere o cosa risolvere. Esempi di linguaggi
che supportano il paradigma procedurale sono FORTRAN, COBOL, Pascal e C.
Il paradigma ad oggetti, dove lunit principale di programmazione loggetto
(nei sistemi basati sui prototipi) oppure la classe (nei sistemi basati sulle classi).
Questi oggetti, definibili come virtuali, rappresentano, in estrema sintesi,
astrazioni concettuali degli oggetti reali del mondo fisico che si vogliono
modellare. Questi ultimi possono essere oggetti pi generali (pensate a un
computer, per esempio) oppure oggetti pi specifici, ovvero maggiormente
specializzati (per esempio una scheda madre, una scheda video e cos via). Noi
utilizziamo tali oggetti senza sapere nulla della complessit con cui sono
costruiti e comunichiamo con essi attraverso linvio di messaggi (sposta il
puntatore, digita dei caratteri) e mediante delle interfacce (il mouse, la tastiera).
Inoltre, essi sono dotati di attributi (velocit del processore, colore del case e
cos via) che possono essere letti e, in alcuni casi, modificati. Questi oggetti reali
vengono presi come modello per la costruzione di sistemi software a oggetti,
dove loggetto (o la classe) avr metodi per linvio di messaggi e propriet che
rappresenteranno gli attributi da manipolare. Esempi di linguaggi che
supportano il paradigma ad oggetti sono: Java, C#, C++, JavaScript, Smalltalk e
Python.

TERMINOLOGIA
Oggetto, tecnicamente, significa istanza di una classe; questa terminologia verr introdotta nel
Capitolo 7 relativo alle classi.

Il paradigma funzionale, dove lunit principale di programmazione la


funzione vista in puro senso matematico. Infatti, il flusso esecutivo del codice
guidato da una serie di valutazioni di funzioni che, trasformando i dati che
elaborano, conducono alla soluzione di un problema. Gli aspetti rilevanti di
questo paradigma sono: nessuna mutabilit di stato (le funzioni sono side-effect
free, ossia non modificano alcuna variabile); il programmatore non si deve
preoccupare dei dettagli implementativi del come risolvere un problema ma
piuttosto di cosa si vuole ottenere dalla computazione. Esempi di linguaggi
che supportano il paradigma funzionale sono: Lisp, Haskell, F#, Erlang e
Clojure.
Il paradigma logico, dove lunit principale di programmazione il predicato
logico. In pratica con questo paradigma il programmatore dichiara solo i fatti
e le propriet che descrivono il problema da risolvere lasciando al sistema il
compito di inferirne la soluzione e dunque raggiungerne il goal (lobiettivo).
Esempi di linguaggi che supportano il paradigma logico sono Datalog, Mercury,
Prolog e ROOP.

Il linguaggio Java supporta, principalmente, il paradigma di programmazione ad


oggetti, dove lunit principale di astrazione rappresentata dalla classe e dove vi
piena conformit con i principi fondamentali di tale paradigma: incapsulamento,
ereditariet e polimorfismo.
NOTA
Java considerato un linguaggio di programmazione multiparadigma poich, di fatto, supporta
anche quello procedurale e, con la sua ultima versione, ha iniziato a supportare, seppure
limitatamente allintroduzione delle lambda expression, anche quello funzionale.

Vediamo in breve che cosa significano questi principi, che saranno poi
approfonditi nei capitoli di pertinenza.

Lincapsulamento un meccanismo attraverso il quale i dati e il codice di un


oggetto sono protetti da accessi arbitrari (information hiding). Per dati e codice
intendiamo tutti i membri di una classe, ovvero sia i dati membro (come le
variabili), sia le funzioni membro (definite anche, in molti linguaggi di
programmazione orientati agli oggetti, semplicemente come metodi). La
protezione dellaccesso viene effettuata applicando ai membri della classe degli
specificatori di accesso, definibili come pubblico, con cui si consente laccesso a
un membro di una classe da parte di altri metodi di altre classi; protetto, con cui
si consente laccesso a un membro di una classe solo da parte di metodi
appartenenti alle sue classi derivate; privato, con cui un membro di una classe
non accessibile n da metodi di altre classi n da quelli delle sue classi
derivate, ma soltanto dai metodi della sua stessa classe.
Lereditariet un meccanismo attraverso il quale una classe pu avere
relazioni di ereditariet nei confronti di altre classi. Per relazione di ereditariet
intendiamo una relazione gerarchica di parentela padre-figlio, dove una classe
figlio (definita classe derivata o sottoclasse) deriva da una classe padre (definita
classe base o superclasse) i metodi e le propriet pubbliche e protette, e dove
essa stessa ne definisce di proprie. Con lereditariet si pu costruire, di fatto, un
modello orientato agli oggetti che in principio generico e minimale (ha solo
classi base) e poi, man mano che se ne presenta lesigenza, pu essere esteso
attraverso la creazione di sottomodelli sempre pi specializzati (ha anche classi
derivate).
Il polimorfismo un meccanismo attraverso il quale si pu scrivere codice in
modo generico ed estendibile grazie al potente concetto che una classe base pu
riferirsi a tutte le sue classi derivate cambiando, di fatto, la sua forma. Ci si
traduce, in pratica, nella possibilit di assegnare a una variabile A (istanza di una
classe base) il riferimento di una variabile B (istanza di una classe derivata da A)
e, successivamente, riassegnare alla stessa variabile A il riferimento di una
variabile C (istanza di unaltra classe derivata da A). La caratteristica appena
indicata ci consentir, attraverso il riferimento A, di invocare i metodi di A che B o
C hanno ridefinito in modo specialistico, con la garanzia che il sistema run-time
di Java sapr sempre a quale esatta classe derivata appartengono.

TERMINOLOGIA
La discriminazione automatica, effettuata dal sistema run-time di Java, di quale oggetto (istanza
di una classe derivata) contenuto in una variabile (istanza di una classe base) effettuata con
un meccanismo definito dynamic binding (binding dinamico).
Elementi di un ambiente Java
Per poter programmare in Java necessario scaricare il JDK (Java Development
Kit) e installarlo sulla propria piattaforma (si rimanda allAppendice A per
spiegazioni pi dettagliate). Dopo linstallazione del JDK avremo a disposizione tutti
gli strumenti per compilare ed eseguire i programmi creati, incluse svariate librerie di
classi dette API (Application Programming Interface).
Nella Figura 1.1 riportato un esempio di struttura di directory e file creata da
uninstallazione tipica (sono elencate, per motivi di spazio, tutte le directory e solo un
file).

Figura 1.1 Struttura ad albero del JDK 1.8 a 32 bit creata in un sistema Windows.

La Figura 1.1 evidenzia:

una cartella di nome bin che contiene tutti i file eseguibili dei tool di sviluppo di
Java come javac.exe (il compilatore del codice sorgente Java), java.exe
(linterprete ed esecutore di un programma Java), jar.exe (il gestore per file di
archivi basati sul formato Java ARchive o JAR), javap.exe (il disassemblatore di
file .class) e cos via;
una cartella di nome db che contiene Java DB, un sistema per la gestione di basi
di dati relazionali realizzato da Oracle e basato sul software open source Apache
Derby che anchesso un sistema per la gestione di database ma realizzato in
seno allApache Software Foundation. In dettaglio troviamo le directory: bin, al
cui interno sono presenti file di script per la configurazione dellambiente di
utilizzo del server (modalit embedded o client/server), per lavvio e per
larresto del server e cos via; lib, che contiene file di archivio .jar come
derby.jar (lengine library), derbyclient.jar (la network client library, necessaria
per utilizzare il network client driver), derbynet.jar (la network server library,
necessaria per avviare il network server) e cos via;
una cartella di nome include che contiene file header in linguaggio C per la
programmazione in codice nativo mediante lutilizzo della Java Native Interface
(JNI) e della Java Virtual Machine Tools Interface (JVMTI);
una cartella di nome lib che contiene librerie di classi e altri file utilizzati dagli
eseguibili di sviluppo (per esempio tools.jar e dt.jar);
una cartella di nome jre che contiene unimplementazione di un ambiente di run-
time di Java (JRE, Java Runtime Environment) utilizzato dal JDK. In breve un
JRE include una virtual machine (JVM, Java Virtual Machine), librerie di classi
e altri file necessari per lesecuzione dei programmi scritti con il linguaggio
Java;
un file di archivio basato sul formato ZIP di nome src.zip che contiene il codice
sorgente di tutte quelli classi che rappresentano il core delle API di Java (per
esempio quelle che si trovano nei package java.*, javax.* e cos via);
un file di nome release che contiene informazioni di servizio varie codificate
come coppie di chiave/valore (per esempio, JAVA_VERSION="1.8.0", OS_NAME="Windows",
OS_VERSION="5.1" e cos via).

TERMINOLOGIA
Con il termine codice nativo si intende codice che compilato per una specifica
piattaforma/sistema operativo, come il codice C. Il codice Java, invece, nel momento in cui viene
compilato produce un codice in linguaggio intermedio (detto bytecode) che interpretabile da
una qualsiasi macchina virtuale Java e quindi risulta indipendente dalla specifica piattaforma.
Il primo programma Java
Vediamo, attraverso la disamina del Listato 1.1, quali sono gli elementi basilari per
scrivere un programma in Java.
Listato 1.1 PrimoProgramma.

package com.pellegrinoprincipe;

/*
Primo programma in Java
*/
public class PrimoProgramma
{
private static int counter = 0;

public static void main(String[] args)


{
String testo = "Primo programma in Java:",
testo2 = " Buon divertimento!";

int a = 10, b = 20;

// stampa qualcosa
System.out.println(testo + testo2);

String s = "Stamper un test condizionale: " + "tra a=" + a + " e b=" + b;

System.out.println(s);

if (a < b)
{
System.out.println("a<b");
}
else
{
System.out.println("a>b");
}

s = "Stamper un ciclo iterativo, " + "dove legger per 10 volte il valore di a";

System.out.println(s);

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


{
System.out.print("Passo " + i);
System.out.println("--> " + "a=" + a);
}
}
}

Il Listato 1.1 inizia con listruzione package, che consente di creare librerie di tipi
correlati. Infatti la classe denominata PrimoProgramma, definita con la keyword class, un
nuovo tipo di dato che apparterr al package (o libreria) denominato
com.pellegrinoprincipe.

Successivamente abbiamo unistruzione di commento multiriga che consente di


scrivere, tra i caratteri di inizio commento /* e fine commento */, qualsiasi cosa che
possa servire a chiarire il codice. Listruzione di commento a singola riga prevede
lutilizzo dei caratteri //.
Le note esplicative scritte come commento possono contenere qualsiasi carattere,
poich saranno ignorate dal compilatore.
Dopo il commento multiriga abbiamo, come gi detto, la definizione della classe
denominata PrimoProgramma con la scrittura tra le parentesi graffe aperta { e chiusa } dei
suoi membri (dati e metodi). La nostra classe ha un dato membro denominato counter
e un metodo denominato main.

Il dato membro counter una variabile, ovvero una locazione di memoria che pu
contenere valori che possono cambiare nel tempo. A ogni variabile deve essere
associato un insieme di valori che pu contenere, tramite la specificazione di un tipo
di dato di appartenenza. La variabile counter ha, infatti, associato un tipo di dato
intero.
Il metodo main rappresenta una sorta di metodo di avvio (o di entry point) per la
nostra applicazione e deve essere sempre presente almeno in una classe, poich
invocato automaticamente dallinterprete java allatto dellesecuzione
dellapplicazione.
main preceduto dalle keyword descritte di seguito.

void indica che il metodo non restituisce alcun valore. Ovviamente un metodo
pu restituire un valore e in questo caso occorre indicarne il tipo di
appartenenza, per esempio int per la restituzione di un tipo intero.
static indica che si tratta di un metodo di classe che pu essere invocato senza la
creazione del relativo oggetto.
public indica che il metodo accessibile da client esterni alla classe dove il

metodo stesso stato definito.

Le keyword static e public permettono al metodo main di essere invocato


pubblicamente dallinterprete java, in quanto client esterno, e di avviarne il relativo
programma.
Seguono il main una serie di parentesi tonde allinterno delle quali posta una
variabile denominata args che rappresenta il parametro del metodo. Ogni metodo,
infatti, pu avere zero o pi parametri che rappresentano, se presenti, delle variabili
che saranno riempite con valori (detti argomenti) passati al metodo medesimo allatto
della sua invocazione. I parametri di un metodo devono avere, inoltre, un tipo di dato
associato; infatti, il parametro args dichiarato come di tipo array di stringhe
(String[]).
Il parametro args permette al metodo main di ottenere in input gli argomenti
eventualmente passati quando si invoca dalla riga di comando il programma che lo
contiene.
Allinterno del metodo main sono scritte delle istruzioni che nel loro complesso
rappresentano le operazioni che il metodo deve eseguire.
Troviamo infatti:

la dichiarazione di variabili locali come String testo, int a e cos via;


linvocazione di un metodo di stampa di dati su console (println);
lesecuzione di unistruzione di selezione doppia if/else che valuta se una data
espressione vera o falsa eseguendone, a seconda del risultato della valutazione,
il codice corrispondente (quello del ramo valutato vero oppure quello del ramo
valutato falso);
lesecuzione di unistruzione di iterazione for che consente di eseguire
ciclicamente una serie di istruzioni finch una data espressione vera.

Concludiamo con alcune indicazioni.

Ogni istruzione deve terminare con il carattere ; (punto e virgola).


Le parentesi graffe aperta { e chiusa } delimitano un blocco di codice contenente
delle istruzioni.
Il codice pu essere scritto secondo il proprio personale stile di indentazione
utilizzando i caratteri di spaziatura (spazio, tabulazione, Invio e cos via)
desiderati.
Compilazione ed esecuzione del codice
Dopo aver scritto, con un qualunque editor di testo, il programma del Listato 1.1,
vediamo come eseguirne la compilazione che, lo ricordiamo, un processo mediante
il quale il compilatore javac di Java legge un file sorgente (nel nostro caso
PrimoProgramma.java ) per trasformarlo in un file (PrimoProgramma.class) che conterr
istruzioni scritte in un linguaggio intermedio detto bytecode.
Shell 1.1 Invocazione del comando di compilazione (Windows).

javac -d c:\MY_JAVA_CLASSES PrimoProgramma.java

Shell 1.2 Invocazione del comando di compilazione (GNU/Linux).

javac -d /opt/MY_JAVA_CLASSES PrimoProgramma.java

Dopo la fase di compilazione segue la fase di esecuzione, nella quale un file .class
(nel nostro caso PrimoProgramma.class) viene letto dallinterprete java per convertire il
bytecode in esso contenuto in codice nativo del sistema dove eseguire il programma
stesso.
Shell 1.3 Invocazione dellinterprete java che esegue il programma.

java com.pellegrinoprincipe.PrimoProgramma

Output 1.1 Esecuzione di Shell 1.3.


Primo programma in Java: Buon divertimento!
Stamper un test condizionale: tra a=10 e b=20
a<b
Stamper un ciclo iterativo, dove legger per 10 volte il valore di a
Passo 0--> a=10
Passo 1--> a=10
Passo 2--> a=10
Passo 3--> a=10
Passo 4--> a=10
Passo 5--> a=10
Passo 6--> a=10
Passo 7--> a=10
Passo 8--> a=10
Passo 9--> a=10

Dalla Shell 1.3 vediamo che il comando java esegue il programma PrimoProgramma che
stampa quanto mostrato nellOutput 1.1.
utile sottolineare alcuni aspetti.

Il nome del programma PrimoProgramma il nome della classe contenuta nel file
omonimo.
Il nome del file PrimoProgramma.class contenente il programma da eseguire viene
passato al comando java senza lindicazione dellestensione .class.
La classe PrimoProgramma invocata preceduta dal nome del package di
appartenenza com.pellegrinoprincipe, poich quando una classe appartiene a un
package, il suo nome deve sempre far parte di quel package.
Problemi di compilazione ed esecuzione?
Elenchiamo alcuni problemi che si potrebbero incontrare durante la fase di
compilazione o di esecuzione del programma appena esaminato.

I comandi javac o java sono inesistenti? Verificare che il path del sistema
operativo contenga la directory bin del JDK tra i percorsi di risoluzione. Si
rimanda allAppendice A per i dettagli su come impostare correttamente il path.
Il compilatore javac non trova il file PrimoProgramma.java? Verificare che la directory
corrente sia c:\MY_JAVA_SOURCES (per Windows) oppure /opt/MY_JAVA_SOURCES (per
GNU/Linux).
Linterprete java non trova il file PrimoProgramma? Verificare che la directory
corrente sia c:\MY_JAVA_CLASSES (per Windows) oppure /opt/MY_JAVA_CLASSES (per
GNU/Linux).
Capitolo 2
Variabili, costanti, letterali e tipi

Una variabile rappresenta uno spazio di memoria alterabile dove vengono


memorizzati dei valori. Prima di poter utilizzare tale variabile, ovvero prima di
poterle assegnare (o leggere) un valore, necessario dichiararne il tipo, cio
determinare che specie di dato essa potr gestire. Ci necessario poich Java un
linguaggio di programmazione strongly typed, ovvero fortemente tipizzato.
TERMINOLOGIA
Oltre ai linguaggi fortemente tipizzati (strongly typed), esistono anche linguaggi debolmente
tipizzati (weakly typed o loosely typed) in cui le variabili non sono dichiarate con un tipo
predefinito e nelle stesse, in tempi successivi, possono essere contenuti valori di tipo diverso:
oggetti, stringhe, numeri e cos via. Tra questi ultimi linguaggi vi sono, solo per citarne i pi
comuni, JavaScript, PHP e Python.

Il compilatore allatto della compilazione effettuer un controllo rigoroso di come


le variabili sono utilizzate nelle espressioni, nei metodi e negli assegnamenti,
verificando se queste operazioni rispettano i tipi in esse dichiarati non generando
conversioni in conflitto.
Dopo aver dichiarato il tipo, si deve scrivere un identificatore, ovvero un nome
simbolico con cui referenziare la variabile per il suo utilizzo. Tale identificatore pu
essere scritto utilizzando qualsiasi combinazione di lettere minuscole, maiuscole,
numeri, caratteri di dollaro, di sottolineatura, di euro, di sterlina, ma non pu essere
composto da pi parole separate da spazi e non pu iniziare con un numero e con
caratteri quali /, % e in genere con caratteri che hanno a che fare con la sintassi del
linguaggio (si pensi alle parentesi ( ), ai simboli di relazione > < e cos via). Inoltre gli
identificatori sono case-sensitive, nel senso che si fa distinzione tra lettere minuscole
e lettere maiuscole.

NAMING CONVENTION
P e r naming convention si intende la regola di scrittura utilizzata per la denominazione degli
elementi di un programma. Nel linguaggio Java le classi si dovrebbero scrivere usando la
notazione UpperCamelCase (Pascal Case) in cui lidentificatore, se formato da pi parole, deve
essere scritto tutto unito e ogni parola deve iniziare con la lettera maiuscola (per esempio
UsoDiChar), mentre le variabili e i metodi si dovrebbero scrivere utilizzando la notazione definita
lowerCamelCase in cui, se lidentificatore formato da pi parole, deve essere scritto tutto unito, e
la prima parola deve iniziare con la minuscola mentre le altre con la maiuscola (per esempio myStr,
myMethod).

Snippet 2.1 Alcuni identificatori.


int number_1; // CORRETTO
int number 1; // ERRORE - ';' expected
int 1number; // ERRORE - not a statement

// a e A sono variabili DIVERSE!!!


int a;
int A;

Quando una variabile viene scritta nella forma dello Snippet 2.1 si dice che essa
dichiarata. Con tale dichiarazione, di fatto, si comunica al compilatore di allocare per
lidentificatore un determinato spazio di memoria relativo al suo tipo.
La dichiarazione pu essere accompagnata da unoperazione di inizializzazione
(effettuata utilizzando loperatore di assegnamento =), cio dalla scrittura di un valore
nella variabile, ricordando che tale valore deve essere dello stesso tipo della variabile
in questione.
Ogni dichiarazione/inizializzazione pu essere effettuata su pi variabili in una
sola riga utilizzando il simbolo di virgola (,) oppure scrivendo prima la dichiarazione
e poi linizializzazione.
Snippet 2.2 Alcune dichiarazioni e inizializzazioni.

// dichiarazione e inizializzazione di variabili primitive


int nr1 = 44, nr2 = 55;

// dichiarazione e inizializzazione di variabili riferimento


String my_str = new String("Java is a great programming language!!!");

// dichiarazione
float fl1, fl2;
// inizializzazione
fl1 = 33.33f;
fl2 = 44.44f;

Le inizializzazioni dello Snippet 2.2 sono tutte effettuate con valori definiti
letterali (tranne quella della variabile my_str).

Una variabile pu ottenere un valore anche in modo dinamico, ovvero mediante la


valutazione di unespressione.
Snippet 2.3 Valore dinamico.
double db = Math.sqrt(44.44);

Nello Snippet 2.3 la variabile db di tipo double otterr un valore che il risultato del
calcolo della radice quadrata di 44.44, dopo linvocazione del metodo sqrt della classe
Math.
Variabili primitive
Le variabili primitive sono variabili che contengono direttamente al loro interno un
valore (Figura 2.1) che pu derivare da uno dei seguenti tipi di dato:

boolean di 8 bit con un range di valori true o false;


char di 16 bit con un range di valori da \u0000 a \uFFFF come definiti dallo standard
ISO Unicode;
byte di 8 bit con un range di valori da -128 a +127;

short di 16 bit con un range di valori da 32.768 a +32.767;


int di 32 bit con un range di valori da 2.147.483.648 a +2.147.483.647;
long di 64 bit con un range di valori da 9.223.372.036.854.775.808 a
+9.223.372.036.854.775.807 ;
float di 32 bit con un range di valori da 1.4E-45 a +3.4028235E+38;
double di 64 bit con un range di valori da 4.9E-324 a +1.7976931348623157E+308.

Figura 2.1 Variabile primitiva denominata my_var, di tipo intero e contenente il valore 450.
APPROFONDIMENTO
I tipi di dato float e double sono stati progettati secondo lo standard internazionale IEEE 754
(IEEE Standard for Binary Floating-Point Arithmetic), il quale definisce delle regole per i sistemi di
computazione in virgola mobile, ovvero formalizza come devono essere rappresentati, quali
operazioni possono essere compiute, le conversioni operabili e come devono essere gestite le
condizioni di eccezione come, per esempio, la divisione per 0. I formati esistenti sono: a
precisione singola (32 bit), a precisione singola estesa (>= 43 bit), a precisione doppia (64 bit) e a
precisione doppia estesa (>= 79 bit).

Come si nota dallelenco, i tipi, tranne char, sono con segno, cio accettano sia
valori negativi sia positivi, e non possiamo modificare tale impostazione rendendoli
unsigned.

In Java, inoltre, la dimensione in byte stabilita per i tipi di dato fissa e non varia
se la macchina virtuale installata su sistemi operativi o processori differenti.
I tipi byte, short e int sono utilizzati per eseguire calcoli con valori senza parte
frazionaria, mentre i tipi float e double possono essere usati per calcoli con
componente frazionaria a precisione singola o doppia. Il tipo char utile per
memorizzare un singolo carattere e occupa 16 bit; in grado di contenere caratteri in
tutte le lingue del mondo come stabilito dallo standard Unicode. Il range va da 0 a
65535 , dove da 0 a 127 sono contenuti i caratteri del set ASCII.

UNICODE
Unicode un sistema di codifica universale per i caratteri (sviluppato e mantenuto da
unorganizzazione non-profit denominata Unicode Consortium), indipendente dal sistema
informatico e dalla lingua in uso, che assegna a ciascun carattere un valore numerico. Il sistema
nasce con lobiettivo di rappresentare i caratteri di tutte le lingue del mondo (anche di quelle
antiche), i simboli scientifici, gli ideogrammi e cos via. Nella prima versione di Unicode, dal 1991 al
1995, la codifica dei caratteri era a 16 bit, con cui si potevano codificare fino a 65.536 caratteri, ma
successivamente, con la versione 2.0 del 1996 la codifica pass a 21 bit con la possibilit di
rappresentare circa 2 milioni di caratteri. Dopo di allora vi sono state altre versioni dello standard
che lo hanno migliorato sia dal punto di vista formale (per esempio attraverso il cambiamento di
definizioni terminologiche poco chiare delle versioni precedenti) sia da quello pi pratico grazie
allaggiunta, via via, di ulteriori caratteri (per esempio per la lingua etiopica, cherokee e cos via).
Attualmente Unicode giunto alla versione 6.3, mentre la corrente versione di Java ne supporta la
versione 6.2.

Con le variabili del tipo char, visto che in esso si possono scrivere o leggere valori
numerici a cui sono associati i relativi simboli dei caratteri, lecito compiere
unoperazione come spostarsi al carattere successivo, semplicemente incrementando
di uno il suo valore (Listato 2.1).
Listato 2.1 Classe UsoDiChar.
package com.pellegrinoprincipe;

public class UsoDiChar


{
public static void main(String[] args)
{
char ch = 82;
System.out.println(ch); // stampa R
ch++; // sposta al carattere successivo
System.out.println(ch); // stampa S
}
}

Output 2.1 Dal Listato 2.1 Classe UsoDiChar.


R
S

Il tipo boolean pu contenere solo valori true o false, cio valori che derivano da
valutazioni logiche di verit o falsit. Unespressione ritorna in automatico il valore
true o false a seconda che sia vera o falsa.

Snippet 2.4 Valutazione di unespressione booleana.

int a = 82, b = 90;


boolean c = a > b;
Nello Snippet 2.4 lespressione a > b ritorner un valore false che sar memorizzato
nella variabile c di tipo boolean.
Variabili riferimento
Le variabili riferimento contengono al loro interno un valore che un riferimento
(un puntamento) a unarea di memoria dove stato allocato un tipo di dato astratto e
complesso (Figura 2.2). Questo tipo di dato un oggetto che rappresenta unistanza
della sua classe di definizione. Ci significa, per esempio, che la variabile riferimento
my_str dello Snippet 2.2 non conterr direttamente al suo interno il valore "Java is a

great programming language!!!" , bens conterr un valore che un riferimento a unarea di


memoria dove sar stato allocato un oggetto del tipo della classe String attraverso il
cui stesso riferimento potr poi manipolarne i dati.

Figura 2.2 Variabile riferimento denominata my_str, di tipo String.

CHE COSA SONO ESATTAMENTE I RIFERIMENTI?


Per meglio comprendere il concetto di riferimento a unarea di memoria, cerchiamo di rispondere
alla seguente domanda: tale concetto di riferimento assimilabile direttamente quello di puntatore
presente in altri linguaggi come il C/C++, ovvero i tipi riferimento contengono al loro interno un
semplice indirizzo di memoria dove stato allocato loggetto che riferiscono? Se intendiamo come
puntatore una variabile che contiene come valore un dato che rappresenta ma non un indirizzo
di memoria dove stato allocato un determinato oggetto, allora la risposta affermativa.
Infatti, in Java il dato contenuto nella variabile non esattamente il valore dellindirizzo di memoria,
ma un valore che lo rappresenta.

Listato 2.2 Classe Riferimenti.

package com.pellegrinoprincipe;

class T
{
}

public class Riferimenti


{
public static void main(String[] args)
{
int x[] = new int[2];
System.out.println("Valore riferimento dell'array x: " + x);
T t = new T();
System.out.println("Valore riferimento dell'oggetto t: " + t);
}
}

Output 2.2 Dal Listato 2.2 Classe Riferimenti.


Valore riferimento dell'array x: [I@15fbaa4
Valore riferimento dell'oggetto t: com.pellegrinoprincipe.T@1ee12a7

LOutput 2.2 mostra chiaramente che il sistema stampa per la variabile x e per la
variabile t un valore che ha una sintassi particolare. Infatti, il valore di un riferimento
costituito da due parti: la prima parte, a sinistra del simbolo @, sta a indicare il tipo
di oggetto, mentre la seconda parte, a destra del medesimo simbolo, sta a indicare un
valore esadecimale che un hash code delleffettivo indirizzo di memoria
delloggetto (per hash code si intende un valore che deriva da un altro valore,
trasformato da una funzione di hashing).
Nel nostro caso, per la variabile x la prima parte [I indica che essa un array di
tipo intero, mentre la seconda parte d come hash code il valore 15fbaa4; per la
variabile t la prima parte indica che essa un oggetto di tipo T appartenente al
package com.pellegrinoprincipe, mentre la seconda parte d come hash code il valore
1ee12a7 .
ATTENZIONE
La corrispondenza tra lindirizzo di memoria delloggetto puntato e un valore derivato da una
funzione di hashing non garantita in tutte le implementazioni delle macchine virtuali Java,
poich limplementazione di tale uguaglianza non un requisito obbligatorio richiesto dalle
specifiche del linguaggio. Ci significa che in alcune implementazioni il valore potrebbe essere un
riferimento che non necessariamente una rappresentazione di un mero indirizzo di memoria.

Possiamo pertanto affermare che i riferimenti sono come i puntatori ma non sono
esattamente la stessa cosa, e ci sia per la spiegazione appena riportata, sia perch
essi:

non possono essere deallocati direttamente; infatti, la loro deallocazione


demandata a un componente software della virtual machine denominato garbage
collector che provvede autonomamente a deallocare un oggetto che non ha pi
nessuna variabile che lo riferisce;
non possono essere manipolati direttamente per svolgere operazioni di
aritmetica dei puntatori (come invece possibile fare in C/C++) e per svolgere
operazioni di inizializzazione con valori di indirizzo arbitrari.
Snippet 2.5 Aritmetica dei puntatori non permessa.

int x[] = {1,2,3};


x++; // ERRORE - bad operand type int[] for unary operator '++'

Lo Snippet 2.5 evidenzia come non sia possibile far spostare in avanti di ununit il
riferimento della variabile x.
Snippet 2.6 Inizializzazione con valori di indirizzo arbitrari.
int x[] = new int[2];
int y[] = null;
int z[] = 1b67f74; // ERRORE - incompatible types: int cannot be converted to int[]

Lo Snippet 2.6 evidenzia che quando si crea un riferimento non lo si pu


inizializzare con valori arbitrari; lo si potr inizializzare con lindirizzo delloggetto
contenente i dati a cui riferisce oppure con il valore speciale null.
Variabili locali, globali e scope
In Java una variabile pu essere definita come locale o come globale. Anche se
tale definizione non perfetta in un ambiente interamente ad oggetti, la utilizzeremo
per permettere a chi ha gi esperienza con un linguaggio procedurale come il C di
avere un rapido confronto concettuale.
Una variabile locale quando il suo identificatore usabile solo allinterno del
blocco di codice ove dichiarata. Un parametro di un metodo, una variabile
dichiarata allinterno di un metodo e una variabile dichiarata allinterno di un blocco
di codice sono tutti esempi di variabili locali. Le propriet di una classe, definite
variabili di istanza, sono variabili globali e sono utilizzabili dovunque allinterno
della classe medesima. Quando si dichiara una variabile di istanza che ha un
identificatore uguale a quello di una variabile locale a un metodo, allora la prima sar
nascosta, cio il metodo user quella a esso locale (quando studieremo le classi
vedremo come sia comunque possibile accedere alla variabile di istanza).
Possiamo dire, pertanto, che in Java le variabili locali hanno uno scope (o ambito
di utilizzo) relativo al blocco di codice ove sono state dichiarate, mentre le variabili
globali hanno uno scope di classe.
NOTA
Lo scope globale in Java comunque confinato allinterno della classe, e questo riduce le
problematiche di accesso e manipolazione di dati globali che sono invece presenti in linguaggi
procedurali come C, dove, appunto, lo scope globale si estende a tutto il programma.

La dichiarazione della variabile pu avvenire ovunque allinterno di un blocco di


codice, il quale rappresentato, ripetiamo, da un gruppo di istruzioni poste tra le
parentesi graffe di apertura { e chiusura }, e dopo tale dichiarazione la variabile
medesima pu essere utilizzata.
Se si creano blocchi annidati, la variabile del blocco pi esterno visibile
allinterno del blocco interno, ma non vale il contrario.
Listato 2.3 Classe BlocchiAnnidati.
package com.pellegrinoprincipe;

public class BlocchiAnnidati


{
public static void main(String[] args)
{
int x = 20;
if (x < 20)
{
int y = 11;
System.out.println("X " + x);
}
System.out.println("Y " + y); // errore 'y' non visibile
}
}

Errore 2.1 Dal Listato 2.3 Classe BlocchiAnnidati.

com\pellegrinoprincipe\BlocchiAnnidati.java:13: error: cannot find symbol


System.out.println("Y " + y); // errore 'y' non visibile
symbol: variable y location: class BlocchiAnnidati 1 error

Il Listato 2.3 evidenzia che la variabile x dichiarata nel blocco del main visibile nel
blocco interno (annidato) rappresentato dalle istruzioni poste tra le parentesi graffe
del costrutto if, mentre la variabile y, dichiarata allinterno del blocco if, non
visibile nel blocco esterno del main, poich alla chiusura del blocco dellif cessa di
esistere.
importante rilevare, infine, che se dichiariamo una stessa variabile in blocchi
annidati avremo un errore di duplicazione di una variabile locale:
Snippet 2.7 Dichiarazione di uno stesso identificatore in blocchi annidati.

// dichiarazione stessa variabile


int x = 20;
// blocco di codice
{
int x = 11; // ERRORE - variable x is already defined
}
Costanti
Una costante rappresenta uno spazio di memoria in cui memorizzato un valore
che non pu essere pi alterato dopo che vi stato assegnato. In Java una costante si
dichiara utilizzando la keyword final.
Snippet 2.8 Dichiarazione di una costante.

final int a = 82;


a = 90; // ERRORE - cannot assign a value to final variable a

La dichiarazione di una costante impone delle regole relative a quando e se essa


debba essere inizializzata, e tali regole presentano delle differenze a seconda che la
costante sia locale a un metodo o globale di classe. Vedremo tali differenze quando
studieremo i metodi e le classi.
Letterali
Un letterale un valore che viene assegnato a una variabile e che il programma
non pu alterare. Pu essere intero se rappresentato da valori interi come 10, 100, 4567
e cos via.
Per assegnare un letterale di tipo intero a una variabile di tipo byte o short: basta
semplicemente scrivere il suo valore prestando per attenzione che non ecceda il
range di valori accettato.
Se invece si vuole scrivere un valore numerico di tipo long, si dovr scrivere anche
un suffisso, precisamente inserendo la lettera L subito dopo lultima cifra del valore.

Inoltre, un letterale numerico intero pu essere espresso in una base diversa da 10


come quella ottale, esadecimale o binaria, ponendo prima dei numeri rispettivamente
i prefissi 0, 0x o 0B (0b). Infine, le cifre che compongono un letterale numerico possono
essere, arbitrariamente, separate dal carattere underscore (_) al fine di rendere pi
leggibile il numero stesso.
Listato 2.4 Classe LetteraliNumerici.

package com.pellegrinoprincipe;

class LetteraliNumerici
{
public static void main(String[] args)
{
int d = 10_000_000; // decimale con separatore
int o = 010; // ottale
int x = 0x10; // esadecimale
int b = 0B0000_1111; // binario con separatore
long l = 435435435345345L; // valore long

System.out.println("d = " + d);


System.out.println("o = " + o);
System.out.println("x = " + x);
System.out.println("b = " + b);
System.out.println("l = " + l);
}
}

Output 2.3 Dal Listato 2.4 Classe LetteraliNumerici.

d = 10000000
o = 8
x = 16
b = 15
l = 435435435345345

importante sottolineare che il carattere di separazione tra le cifre di un numero


pu essere posto solo tra di esse e mai:

allinizio o alla fine di un numero (123_);


adiacente al punto decimale (3_.1415F);
prima del suffisso L o F (12.55_F);
in una posizione dove ci si aspetta di trovare una stringa numerica.

I letterali in virgola mobile rappresentano numeri decimali e possono essere


espressi sia in forma standard sia in forma scientifica. In forma standard basta
scrivere il numero separato dalla parte frazionaria con un punto, per esempio 10.44,
mentre in forma scientifica si scrive come nella forma standard con laggiunta del
suffisso E e di un numero positivo o negativo che rappresenta una potenza di 10 per
cui il numero deve essere moltiplicato.
Tali letterali sono di tipo double, perci per assegnarli a una variabile di tipo float
bisogna aggiungere il suffisso F al numero rappresentato.
Snippet 2.9 Letterali numerici in virgola mobile.

// forma standard
double db = 45.1453; // letterale double
float fl = 45.3345F; // letterale float

// forma scientifica
double dbsp = 123.12E+4; // 123.12 * 10 ^ 4 ossia 1231200
double dbsn = 123.12E-4; // 123.12 * 10 ^ -4 ossia 0.012312

Un letterale booleano, invece, un valore che pu essere solo true o false e non
convertibile in nessun controvalore numerico.
Un letterale carattere un valore che rappresenta un carattere secondo il set di
caratteri aderenti allo standard Unicode. Si scrive tra singoli apici ed convertibile in
un valore intero. Tali letterali possono essere espressi anche in una forma ottale o
esadecimale, scrivendo tra gli apici la sequenza \ddd per lottale e la sequenza \udddd
per lesadecimale.
Snippet 2.10 Letterali carattere.
// tutte rappresentazioni del carattere j
char ch_d = 106; // decimale
char ch_x = '\u006A'; // esadecimale
char ch_o = '\152'; // ottale

Infine, i letterali stringa sono valori scritti tra virgolette come "Sono un letterale".
Snippet 2.11 Letterali stringa.

// stampa le lettere j k l tra virgolette e separate da TAB


String str = "\"\u006A\t\u006B\t\u006C\""; // "j k l"

Allinterno dei letterali stringa o carattere, come mostrato dai precedenti snippet,
possiamo utilizzare lo speciale carattere backslash, con simbolo \, definito carattere
di escape, che consente di immettere sia caratteri speciali non inseribili dalla tastiera
(di cui alcuni non saranno visualizzati in output, per esempio, il ritorno a capo), sia i
caratteri propri della definizione del letterale stesso. Utilizzando questo carattere
insieme a uno dei caratteri indicati di seguito si forma una sequenza di escape:

' per lapice;


" per le virgolette doppie;
\ per il backslash;
r per il ritorno a capo;
n per lavanzamento di riga;
f per lavanzamento di pagina;
t per il Tab;
b per il backspace.

ATTENZIONE
I letterali stringa devono essere scritti su ununica riga senza separazione, poich non esiste un
carattere di escape di continuazione di riga.
Snippet 2.12 Letterali stringa separati in una nuova riga.
// ERRORE - unclosed string literal
String str = "Sono una
stringa";
Conversione tra tipi
Quando si lavora con tipi di dato diversi pu capitare di dover assegnare a una
variabile di un tipo un valore di unaltra variabile di un tipo diverso. Se i tipi sono
compatibili, Java effettuer una conversione automatica (implicita) con ampliamento
dei valori. Tale conversione attuata, tuttavia, solo se il tipo finale pi grande del
tipo iniziale. Essa sempre lecita fra tipi interi (int, byte, short e long), decimali (double
e float) e char, ma non con i tipi boolean.

Se invece i tipi sono incompatibili, si pu provare comunque ad assegnare il valore


incompatibile effettuando unoperazione di conversione esplicita con riduzione del
valore tramite un operatore definito di cast che ha la sintassi che segue:
Sintassi 2.1 Cast.

(tipo-finale) valore

dove tipo-finale il tipo di dato in cui si vuole convertire valore.


Nellattuare la conversione possono verificarsi i seguenti casi:

se si forza lassegnamento di una variabile contenente un valore decimale a una


variabile di tipo intero, si avr un troncamento della sua parte frazionaria;
se si assegna un valore di una variabile che pi grande del valore massimo
contenibile nellaltra variabile, sar assegnato un valore (detto modulo) che
rappresenta il resto della divisione tra i due valori.

Listato 2.5 Classe ConversioneTipi.

package com.pellegrinoprincipe;

public class ConversioneTipi


{
public static void main(String[] args)
{
int a = 260;
double d = 323.123;
byte b;

// il risultato sar infatti 260 % 256 che dar come resto 4


System.out.println("b = (byte) a ---> " + (b = (byte) a));

// il risultato sar 67 infatti prima 323.123 sar troncato in 323


// e poi si far 323 % 256
// che dar come resto appunto 67
System.out.println("b = (double) d ---> " + (b = (byte) d));
}
}

Output 2.4 Dal Listato 2.5 Classe ConversioneTipi.

b = (byte) a ---> 4
b = (double) d ---> 67
DallOutput 2.4 si evidenzia come lespressione (b = (byte) a) d come risultato 4, il
resto della divisione tra 260, che rappresenta il valore contenuto nella variabile a, e 256,
che rappresenta il massimo valore assegnabile alla variabile b (poich ricordiamo che
in un tipo byte il range di valori assegnabile va da 0 a 255). Lespressione (b = (byte) d)
invece d come risultato 67, poich prima il valore della variabile d (323.123) viene
troncato della sua parte decimale (diventando 323) e poi viene generato il modulo tra
323 e 256, per le stesse ragioni viste per la prima espressione.

Infine, in espressioni con valori diversi da un tipo int (per esempio valori di tipo
byte o short) Java convertir automaticamente gli operandi nel tipo int, e se tale valore
deve essere assegnato a una variabile, la stessa dovr essere sempre di tipo int; se di
tipo differente, allora il valore dovr essere convertito verso tale tipo con un cast
specifico.
Snippet 2.13 Operandi promossi in int.

byte b = 2;
int i;
short s = 111;

// ok valido b e s sono stati convertiti in int e possono essere assegnati


// a i che di tipo int
i = b * s;
// non valido poich anche se b di tipo byte e il valore nel suo range,
// gli operandi b e s sono stati convertiti direttamente in int
// b = b + s; // ERRORE - incompatible types: possible lossy conversion from int to byte
// ... quindi si deve prevedere uno specifico cast
b = (byte) (b + s); // CORRETTO

Se, invece, un operando di tipo long, allora tutta lespressione sar long, se un
operando di tipo float allora tutta lespressione sar float e se un operando di tipo
double allora tutta lespressione sar double.
Snippet 2.14 Espressione con operandi di differente tipo.
byte b = 111;
char c = 'd';
short s = 444;
int i = 2131;
long l = 2112;
float f = 5.6f;
double d = 3322.11;
double ris = (c*i) + (f*b) - (d/s) / l;

Nello Snippet 2.14 lintera espressione sar valutata ed eseguita come segue:

1. (c*i) sar convertito in un int con il valore 213100.


2. (f*b) sar convertito in un float con il valore 621.6.
3. (d/s) sar convertito in un double con il valore 7.48222972972973.
4. il punto 3) / l sempre un double con il valore 0.00354272240990991.
5. il punto 1) + il punto 2) d un float con il valore 213721.6.
6. il punto 5) il punto 4) d un double con il valore 213721.5902072776.
7. sar uguale a un valore double ovvero quello di cui il punto 6.
ris

In conclusione, per valutare se unespressione genera un valore dello stesso tipo


della variabile di destinazione bisogna sempre guardare al tipo degli operandi che ha
il massimo range di valori rappresentabili.
Ci significa, dunque, che se unespressione ha un operando di tipo double e altri
operandi di tipo differente, lintera espressione dar sempre il valore convertito in
double, perch un double sicuramente potr contenere valori interi (long, int, short e byte)

e valori float.
Capitolo 3
Array

Un array una variabile che contiene un riferimento a unarea di memoria al cui


interno vi sono dei dati del suo stesso tipo. pertanto definibile come una sorta di
struttura omogenea contenente un insieme di variabili a cui ci si riferisce come
ununica e indivisibile entit. Inoltre ha natura statica, poich mantiene la propria
dimensione il numero di elementi che contiene dallinizio alla fine del
programma (anche se a un array si pu passare un riferimento di un altro array
cambiandogli, di fatto, la dimensione).
Array monodimensionali
Un array monodimensionale, definito anche vettore, una struttura dati composta
da una serie di variabili disposte in una singola riga o colonna, e si dichiara
utilizzando la sintassi che segue.
Sintassi 3.1 Dichiarazione di un array.

type variable_name[]
type[] another_variable_name

dove type indica il tipo di dato che avranno gli elementi costituenti larray, mentre
le parentesi quadre [] (dette operatore di subscript), poste indifferentemente o dopo
lidentificatore o dopo il tipo, sono obbligatorie e stanno a indicare che la variabile
sar un array del tipo stabilito.

QUALE SINTASSI SCEGLIERE


La scelta della sintassi per la dichiarazione di un array assume una certa importanza quando si
dichiarano in successione pi variabili dello stesso tipo, poich, se non si presta attenzione si pu
incorrere in errori di semantica. Per esempio, quando scriviamo unistruzione come int[] a, b,
c[], la variabile a e la variabile b sono array di interi, mentre la variabile c un array di array. Se
invece scriviamo int a, b[], c, la variabile a e la variabile c sono normali variabili primitive di
interi, mentre la variabile b un array di interi. In pratica, ponendo loperatore di subscript subito
dopo il nome del tipo si indica che tutte le variabili dichiarate saranno degli array di quel tipo,
mentre il contrario non sar mai vero.

Linizializzazione di un array si attua utilizzando la sintassi che segue.


Sintassi 3.2 Inizializzazione di un array.

variable_name = new type[nr_of_elements]

dove variable_name una variabile di tipo array gi dichiarata, type il suo tipo e
nr_of_elements rappresenta il numero di elementi che essa conterr. La keyword new
in questo contesto obbligatoria.
In concreto, un array si crea dichiarandolo e inizializzandolo nel modo seguente.
Snippet 3.1 Creazione e inizializzazione di un array.
int c[]; // dichiaro
c = new int[10]; // inizializzo e alloco memoria

Analizzando lo Snippet 3.1 vediamo che:

nella fase di dichiarazione comunichiamo al compilatore che la variabile c di


tipo array e gli elementi costituenti saranno di tipo intero;
nella fase di inizializzazione comunichiamo al compilatore che per la variabile c
dovr creare (allocare) unarea di memoria capace di contenere 10 valori di tipo
intero (Figura 3.1). Lallocazione ottenuta utilizzando loperatore new che, in
sostanza, crea un nuovo oggetto di tipo array passandone il riferimento alla
variabile c. Larray peser in memoria 40 byte, valore che si ottiene
moltiplicando il numero degli elementi (10) per lo spazio occupato dal tipo di
dato (un int occupa 4 byte). Quando si crea un array, il compilatore inizializza
automaticamente gli elementi con un valore che per i tipi numerici 0, per i
riferimenti null, per i booleani false e per i char il valore Unicode \u0000.

Snippet 3.2 Creazione di pi array.


int[] a, b, c;
a = new int[3];
b = new int[4];
c = new int[5];

Ogni elemento dellarray posizionato al suo interno secondo un indice numerico


che parte dal valore 0 e termina con il valore espresso tra le parentesi quadre meno il
valore 1. Gli array a, b e c di tipo intero dello Snippet 3.2 saranno strutturati come
nella Figura 3.2.

Figura 3.1 Array di 10 elementi di tipo intero.

CALCOLARE LA CORRETTA QUANTIT DI MEMORIA DI UN ARRAY


In realt il calcolo dello spazio in memoria occupato da un array pi complesso perch si deve
tener conto anche di alcuni byte supplementari che la virtual machine alloca e che sono dipendenti
dalle implementazioni. Infatti, in alcune implementazioni potrebbe essere allocato uno spazio
supplementare di 12 byte, definito object header, dove vengono poste alcune informazioni di
servizio per larray (ID, flag di status e cos via) e poi un eventuale spazio, definito di padding, se la
dimensione in memoria occorrente non un multiplo di 8. Nel caso del nostro array la dimensione
ricalcolata potrebbe essere di 56 byte, derivante dalla somma di: 12 (object header) + 4 * 10
(elementi di tipo int) + 4 (padding).

Figura 3.2 Array a di 3 elementi, b di 4 elementi e c di 5 elementi.

Per ottenere un particolare elemento, o per scrivere in esso, si user il nome della
variabile che riferisce larray e loperatore di subscript con un valore numerico (che
pu essere anche il risultato di unespressione) che ne rappresenta lindice.
Snippet 3.3 Accesso e scrittura di un elemento di un array.
int[] c = new int[10];
int u = 2, z = 4;
c[1] = 333; // scrivo alla posizione con indice 2
int x = c[u + z]; // prendo dalla posizione con indice 6
c[10] = 1000; // ERRORE - ArrayIndexOutOfBoundsException

Lo Snippet 3.3 evidenzia come listruzione c[10] = 1000 generi uneccezione


software che ci dice che stiamo cercando di scrivere un valore in un elemento al di
fuori dellindice massimo dellarray. Infatti, come si detto, la numerazione
dellindice per un array parte dal valore 0 e pertanto il calcolo dellultimo indice
pari al numero di elementi scritti in fase di creazione dellarray meno uno (1). Cos
nel nostro caso gli indici referenziabili andranno dal valore 0 per lelemento numero
1 al valore 9 per lelemento numero 10.
Un oggetto di tipo array conosce sempre il numero di elementi grazie alla propriet
length del suo riferimento.

Snippet 3.4 Propriet length.

int[] b = new int[78];


int l = b.length; // l = 78

Gli elementi di un array si possono indicare anche allatto dellinizializzazione


dellarray medesimo.
Sintassi 3.3 Inizializzazione inline di un array.
type variable_name[] = {value, , value}
Qui, dopo aver dichiarato una variabile di tipo array, ne inizializziamo direttamente
gli elementi scrivendone i valori tra parentesi graffe e separandoli con il carattere
virgola (,).
Snippet 3.5 Set di valori allatto dellinizializzazione dellarray.

int c[] = {1, 5, 6, 7};

Nello Snippet 3.5 la variabile c un array con quattro elementi i cui valori sono
cos indicati: c[0] ha valore 1; c[1] ha valore 5; c[2] ha valore 6; c[3] ha valore 7.

Notiamo inoltre che non occorre scrivere il numero di elementi dellarray, poich
lo stesso sar automaticamente determinato in base al numero di elementi posti tra le
parentesi graffe per linizializzazione.
Finora abbiamo visto come si creano array di valori numerici, ma possibile
creare array di diverso tipo, inclusi anche i tipi creati dal programmatore.
Snippet 3.6 Array di differente tipo.
class MyClass {};

boolean b[] = {false, false, true}; // array di booleani


short[] s = new short[4]; // array di short
byte[] by = {1, 3, 4}; // array di byte
long l[] = new long[b.length]; // array di long
float f[] = {12.44f, 678.12f}; // array di float
double d[] = {f[0], f[1], 12E4}; // array di double
String str[] = {"RED", new String("GREEN")}; // array di stringhe
char ac[] = {'h', 'e', 'l', 'l', 'o'}; // array di caratteri
Object[] obj = new Object[2]; // array di oggetti di tipo Object
MyClass[] mc = new MyClass[11]; // array di oggetti di tipo MyClass

Per quanto attiene alla creazione di array di tipo carattere doveroso precisare che,
mentre in altri linguaggi di programmazione come per esempio C o C++, un array di
caratteri di fatto una stringa, in Java tale equivalenza falsa, perch una stringa
un oggetto, mentre un array di caratteri un array in cui ogni elemento un carattere.
Snippet 3.7 Array di caratteri.

char c[] = "Stringa"; // ERRORE - String cannot be converted to char[]


char f[] = {'S', 't', 'r', 'i', 'n', 'g', 'a'}; // LECITO
String s = "Stringa"; // LECITO

Lo Snippet 3.7 evidenzia che larray c non pu contenere direttamente una stringa,
poich per il compilatore il letterale "Stringa" un oggetto di tipo stringa, mentre la
variabile c un oggetto di tipo array di caratteri.

In conclusione vediamo un semplice esempio (Listato 3.1) che crea un array di


caratteri contenente un nome e poi stampa i caratteri a video scorrendo, in una
struttura iterativa (for), i singoli elementi dellarray ove sono contenuti.
Listato 3.1 Classe ArrayMono.
package com.pellegrinoprincipe;

public class ArrayMono


{
public static void main(String[] args)
{
char name[] = {'P', 'e', 'l', 'l', 'e', 'g', 'r', 'i', 'n', 'o'}; // array di
// caratteri
int div = 4;
for (int i = 0; i < name.length; i++)
{
if (i <= div)
System.out.print(name[i] + " ");
else
System.out.print("\n" + name[i]);
}
System.out.println();
}
}

Output 3.1 Dal Listato 3.1 Classe ArrayMono.

P e l l e
g
r
i
n
o

DETTAGLIO
In questo esempio abbiamo utilizzato una struttura iterativa, creata con listruzione for, che
consente di effettuare ciclicamente le operazioni in essa contenute finch una determinata
condizione vera. Nel nostro caso la struttura iterativa stampa i caratteri contenuti nellarray name
finch il contatore i minore della lunghezza dellarray stesso. In pratica il ciclo eseguito 10
volte da 0 incluso a 9 incluso.
Array multidimensionali
Java consente di dichiarare e inizializzare array con pi di un indice, ovvero con
una dimensione maggiore di uno, al fine di astrarre in una struttura dati ad hoc
oggetti o concetti del mondo reale e non pi complessi; si pensi per esempio a una
scacchiera (ha otto righe e otto colonne e dunque due dimensioni); alla statistica del
numero di sviluppatori dei pi comuni linguaggi di programmazione censiti per
annualit (ha righe per indicare un determinato anno e colonne per indicare il nome
dei linguaggi e dunque due dimensioni); alla mappatura dei clienti di un albergo
(lalbergo ha dei piani, ogni piano dei corridoi, ogni corridoio delle stanze e dunque
tre dimensioni); alla rilevazione della velocit di un corpo data una posizione e un
tempo (ha le coordinate spaziali x, y e z e il tempo t e dunque quattro dimensioni).
Nella sostanza, comunque, gli array n-dimensionali pi utilizzati sono quelli a due
dimensioni, identificati con due coppie di parentesi quadre aperte/chiuse [][] e quelli
a tre dimensioni identificati con tre coppie di parantesi quadre aperte/chiuse [][][].
ATTENZIONE
Lutilizzo di array multidimensionali, soprattutto quelli con pi di due dimensioni, deve essere
ponderato con estrema cautela quando il numero di elementi per indice notevole, e ci per
evitare un impiego inutile ed eccessivo di memoria necessaria per la loro creazione completa (si
potrebbe, infatti, non avere alcuna necessit di utilizzare subito tutti gli elementi allocati). In
questo caso pu essere pi opportuno definire un nuovo tipo di dato (per esempio una classe
Velocity) che ha come propriet le dimensioni di interesse (per esempio le variabili x, y, z e t) e
poi creare un array dove gli elementi riferiscono solo agli oggetti sue istanze effettivamente
occorrenti.

Array bidimensionali
Un array bidimensionale, definito anche matrice, una struttura dati composta da
una serie di variabili disposte in forma tabellare, ovvero in righe e colonne, e si
dichiara utilizzando la sintassi che segue.
Sintassi 3.4 Dichiarazione di un array bidimensionale.

type variable_name[][]
type[][] another_variable_name

dove type indica il tipo di dato che avranno gli elementi costituenti larray, mentre
le doppie parentesi quadre [], poste indifferentemente dopo lidentificatore o dopo il
tipo, sono obbligatorie e stanno a indicare che la variabile sar un array
bidimensionale.
Linizializzazione avviene con la sintassi che segue.
Sintassi 3.5 Inizializzazione di un array bidimensionale.

variable_name = new type[number_of_rows][number_of_ columns];

dove si crea, tramite loperatore new, un oggetto di tipo array a due dimensioni del
tipo type, con un numero di righe indicato da number_of_rows e un numero di
colonne indicato da number_of_columns. In pratica, un array bidimensionale si crea
dichiarandolo e inizializzandolo come nel seguente Snippet 3.8, dove la variabile a
un array a due dimensioni formato da 2 righe e da 3 colonne.
Snippet 3.8 Array bidimensionale.

int a[][] = new int[2][3];

In Java gli array bidimensionali sono di fatto array di array, e non array
bidimensionali puri. Ci significa che ogni elemento della prima dimensione (riga) ha
come valore un riferimento a un altro array che rappresenta laltra dimensione
(colonna).
Quanto detto ha implicazioni sulla memoria allocata per gli array bidimensionali,
infatti loggetto riferito dalla variabile a occupa uno spazio in memoria di 72 byte
dato dalla somma del risultato del valore della prima dimensione (24 byte) pi il
risultato del valore della seconda dimensione (48 byte):

prima dimensione, ossia 12 (object header) + 4 * 2 (elementi di tipo array di int,


ossia object references) + 4 (padding);
seconda dimensione, ossia 12 (object header) + 4 * 3 (elementi di tipo int) + 12
(object header) + 4 * 3 (elementi di tipo int).

NOTA
Se un elemento di un array un object reference la memoria comunemente utilizzata per esso
sar pari a 4 byte (virtual machine per sistemi a 32 bit) o 8 byte (virtual machine per sistemi a 64
bit). In ogni caso, dato che non esiste alcuna specifica ufficiale che quantifichi espressamente
quanti byte allocare per i riferimenti, essi sono comunque dipendenti dalla particolare
implementazione della virtual machine in uso (per esempio, per HotSpot, che la virtual machine
ufficiale di Java sviluppata e mantenuta da Oracle, esiste il flag UseCompressedOops, che abilitato
a on di default dalla versione 7 del linguaggio per le virtual machine a 64 bit; questo consente di
comprimere per ragioni di efficienza e performance i puntatori ad oggetti in modo che sui
sistemi a 64 bit vengano comunque utilizzati 32 bit). Ricordiamo, infine, che per i tipi primitivi la
memoria allocata sar: boolean e byte, 1 byte; char e short, 2 byte; int e float, 4 byte; long e
double 8 byte.

La Figura 3.3 illustra (a sinistra) come rappresentabile la matrice creata dallo


Snippet 3.8 e come sarebbe rappresentata la stessa matrice (a destra) se vi fosse il
supporto diretto agli array bidimensionali puri dove, in questo caso, i valori degli
elementi sono posti direttamente nelle righe e colonne. Infatti, la riga 0 e la riga 1 non
conterranno alcun riferimento a un altro array, ma direttamente il valore 0.
TERMINOLOGIA
Gli array di array in gergo informatico sono chiamati jagged o ragged array (array irregolari) e si
contrappongono agli array bidimensionali puri, chiamati rectangular array (array rettangolari).

Figura 3.3 Array di array a confronto con un array bidimensionale puro.

Negli array bidimensionali laccesso agli elementi si effettua utilizzando la stessa


sintassi degli array monodimensionali, considerando ovviamente che gli indici
riguardano le righe e le colonne (Figura 3.4).

Figura 3.4 Array a di 2 righe e 3 colonne.


Snippet 3.9 Accesso a una riga.

int c[] = a[1];

Lo Snippet 3.9 pone nella variabile c (riferimento a un array di interi) il valore


contenuto nella seconda riga che in pratica il riferimento a un array di 3 elementi.
Snippet 3.10 Accesso a una colonna di una riga.
int d = a[1][1];

Lo Snippet 3.10, invece, pone nella variabile d (una semplice variabile primitiva di
tipo intero) il valore 0 della varabile posta alla seconda colonna della seconda riga.
Snippet 3.11 Scrittura in una colonna di una riga.

a[1][2] = 120;

Lo Snippet 3.11 scrive il valore 120 nella terza colonna della seconda riga.

Anche per gli array bidimensionali la scrittura di un valore in una determinata


posizione della matrice pu essere effettuata con la sintassi inline, come mostrato di
seguito (Sintassi 3.6), dove la coppia di parentesi graffe pi esterne rappresenta
linizializzatore dellarray variable_name mentre ogni coppia di parentesi graffe
interna rappresenta linizializzatore dellarray in quella posizione.
Sintassi 3.6 Inizializzazione inline di un array.

type variable_name[] = { {val, val, val}, {val, val, val} }

Snippet 3.12 Scrittura dei valori di una matrice in fase di inizializzazione.

int a[][] = { {1, 4, 67}, {33, 55, 77} };

La rappresentazione degli array bidimensionali come array di array permette


dunque di creare degli array irregolari, dove ogni riga pu contenere un array di
diverse dimensioni.
Snippet 3.13 Array irregolare.
int a[][] = new int[2][]; // matrice di 2 righe senza la specifica del numero di colonne
a[0] = new int[3];
a[1] = new int[4];

Lo Snippet 3.13 evidenzia come per la creazione di un array irregolare il numero di


colonne viene omesso, poich sar diverso per ogni riga successivamente dichiarata.
Anche per gli array bidimensionali si pu usare la propriet length per conoscere la
dimensione di un array determinato, tenendo presente che la propriet length utilizzata
sul nome dellidentificatore d sempre come valore il numero degli elementi della
prima dimensione.
Snippet 3.14 Propriet length dellarray definito nello Snippet 3.13.

int l = a.length; // 2
int l_1 = a[0].length; // 3
int l_2 = a[1].length; // 4

Lo Snippet 3.14 mostra che la propriet length della variabile a ha come valore il
numero di righe, mentre la propriet length propria di ogni elemento ha come valore il
numero di colonne di quella riga.
Listato 3.2 Classe ArrayBidi.
package com.pellegrinoprincipe;

public class ArrayBidi


{
public static void main(String[] args)
{
// matrice di stringhe
String[][] computer_table =
{
{"MONITOR", "CASSE", "STAMPANTE", "PLOTTER"},
{"MOUSE", "TASTIERA", "JOYPAD", "SCANNER"}
};

System.out.println("PERIFERICHE DI INPUT/OUTPUT");
System.out.println("----------------------------------");

// ciclo esterno
for (int x = 0; x < computer_table.length; x++)
{
for (int y = 0; y < computer_table[x].length; y++)
System.out.print(computer_table[x][y] + " ");
System.out.println();
}
}
}

Output 3.2 Dal Listato 3.2 Classe ArrayBidi.

PERIFERICHE DI INPUT/OUTPUT
--------------------------------------------------------------
MONITOR CASSE STAMPANTE PLOTTER
MOUSE TASTIERA JOYPAD SCANNER

Il Listato 3.2 dichiara una matrice di stringhe e utilizza dei cicli for per scorrere gli
elementi ivi contenuti. In particolare vediamo che il ciclo for pi esterno ottiene la
lunghezza della prima dimensione (righe) leggendo la propriet length
sullidentificatore computer_table, mentre il ciclo for pi interno ottiene la lunghezza
della seconda dimensione (colonne) utilizzando la propriet length sullarray ritornato
dalla valutazione dellespressione computer_table[x].

Array tridimensionali
Un array tridimensionale un array con tre dimensioni. Generalmente un array di
questo tipo impiegato per descrivere delle coordinate in un sistema spaziale a tre
dimensioni, dove possiamo avere un punto che posto in una determinata posizione
sullasse delle x, delle y e delle z.
Listato 3.3 Classe ArrayTridi.

package com.pellegrinoprincipe;

public class ArrayTridi


{
public static void main(String[] args)
{
// uno spazio tridimensionale: coordinate x, y e z
boolean space[][][] = new boolean[100][100][20];

// mettiamo qualche punto nello spazio. true significa che il punto


// presente in quella coordinata. false significa assenza del punto
space[0][0][0] = true;
space[0][0][2] = true;
space[0][1][0] = true;
space[0][1][1] = true;
space[0][1][2] = true;
space[0][2][1] = true;

for (int x = 0; x < space.length; x++)
{
for (int y = 0; y < space[x].length; y++)
{
for (int z = 0; z < space[x][y].length; z++)
{
// comunica le coordinate spaziali solo se presente un punto
if (space[x][y][z])
{
System.out.println("[X = " + x + ", Y = " + y + ", Z = " + z + "]");
}
}
}
}
}
}

Il Listato 3.3 dichiara e inizializza larray a tre dimensioni space atto a contenere
informazioni se un punto presente (valore true) o meno (valore false) in una
determinata locazione spaziale. A tal fine assegniamo il valore true a sei posizioni
dellarray space con le successive istruzioni di assegnamento e mediante lutilizzo
della consueta square bracket notation.
In conclusione utilizziamo il solito ciclo for per scorrere i valori dellarray e per
stampare su schermo solo le coordinate dove effettivamente presente un punto.
Output 3.3 Dal Listato 3.3 Classe ArrayTrid.

[X = 0, Y = 0, Z = 0]
[X = 0, Y = 0, Z = 2]
[X = 0, Y = 1, Z = 0]
[X = 0, Y = 1, Z = 1]
[X = 0, Y = 1, Z = 2]
[X = 0, Y = 2, Z = 1]

Un array tridimensionale anchesso rappresentato come array di array e in


particolare il valore della prima dimensione conterr un riferimento a un array
bidimensionale e il valore della prima dimensione e della seconda dimensione di
questultimo conterr un riferimento a un array monodimensionale che conterr i
valori da manipolare (Figura 3.5).

Figura 3.5 Array tridimensionale.


Capitolo 4
Operatori

Un operatore definibile come una sorta di istruzione che agisce su dei dati, detti
operandi, e permette di ottenere un risultato eseguendo unoperazione. Ogni
operatore rappresentato da simboli che determinano su quali operandi agisce.
Quando in unespressione si incontrano diversi operatori, lordine di esecuzione
viene scelto in base alla precedenza (che indica se un operatore ha priorit maggiore
di un altro) e allassociativit (se pi operatori hanno la stessa precedenza allora
viene eseguito, nel caso dellassociativit da sinistra a destra, prima quello che si
trova pi a sinistra e poi a seguire gli altri sempre da sinistra; nel caso
dellassociativit da destra a sinistra, viene eseguito prima quello che si trova pi a
destra e poi sempre da destra a seguire gli altri). In ogni caso, lordine di precedenza
prestabilito pu essere variato con lutilizzo delloperatore parentesi tonde (), che ha
la precedenza pi alta in assoluto; se vi sono varie coppie di parentesi annidate, la
priorit sar dalla pi interna verso quella pi esterna, mentre se ve ne sono varie
sullo stesso livello, lassociativit sar da sinistra a destra.
Operatore di assegnamento
Loperatore di assegnamento permette di porre un valore in una variabile; ha il
simbolo di uguale = e associa da destra a sinistra. Questo operatore, inoltre, permette
di effettuare una catena di assegnamenti.
Snippet 4.1 Assegnamenti multipli.

int a, b, c;
a = b = c = 400; // a, b e c contengono il valore 400

TERMINOLOGIA
Il termine assegnamento utilizzato quanto assegnazione e infatti entrambi hanno praticamente
lo stesso significato. Per i programmatori pi consueto parlare di assegnamento, forse perch
c maggiore assonanza con il termine inglese assignment.
Operatori aritmetici
Gli operatori aritmetici permettono di costruire delle espressioni aritmetiche:

moltiplicazione con simbolo *


divisione con simbolo /
modulo con simbolo %, che d il resto di una divisione tra interi o decimali
addizione con simbolo +
sottrazione (o meno unario) con simbolo

Gli operandi possono essere solo di tipo numerico e di tipo char (in quanto
considerato, essenzialmente, come un sottotipo di un tipo int e il cui range di valori
accettato, ricordiamo, va da 0 a 65535). Tra gli operatori elencati, quello relativo alla
sottrazione si comporta anche come meno unario invertendo il segno del suo
operando (poich lo moltiplica, di fatto, per -1).
Snippet 4.2 Meno unario.

int a = -55;
int b = -a; // b avr il valore di +55

Snippet 4.3 Espressione aritmetica.

int a = 120 + 40 * 12 * 11 / 10 - 4; // a conterr il valore 644

Snippet 4.4 Operatore ().

int a = (66 + 44) / 22 * 9 * (145 - 140 * (11 - 5 - 2) ); // a conterr il valore -18675


Operatori aritmetici di assegnamento
Gli operatori aritmetici di assegnamento permettono di assegnare un valore che ha
subito, come prima operazione, unoperazione aritmetica:

addizione e assegnamento con simbolo +=


sottrazione e assegnamento con simbolo =
moltiplicazione e assegnamento con simbolo *=
divisione e assegnamento con simbolo /=
modulo e assegnamento con simbolo %=

Tali simboli, in effetti, sono forme abbreviate per espressioni come le seguenti.
Snippet 4.5 Operatori aritmetici di assegnamento.

int a = 120, b = 111;


a = a + 5; // a += 5;
b = b * 10; // b *= 10;

che si leggono, come nel caso di a += 5, somma ad a il valore 5 e poi assegnalo ad a


medesima.
Operatori unari di incremento e
decremento
Gli operatori unari di incremento, con simbolo ++, e di decremento, con simbolo --,
permettono di sommare o sottrarre il valore 1 alla variabile operando in una forma
abbreviata rispetto alle sintassi n = n + 1 e n = n - 1. Sono operatori unari poich
agiscono su un solo operando; se sono prefissi alloperando si dicono di
preincremento o predecremento, mentre se sono postfissi alloperando si dicono di
postincremento o postdecremento. In unespressione, se si usa un preincremento o un
predecremento, prima la variabile subisce lincremento o il decremento e poi il nuovo
valore viene utilizzato; se invece si usa un postincremento o un postdecremento,
prima il valore della variabile viene utilizzato e poi la variabile subisce lincremento
o il decremento.
Snippet 4.6 Operatori ++ e .

int a = 10, b = 10;


int c = a++; // a c sar assegnato il valore 10 e poi a sar incrementata di 1 valendo 11
int d = --b; // prima b sar decrementato di 1, valendo 9
// e poi a d sar assegnato il valore di 9
Operatori relazionali e di uguaglianza
Gli operatori relazionali determinano delle relazioni dordine tra due operandi:

maggiore, con simbolo >


minore, con simbolo <
maggiore o uguale, con simbolo >=
minore o uguale, con simbolo <=

Tutti i tipi di dati primitivi, tranne i boolean, possono fare da operandi.


Snippet 4.7 Booleani come operandi.

boolean b = true, c = false;


boolean d = b > c; // ERRORE - bad operand types for binary operator '>'

Gli operatori di uguaglianza determinano eguaglianze o diseguaglianze tra due


operandi:

uguale a, con simbolo di ==


non uguale a o diverso da, con simbolo !=

I tipi di dati primitivi (anche i boolean) e i riferimenti possono fare da operandi.

OPERATORE DI ASSEGNAMENTO E OPERATORE DI UGUAGLIANZA


Spesso si tende a confondere loperatore di assegnamento (simbolo =) con loperatore di
uguaglianza (simbolo ==). Occorre prestare sempre attenzione alla loro differente semantica:
assegnare significa inserire un valore di una variabile, letterale o costante (detta anche rvalue o
right value) situata a destra delloperando in una variabile (detta lvalue o left value), mentre
uguagliare significa confrontare se due variabili contengono lo stesso valore. Per discriminarli
possiamo pensare semplicemente che quando vogliamo assegnare un valore dobbiamo utilizzare il
simbolo = una sola volta, mentre quando vogliamo verificare luguaglianza tra due valori dobbiamo
utilizzare il simbolo = due volte di seguito.

Snippet 4.8 Espressione con entrambi gli operatori di assegnamento e di uguaglianza.


int a = 120, b = 111, c = 111, d = 112;
boolean e = a < b == c > d; // true

Nello Snippet 4.8 lespressione sar valutata come vera (true) perch, nellordine,
verranno eseguiti: a < b che dar false; c > d che dar false; false == false che dar true.
Snippet 4.9 Valutazione di pi espressioni con entrambi gli operatori.

int nr1 = 120, nr2 = 111;


boolean b = nr1 > nr2; // true
b = nr1 >= nr2; // true
b = nr1 < nr2; // false
b = nr1 <= nr2; // false
b = nr1 != nr2; // true
b = nr1 == nr2; // false

Vediamo ora un esempio (Listato 4.1) in cui, data una matrice di valori, si cerca di
determinare se vi sono dei valori che sono minori di altri valori, passati come criteri
di ricerca, e per ogni valore che soddisfa tale condizione si incrementa di ununit
una variabile che tiene traccia della quantit trovata.
Listato 4.1 Classe RelationalOperators.

package com.pellegrinoprincipe;

public class RelationalOperators


{
public static void main(String args[])
{
// matrice per la ricerca
int[][] values = {{10, 20, 30}, {-22, -11, -18}, {105, 205, -963}};
int filter_values[] = {33, 13, 56}; // valori da confrontare
int how_many = 0; // tiene traccia delle occorrenze trovate

// ciclo per la ricerca


for (int k = 0; k < filter_values.length; k++)
{
for (int i = 0; i < values.length; i++)
{
for (int j = 0; j < values[i].length; j++)
{
int value1 = values[i][j];
int value2 = filter_values[k];
System.out.print("Il valore " + value1 + " minore del valore "
+ value2 + " ? ");

if (value1 < value2)


{
how_many += 1; // incrementiamo di 1 la variabile
System.out.println(" VERO ");
}
else
System.out.println(" FALSO ");
}
}
}
System.out.println("Numero valori trovati: " + how_many);
}
}

Output 4.1 Dal Listato 4.1 Classe RelationalOperators.

Il valore 10 minore del valore 33 ? VERO


Il valore 20 minore del valore 33 ? VERO
Il valore 30 minore del valore 33 ? VERO
Il valore -22 minore del valore 33 ? VERO
...
Il valore -963 minore del valore 33 ? VERO
Il valore 10 minore del valore 13 ? VERO
Il valore 20 minore del valore 13 ? FALSO
Il valore 30 minore del valore 13 ? FALSO
...
Il valore 10 minore del valore 56 ? VERO
Il valore 20 minore del valore 56 ? VERO
Il valore 30 minore del valore 56 ? VERO
...
Numero valori trovati: 19
Operatori logici
Gli operatori logici permettono di costruire espressioni complesse a partire da
quelle pi semplici e di valutarle, quindi, nella loro interezza applicando delle regole
di logica booleana:

AND logico (conditional AND) con simbolo &&


OR logico (conditional OR) con simbolo ||
AND logico booleano (boolean logical AND) con simbolo &
OR inclusivo logico booleano (boolean logical inclusive OR) con simbolo |
OR esclusivo logico booleano, o XOR (boolean logical exclusive OR), con
simbolo ^
NOT logico o negazione logica (logical NOT), con simbolo !

NOTA STORICA
Gli operatori logici sono stati introdotti nel 1854 dal matematico e logico britannico George Boole
nellambito della formalizzazione di un nuovo tipo di algebra chiamata appunto booleana, in cui i
calcoli possono essere effettuati con lutilizzo di due soli valori: vero e falso.

La valutazione di unespressione nel suo complesso avverr considerando le


seguenti tabelle di verit.

Per loperatore AND logico ( &&) lespressione complessiva sar valutata vera
solo se entrambe le espressioni semplici che la costituiscono saranno valutate
vere. Se la prima espressione semplice risulta subito false, la valutazione delle
altre espressioni non verr effettuata e lintera espressione complessiva sar
false.

Tabella 4.1 Operatore logico AND (&&).


Espressione 1 Espressione 2 Risultato
false false false
false true false
true false false
true true true

Per loperatore OR logico (||) lespressione complessiva sar valutata vera se


una delle espressioni semplici che la costituiscono sar vera o se entrambe le
espressioni semplici saranno vere. Se la prima espressione semplice risulter
false, la valutazione delle altre espressioni verr effettuata finch non se ne

trover una true e allora lespressione complessiva sar true.


Tabella 4.2 Operatore logico OR (||).
Espressione 1 Espressione 2 Risultato
false false false
false true true
true false true
true true true

Per loperatore AND logico booleano ( &) e per loperatore OR inclusivo logico
booleano (|) lespressione complessiva sar valutata come per gli operatori
AND e OR logico.
Per loperatore OR esclusivo logico booleano (^) o XOR lespressione
complessiva sar valutata vera se, e solo se, unespressione semplice sar true e
una sar false.

Tabella 4.3 Operatore esclusivo logico XOR (^).


Espressione 1 Espressione 2 Risultato
false false false
false true true
true false true
true true false

Per loperatore NOT logico (!) la valutazione dellespressione avverr


rovesciando il valore delloperando su cui loperatore medesimo agisce; infatti,
lespressione sar valutata come true se sar scritta come NOT false e sar valutata
come false se sar scritta come NOT true.

Tabella 4.4 Operatore logico NOT (!).


Espressione 1 Risultato
!true false
!false true

VALUTAZIONE DI CORTOCIRCUITO
Gli operatori &, | e ^, a differenza degli operatori && e ||, valutano sempre tutte le espressioni che
rappresentano i loro operandi. Per esempio, nellespressione complessa c == 1 && b++ == 3 la
valutazione della seconda espressione sar effettuata solo se la prima espressione sar vera,
mentre nellespressione complessa c == 1 & b++ == 3 la valutazione della seconda espressione,
che causer anche lincremento della variabile b, sar effettuata anche se la prima espressione
sar falsa. Il modo di operare degli operatori && e || definito valutazione di cortocircuito proprio
perch essi interrompono subito le altre eventuali valutazioni se sono subito soddisfatte le seguenti
condizioni che consentono di rilevare immediatamente il risultato di tutta lespressione complessa:
per loperatore && se lespressione alla sua sinistra falsa, allora tutta lespressione complessa
sar subito falsa (lespressione alla sua destra non verr valutata); per loperatore || se
lespressione alla sua sinistra vera, allora tutta lespressione complessa sar subito vera
(lespressione alla sua destra non verr valutata).

Snippet 4.10 Operatori logici.

int a = 10, b = 14;


boolean c = a > 10 && b < 15; // AND logico espressione false
c = a > 10 || b < 15; // OR logico espressione true

c = a > 10 & b-- < 15; // AND logico booleano espressione false e decremento di b
int d = b; // d varr 13

c = a == 10 | b-- < 15; // OR inclusivo logico booleano espressione true e decremento di b


d = b; // d varr, ora, 12

c = a == 10 ^ b < 15; // OR esclusivo logico booleano espressione false


c = (!(a > 10)); // NOT logico espressione true
Operatore ternario
Loperatore ternario permette di eseguire in modo abbreviato unistruzione del tipo
if-then-else e agisce su tre operandi. Il suo simbolo costituito dal simbolo ? e dal
simbolo : che vengono posti in un particolare ordine per dare senso allespressione
complessiva.
Sintassi 4.1 Operatore ternario.

espressione1 ? espressione2 : espressione3

La Sintassi 4.1 si legge nel seguente modo: Se espressione1 true allora


(rappresentato dal simbolo ?) valuta espressione2 altrimenti (rappresentato dal simbolo
:) valuta espressione3. Sia espressione2 sia espressione3 devono ritornare un valore dello
stesso tipo di dato della variabile che lo conterr.
Snippet 4.11 Operatore ternario.
int a = 10, b = 11;
String c = a < b ? "si" : "no"; // c conterr "si"

Nello Snippet 4.11 lespressione si legge cos: Se a minore di b allora il letterale


stringa "si" sar assegnato alla variabile c altrimenti le sar assegnato il letterale
stringa "no".

Lesempio seguente (Listato 4.2) mostra un utilizzo delloperatore ternario con cui
verifichiamo se, dato un valore estratto dalla matrice values che sia pari, lo stesso sia
maggiore del valore 33 (filter_value); se tale verifica positiva, lo memorizziamo in
un array il cui contenuto sar poi mostrato a video.
Listato 4.2 Classe LogicalOperators.

package com.pellegrinoprincipe;

public class LogicalOperators


{
public static void main(String args[])
{
// matrice per la ricerca
int[][] values = {{10, 100, 30}, {-22, -11, 66}, {105, 204, 333}};
int filter_value = 33; // valori da confrontare
int found_values[] = new int[9]; // numero massimo di elementi da inserire

// ciclo per la ricerca


for (int i = 0; i < values.length; i++)
{
for (int j = 0; j < values[i].length; j++)
{
int value = values[i][j];
// posiziono il valore trovato nell'array spostandomi
// alla corretta posizione
if (value % 2 == 0)
found_values[i * values[j].length + j] = value > filter_value ? value : 0;
}
}
// valori trovati
for (int i = 0; i < found_values.length; i++)
System.out.print(found_values[i] + " ");

System.out.println();
}
}

Output 4.2 Dal Listato 4.2 Classe LogicalOperators.

0 100 0 0 0 66 0 204 0
Operatori a livello di bit (bitwise)
Gli operatori a livello di bit agiscono sui singoli bit di operandi di tipo byte, short,
,
int long e char:

NOT unario, o complemento a uno a livello di bit (bitwise complement), con


simbolo ~
AND a livello di bit (bitwise AND), con simbolo &
OR inclusivo a livello di bit (bitwise inclusive OR) con simbolo |
OR esclusivo a livello di bit, o XOR (bitwise exclusive OR), con simbolo ^
spostamento a sinistra dei bit (left shift) con simbolo <<
spostamento a destra dei bit (signed right shift) con simbolo >>
spostamento a destra dei bit senza conservazione del segno (unsigned right shift)
con simbolo >>>

CONSIGLIO
Per meglio comprendere il seguente paragrafo sarebbe opportuno avere almeno una conoscenza
basilare dei sistemi numerici, delle conversioni tra sistemi numerici e delle operazioni aritmetiche
binarie.

Per vedere come tali operatori agiscono sui bit si pu osservare la Tabella 4.5,
dove ogni riga contiene la valutazione di unespressione tra A e B rispetto alloperatore
utilizzato.

Tabella 4.5 Operatori bitwise.


A B ~A A & B A | B A ^ B
0 0 1 0 0 0
1 0 0 0 1 1
0 1 1 0 1 1
1 1 0 1 1 0

Nella tabella vediamo che:

loperatore NOT (~) inverte tutti i bit delloperando sui cui agisce: se c un bit
con 1 allora lo stesso diventer 0 e viceversa;
loperatore AND ( &) confronta i bit dei due operandi: se entrambi sono 1 allora il
risultato sar 1. Tale operatore utile per creare maschere di bit che cancellano i
bit di un altro operando ponendoli a 0;
loperatore OR (|) confronta i bit dei due operandi: il risultato sar sempre 1
tranne se entrambi sono 0. Tale operatore utile per creare maschere di bit che
impostano i bit di un altro operando a 1;
loperatore XOR (^) confronta i bit dei due operandi: il risultato sar 1 solo se
uno sar 1 e laltro sar 0.

Snippet 4.12 Operatori a livello di bit.

// mostriamo solo i primi 8 bit dato che i numeri sono molto piccoli
int a = 5, b = 6;

// c = -6 per rappresentazione numeri negativi con complemento a due


// 00000101 --> 5
// ------- ---> ~
// 11111010 --> -6
int c = ~a;

// c = 4
// 00000101 --> 5
// 00000110 --> 6
// ------- ---> &
// 00000100 --> 4
c = a & b;

// c = 7
// 00000101 --> 5
// 00000110 --> 6
// ------- ---> |
// 00000111 --> 7
c = a | b;

// c = 3
// 00000101 --> 5
// 00000110 --> 6
// ------- ---> ^
// 00000011 --> 3
c = a ^ b;

Per loperatore di spostamento a sinistra dei bit la sintassi da adottare la seguente.


Sintassi 4.2 Shift a sinistra.

valore << num

dove valore la variabile che subir lo spostamento di tanti bit a sinistra quanti
sono indicati da num. Quando avverr lo spostamento, i valori a destra entranti saranno
riempiti di zero e i valori di sinistra che supereranno i bit massimi del tipo saranno
perduti. Occorre tenere presente quanto segue: se un bit con valore 1 cadr
nellultimo bit, il valore sar considerato negativo; poich i tipi byte e char nelle
espressioni sono automaticamente convertiti in int, per assegnare tale valore a una
variabile di quel tipo si dovr effettuare un cast esplicito.
Snippet 4.13 Shift a sinistra con cast esplicito.
byte a = 64, i;

// a convertito in int e poi per riassegnarlo al byte si fa il cast.


// i conterr un valore negativo (-128) perch lo shift di 1 bit ha posto
// il valore 1 sull'ultimo bit che interpretato come negativo
// mostriamo solo i primi 16 bit:
// prima --> 00000000 01000000 dopo --> 11111111 10000000
i = (byte) (a << 1);
Lo shift a sinistra, come si pu rilevare, di fatto esegue unoperazione di
moltiplicazione.
Snippet 4.14 Shift a sinistra visto come operazione di moltiplicazione per 2.

// a ---> 00100000 --> 32


// b ---> 01000000 --> 64
byte a = 32;
byte b = (byte) (a << 1);

Per lo spostamento a destra la sintassi da adottare la seguente.


Sintassi 4.3 Shift a destra.
valore >> num

dove valore la variabile che subir lo spostamento di tanti bit a destra quanti sono
indicati da num. Quando avverr lo spostamento, i valori a sinistra entranti saranno
mantenuti al fine di mantenere il segno (in modo che il numero, se negativo,
rimanga tale) e i valori a destra saranno persi. Lo shift a destra si pu considerare
come una divisione.
Snippet 4.15 Shift a destra visto come una divisione per 2.

// a ---> 00100000 --> 32


// b ---> 00010000 --> 16
byte a = 32;
byte b = (byte) (a >> 1);

Diverso , infine, lo spostamento a destra senza segno, la cui sintassi la seguente.


Sintassi 4.4 Shift a destra segna segno.
valore >>> num

dove valore la variabile che subir lo spostamento di tanti bit a destra quanti sono
indicati da num. Quando avverr lo spostamento, i valori a sinistra entranti non saranno
mantenuti e saranno riempiti con 0:
Snippet 4.16 Shift a destra senza mantenimento del segno.

// a ---> 11111111 11111111 11111111 11100000 --> -32


// b ---> 01111111 11111111 11111111 11110000 --> 2147483632
int a = -32;
int b = a >>> 1;

Lo Snippet 4.16 mostra chiaramente che il numero da negativo diventato


positivo, poich lo shift non ha mantenuto il segno.
Anche per gli operatori bitwise esiste la possibilit di usare la forma abbreviata di
assegnamento con operazione:

assegnamento AND a livello di bit con simbolo &=


assegnamento OR a livello di bit con simbolo |=
assegnamento XOR a livello di bit con simbolo ^=
assegnamento con spostamento a sinistra con simbolo <<=
assegnamento con spostamento a destra con simbolo >>=
assegnamento con spostamento a destra senza segno con simbolo >>>=

Ricordiamo infine che in Java non esiste loperatore virgola (,) inteso come
operatore che valuta degli operandi, poich la virgola serve solo per separare una
serie di variabili durante una dichiarazione o inizializzazione.
Tabella di precedenza degli operatori
Riportiamo una tabella riepilogativa di tutti gli operatori di Java disposti a partire
da quelli con la priorit pi alta e con la relativa associativit (le operazioni con
lasterisco saranno esaminate pi avanti).

Tabella 4.6 Tabella riepilogativa di precedenza degli operatori.


Priorit Operatore Operazione Associativit
1 [] indice array sinistra
&&& () invocazione di metodo o raggruppamento sinistra
&&& . accesso di membro* sinistra
&&& ++ postincremento destra
&&& -- postdecremento destra
2 ++ preincremento destra
&&& -- predecremento destra
&&& + pi unario destra
&&& meno unario destra
&&& ~ bitwise NOT destra
&&& ! NOT logico destra
3 (type) cast destra
&&& new creazione di un oggetto* destra
4 * moltiplicazione sinistra
&&& / divisione sinistra
&&& % modulo sinistra
5 + addizione sinistra
&&& sottrazione sinistra
6 << shift a sinistra con segno sinistra
&&& >> shift a destra con segno sinistra
&&& >>> shift a destra senza segno sinistra
7 < minore di sinistra
&&& <= minore di o uguale a sinistra
&&& > maggiore di sinistra
&&& >= maggiore di o uguale a sinistra
&&& instanceof test di referenza* sinistra
8 == uguale a sinistra
&&& != diverso da sinistra
9 & bitwise AND sinistra
&&& & AND logico booleano sinistra
10 ^ bitwise XOR sinistra
&&& ^ OR esclusivo logico booleano sinistra
11 | bitwise OR sinistra
&&& | OR inclusivo logico booleano sinistra
12 && AND logico sinistra
13 || OR logico sinistra
14 ? : condizionale ternario destra
15 = assegnamento destra
&&& += somma e assegnamento destra
&&& -= sottrazione e assegnamento destra
&&& *= moltiplicazione e assegnamento destra
&&& /= divisione e assegnamento destra
&&& %= modulo e assegnamento destra
&&& <<= shift a sinistra e assegnamento destra
&&& >>= shift a destra e assegnamento destra
&&& >>>= shift a destra senza segno e assegnamento destra
&&& &= AND e assegnamento destra
&&& ^= XOR e assegnamento destra
&&& |= OR e assegnamento destra
Capitolo 5
Strutture di controllo

Un programma composto da una serie di istruzioni che possono essere eseguite


nellordine in cui vengono scritte e in modo sequenziale, oppure in modo non lineare,
mediante lutilizzo di appositi costrutti sintattici di programmazione definiti nel loro
insieme come strutture di controllo.
Struttura di selezione singola if
La prima e pi semplice struttura di controllo quella definita di selezione singola
if, con cui si valuta se unespressione vera o falsa.
Sintassi 5.1 Istruzione if.

if (expression) { statements; }

dove allinterno delle parentesi tonde ( ) va posta unespressione da valutare,


mentre tra le parentesi graffe { } vanno poste le istruzioni che saranno eseguite se
lespressione medesima risulter vera. Se, viceversa, lespressione risulter falsa, le
istruzioni non saranno eseguite e il flusso esecutivo del programma proseguir alla
prima istruzione posta subito dopo la parentesi graffa di chiusura.

ESPRESSIONI, ISTRUZIONI E BLOCCHI DI CODICE


Unespressione un qualsiasi costrutto sintattico composto da uno o pi valori che sono valutati al
fine di ritornare, generalmente, un altro valore come risultato. Gli operandi possono essere variabili,
costanti, invocazioni di metodi, altre espressioni e cos via, mentre gli operatori possono essere tutti
quelli visti finora. Unistruzione , invece, definibile come un costrutto sintattico che consente di
compiere determinate operazioni nellambito di un programma informatico. Possiamo infatti avere
istruzioni di dichiarazione e di inizializzazione delle variabili, di selezione e di controllo del flusso
esecutivo del programma e anche istruzioni che rappresentano esse stesse delle espressioni,
come quando, per esempio, assegniamo una valore a una variabile. Un blocco di codice , infine,
un insieme di pi istruzioni racchiuse tra parentesi graffe { }.

Listato 5.1 Classe If.


package com.pellegrinoprincipe;

public class If
{
public static void main(String[] args)
{
int a = -1;

// a minore di 10?
if (a < 10)
System.out.println("a < 10");
}
}

Output 5.1 Dal Listato 5.1 Classe If.


a < 10

Nel Listato 5.1 listruzione if valuta lespressione a < 10, la quale ritorna un valore
true, e pertanto viene stampata in output la stringa "a < 10".
Struttura di selezione doppia if/else
La struttura di selezione doppia if-else (Sintassi 5.2) consente di eseguire delle
istruzioni se lespressione valutata vera oppure altre istruzioni se lespressione
valutata falsa. Tali istruzioni sono mutuamente esclusive.
Sintassi 5.2 Istruzione if/else.

if (expression) { statements; }
else { statements; }

Qui avremo, oltre al consueto costrutto if, anche un blocco, definito costrutto else,
che eseguir le istruzioni poste al suo interno solamente se lespressione sar falsa.
Listato 5.2 Classe IfElse.
package com.pellegrinoprincipe;

public class IfElse


{
public static void main(String[] args)
{
int a = 5;

if (a >= 10)
System.out.println("a >= 10"); // eseguita se a maggiore o uguale a 10
else
System.out.println("a < 10"); // eseguita in caso contrario
}
}

Output 5.2 Dal Listato 5.2 Classe IfElse.


a < 10

La struttura if/else pu essere costruita con pi livelli di annidamento.


Listato 5.3 Classe IfElseNidificati.

package com.pellegrinoprincipe;

public class IfElseNidificati


{
public static void main(String[] args)
{
int a = 3;

if(a >= 10)


System.out.println("a >= 10"); // eseguita se a maggiore o uguale di 10
else
if(a >= 5)
System.out.println("a >= 5 e a < 10");
else
if(a >= 0)
System.out.println("a >= 0 e a < 5");
}
}

Lo stesso codice scritto, quasi sempre, nel modo seguente (sicuramente pi


leggibile).
Listato 5.4 Classe IfElseNidificatiIndentati.
package com.pellegrinoprincipe;

public class IfElseNidificatiIndentati


{
public static void main(String[] args)
{
int a = 3;

if (a >= 10)
System.out.println("a >= 10"); // eseguita se a maggiore o uguale di 10
else if (a >= 5)
System.out.println("a >= 5 e a < 10");
else if (a >= 0)
System.out.println("a >= 0 e a < 5");
}
}

Comunque si scriva la struttura di controllo, la logica sempre la stessa: se vera


una delle condizioni, allora vengono eseguite le istruzioni corrispondenti e il
programma salta al di fuori di tutti gli altri if/else; se nessuna condizione vera,
allora il programma le salta tutte.
Output 5.3 Dal Listato 5.4 Classe IfElseNidificati e Classe IfElseNidificatiIndentati.

a >= 0 e a < 5

Quando si scrivono pi strutture if/else, si pu incorrere nellerrore denominato


dellelse pendente, in cui lelse non attribuito allif corretto.
Listati 5.5 Classe ElsePendente.
package com.pellegrinoprincipe;

public class ElsePendente


{
public static void main(String[] args)
{
int a = 9, b = 3;

if(a > 10)


if(b > 10)
System.out.println("a e b > 10"); // eseguita se a e b sono maggiori di 10
else
System.out.println("a < 10");
}
}

Lintento del programma del Listato 5.5 sarebbe quello di far stampare "a e b > 10"
se le variabili a e b fossero maggiori di 10 e "a < 10" nel caso in cui a fosse minore di 10.
Tuttavia, per come stato scritto il codice, eseguendo il programma non viene
stampata listruzione dellelse, anche se la variabile a minore di 10 ( infatti uguale
a 9) e quindi soddisfa la condizione corrispondente.

Ci si verifica perch il compilatore assegna la clausola else al primo if precedente


che trova; nel nostro caso il compilatore interpreta le istruzioni nel seguente modo: la
variabile a maggiore di 10? Se lo , allora valuta se la variabile b maggiore di 10, e
se lo stampa "a e b > 10", altrimenti stampa "a < 10".
Al fine di ottenere il risultato corretto dovremo scrivere il codice come illustrato di
seguito, dove evidente a quale if fa riferimento lelse.
Listato 5.6 Classe ElsePendenteCorretto.

package com.pellegrinoprincipe;

public class ElsePendenteCorretto


{
public static void main(String[] args)
{
int a = 9, b = 3;

if (a > 10)
{
if (b > 10)
System.out.println("a e b > 10"); // se a e b sono maggiori di 10
}
else
System.out.println("a < 10");
}
}

Output 5.4 Dal Listato 5.6 Classe ElsePendenteCorretto.

a < 10
Struttura di selezione multipla switch/case
La struttura di selezione multipla switch/case consente di eseguire le istruzioni di
un blocco di codice, identificato da una particolare etichetta, se il valore costante che
questa rappresenta uguale (e mai maggiore o minore) al valore dellespressione da
valutare, che pu essere di tipo byte, short, char, int, String ed enumerativo.
Sintassi 5.3 switch/case.

switch (expression)
{
case value1:
statements;
[break];
case value2:
statements;
[break]
...
[default]:
statements;
}

La keyword switch seguita dalle parentesi tonde ( ) che racchiudono lespressione


da valutare. Poi, tra le parentesi graffe { }, avremo un blocco di etichette (keyword
case) costituite dal valore da confrontare e dalle istruzioni che saranno eseguite se il
relativo valore sar uguale al valore dellespressione. Chiude il blocco switch una
clausola non obbligatoria (keyword default), che esegue delle istruzioni se tutti i
blocchi case non hanno un valore corrispondente al valore dellespressione switch.
Notiamo, inoltre, che ogni case pu avere unistruzione break che, di fatto, interrompe
il flusso esecutivo del codice facendolo uscire dalla struttura switch.
Listato 5.7 Classe SwitchCase.
package com.pellegrinoprincipe;

public class SwitchCase


{
public static void main(String[] args)
{
int number = 4;

// valuto number
switch (number)
{
case 1: // vale 1?
System.out.println("number = " + 1);
break;
case 2: // vale 2?
System.out.println("number = " + 2);
break;
case 3: // vale 3?
System.out.println("number = " + 3);
break;
case 4: // vale 4?
System.out.println("number = " + 4);
break;
default: // nessuna corrispondenza?
System.out.println("number = " + "...none");
}
}
}

Output 5.5 Dal Listato 5.7 Classe SwitchCase.

number = 4

Nel Listato 5.7 la keyword switch valuta il valore della variabile number (in questo
caso 4) e cerca una corrispondenza tra i valori delle etichette case. Se trova
unetichetta case che soddisfa tale valutazione (e nel nostro caso la trova: case 4:),
allora ne esegue listruzione o le istruzioni (che possono non essere raggruppate fra le
parentesi graffe in quanto non sono obbligatorie).
Le etichette case possono essere elencate insieme in modo che per esse possa aver
luogo uno stesso gruppo di azioni.
Listato 5.8 Classe SwitchCaseInGruppo.

package com.pellegrinoprincipe;
public class SwitchCaseInGruppo
{
public static void main(String[] args)
{
char letter = 'd';

switch (letter)
{
// lettere a, b, c ?
case 'a':
case 'b':
case 'c':
System.out.println("Tra le lettere a, b, c");
break;
// lettere d, e, f ?
case 'd':
case 'e':
case 'f':
System.out.println("Tra le lettere d, e, f");
break;
// nessuna corrispondenza
default:
System.out.println("Nessuna corrispondenza di lettera");
}
}
}

Output 5.6 Dal Listato 5.8 Classe SwitchCaseInGruppo.

Tra le lettere d, e, f

Una struttura switch pu essere anche annidata, e ci non crea conflitti con le
costanti case, poich ognuna di esse dichiarata allinterno del proprio blocco switch.
Listato 5.9 Classe SwitchCaseAnnidati.
package com.pellegrinoprincipe;

public class SwitchCaseAnnidati


{
public static void main(String[] args)
{
int exp = 2, exp2 = 1;

// confronta exp
switch (exp)
{
case 1:
System.out.println("exp = " + exp);
break;
case 2:
// confronta exp2 nello SWITCH INDENTATO
switch (exp2)
{
case 1:
System.out.println("exp = " + exp + " e exp2 = " + exp2);
break;
}
break;
}
}
}

Output 5.7 Dal Listato 5.9 Classe SwitchCaseAnnidati.


exp = 2 e exp2 = 1
Struttura di iterazione while
La struttura di iterazione while permette di eseguire lo stesso blocco di istruzioni
ripetutamente finch una determinata condizione vera. La sua sintassi la seguente.
Sintassi 5.4 while.

while (expression) { statements; }

Abbiamo la keyword while, una coppia di parentesi tonde ( ) al cui interno vi sar
lespressione da valutare e un blocco di codice scritto tra le parentesi graffe { }.
Listato 5.10 Classe While.

package com.pellegrinoprincipe;

public class While


{
public static void main(String[] args)
{
int a = 8;

System.out.print("a = ");
while (a >= 0) // finch a >= 0
System.out.print(a-- + " ");
}
}

Output 5.8 Dal Listato 5.10 Classe While.


a = 8 7 6 5 4 3 2 1 0

Nel programma del Listato 5.10 il ciclo while si pu interpretare in questo modo:
finch la variabile a maggiore o uguale al valore 0, stampane il valore
ripetutamente. Nel blocco di codice del while listruzione a-- fondamentale poich
permette di decrementare il valore. Se non ci fosse questa istruzione il ciclo while
sarebbe infinito, perch la condizione sarebbe sempre vera, dato che la variabile a
sarebbe sempre maggiore o uguale a 0.

Ripetiamo, per chiarire meglio come si comporta la struttura iterativa while, il suo
flusso esecutivo.

1. controlla se a >= 0 e se lo va al punto 2 altrimenti va al punto 3.


2. Esegue listruzione di stampa, decrementa a e ritorna al punto 1.
3. Esce dal ciclo.

Si nota che, se la condizione subito falsa, le istruzioni nel corpo della struttura
while non verranno mai eseguite.
Struttura di iterazione do/while
La struttura di iterazione do/while consente, analogamente alla struttura while, di
ripetere un blocco di istruzioni ripetutamente finch una condizione vera. In questo
caso, a differenza di while, le istruzioni del corpo della struttura do/while vengono
eseguite almeno una volta, poich il controllo della condizione viene effettuato dopo
che il flusso esecutivo del codice ha raggiunto listruzione while posta in coda.
Sintassi 5.5 do/while.

do { statements; } while (expression);

dove avremo la keyword do seguita da alcune istruzioni poste tra le parentesi graffe
{ } e la keyword while seguita dallespressione da valutare racchiusa tra le parentesi
tonde ( ).
Listato 5.11 Classe DoWhile
package com.pellegrinoprincipe;

public class DoWhile


{
public static void main(String[] args)
{
int a = 8;

System.out.print("a = ");
do
{
System.out.print(a-- + " ");
}
while (a >= 0); // finch a >= 0
}
}

Output 5.9 Dal Listato 5.11 Classe DoWhile.


a = 8 7 6 5 4 3 2 1 0

Il ciclo del Listato 5.11 si comporta allo stesso modo di quello del Listato 5.10 ma,
a differenza di esso, stampa il valore di a e poi lo decrementa almeno una volta, anche
se potrebbe essere subito minore di 0, e poi verifica se a maggiore o uguale a 0. Il
flusso esecutivo del blocco do/while il seguente.

1. Esegue listruzione di stampa e decrementa a.


2. Controlla se a >= 0 e se lo va al punto 1, altrimenti va al punto 3.
3. Esce dal ciclo.
Struttura di iterazione for
La struttura di iterazione for consente, come le strutture while e do/while, di ripetere
un blocco di istruzioni finch una condizione vera; a differenza di while e do/while,
for consente di gestire nellambito del suo costrutto delle espressioni aggiuntive con
cui, generalmente, si inizializzano e modificano delle variabili di controllo.
Sintassi 5.6 for.

for (expression1; expression2; expression3) { statements; }

La keyword for seguita dalle parentesi tonde ( ) che racchiudono tre espressioni:
expression1 che inizializza delle variabili, expression2 che controlla se la condizione di
continuazione del ciclo ancora vera ed expression3 che modifica le variabili di
expression1 . Il costrutto termina con le parentesi graffe { } del blocco di istruzioni che
sar eseguito ciclicamente finch la condizione sar vera.
Listato 5.12 Classe For.
package com.pellegrinoprincipe;
public class For
{
public static void main(String[] args)
{
System.out.print("a = ");
for (int a = 8; a >= 0; a--) // finch a >= 0 esegue il ciclo
System.out.print(a + " ");
}
}

Output 5.10 Dal Listato 5.12 Classe For.


a = 8 7 6 5 4 3 2 1 0

Esaminando il Listato 5.12 vediamo che la struttura iterativa for formata dai
seguenti blocchi costitutivi: unespressione, eseguita solo una volta, in cui
inizializzata una variabile di controllo (visibile solamente nel ciclo for), rappresentata
dallistruzione int a = 8; unespressione di controllo della condizione di continuazione
del ciclo, rappresentata dallistruzione a >= 0; unespressione di modifica della
variabile di controllo, rappresentata dallistruzione a--.

Il flusso esecutivo del ciclo invece il seguente.

1. Dichiara e inizializza la variabile a.


2. Controlla se a >= 0 e se lo va al punto 3, altrimenti va al punto 6.
3. Stampa il valore di a.
4. Decrementa la variabile a.
5. Ritorna al punto 2.
6. Esce dal ciclo.

Il costrutto for consente, nel blocco di dichiarazione della variabile di controllo e


nel blocco di modifica della medesima, di utilizzare pi variabili separate dal
carattere virgola (,).
Listato 5.13 Classe ForExpr.

package com.pellegrinoprincipe;

public class ForExpr


{
public static void main(String[] args)
{
int var1 = 3, var2 = 2;

System.out.println("a\tz");
for (int a = var1 * 2 + var2, z = 0; a >= 0; a--, z++)
System.out.println(a + "\t" + z);
}
}

Output 5.11 Dal Listato 5.13 Class ForExpr.

a z
8 0
7 1
6 2
...

Il Listato 5.13 evidenzia che, oltre alla variabile di controllo a, abbiamo dichiarato
e inizializzato anche la variabile z e che entrambe sono state gestite nellambito
dellespressione che modifica le variabili di controllo.
I blocchi costitutivi della struttura iterativa for si possono anche omettere, a
condizione per che i punti e virgola di separazione vengano scritti.
Listato 5.14 Classe ForNoExpr.
package com.pellegrinoprincipe;

public class ForNoExpr


{
public static void main(String[] args)
{
int a = 8;

System.out.print("a = ");
for (;;) // ciclo infinito che interrotto dal break
{
if (a < 0)
break;
System.out.print(a-- + " ");
}
}
}

Output 5.14 Dal Listato 5.12 Classe ForNoExpr.


a = 8 7 6 5 4 3 2 1 0
Nel Listato 5.14 notiamo come le espressioni costituenti la struttura iterativa siano
state poste prima dellistruzione for per la definizione della variabile di controllo, e
allinterno del blocco di istruzioni del ciclo per il controllo di terminazione e per la
modifica della variabile di controllo.
Vediamo, infine, che i cicli for si possono costruire anche come cicli senza corpo.
Essi devono per contenere sempre almeno unistruzione definita come vuota o nulla
(carattere punto e virgola ;).
Listato 5.15 Classe ForNoBody.

package com.pellegrinoprincipe;

public class ForNoBody


{
public static void main(String[] args)
{
int val_max = 100, i = 0;

for (i = 0; i < val_max; i++) // ciclo senza corpo


;
System.out.println("i = " + i); // i vale 100!!!
}
}

Output 5.13 Dal Listato 5.15 Classe ForNoBody.

i = 100

ATTENZIONE
Quando si utilizza unistruzione nulla bisogna fare attenzione a non incorrere in un errore di tipo
logico come quello mostrato dallo Snippet 5.1, dove, quando a == -5, il compilatore eseguir
listruzione nulla e anche quella di stampa del valore 5, che probabilmente non si aveva
intenzione di eseguire. Lo Snippet 5.2 mostra invece un errore in tipo sintattico: in questo caso il
compilatore non trover un if da associare allultimo else perch il primo if non ha racchiuso le
sue istruzioni in un blocco delimitato dalle parentesi graffe {}.
Snippet 5.1 Errore logico con istruzione nulla.

int a = -5;
if(a == -5) ; // ERRORE LOGICO
System.out.println("5");

Snippet 5.2 Errore sintattico con istruzione nulla.

int a = -5;
if(a == -5) ;
System.out.println("-5");
else // ERRORE - 'else' without 'if'
System.out.println("non -5");
Enhanced for
A partire dalla versione 5.0 di Java stato introdotto un meccanismo pi rapido e
semplificato per iterare attraverso gli elementi di un array o di una collezione, grazie
a una struttura di iterazione definita enhanced for (ciclo for migliorato).
NOTA
In realt lenhanced for, come meglio apprenderemo nel capitolo dedicato alle collezioni,
utilizzabile anche con qualsiasi oggetto che istanza di una classe che ha implementato
linterfaccia Iterable del package java.lang (come il caso di tutte le classi collezione).

La sintassi la seguente.
Sintassi 5.7 enhanced for.
for (variable_type variable_name : array | collection | iterable) { statements; }

Qui avremo la keyword for, seguita dalle parentesi tonde ( ) al cui interno
scriveremo: una dichiarazione di una variabile di un determinato tipo (definita
variabile di iterazione), il carattere due punti : e larray, la collezione o loggetto
Iterable da cui sar estratto il valore da assegnare alla variabile indicata.
Listato 5.16 Classe EnFor.

package com.pellegrinoprincipe;

public class EnFor


{
public static void main(String[] args)
{
int a[] = {1, 200, 400, 500};
int sum = 0;

for (int elem : a) // for migliorato


sum += elem;

System.out.println("La somma e' " + sum);


}
}

Output 5.14 Dal Listato 5.16 Classe EnFor.

La somma e' 1101

Nel Listato 5.16 il ciclo for scansiona larray a; a ogni iterazione (for each), il
valore del successivo elemento dellarray assegnato alla variabile elem, che deve
essere dello stesso tipo degli elementi dellarray. Viene quindi eseguito il codice del
corpo del for, che ha lobiettivo di memorizzare nella variabile sum un valore che
dato dalla somma dei valori degli elementi dellarray a.
ATTENZIONE
Gli elementi dellarray non possono essere modificati dalla variabile di iterazione a cui sono stati
assegnati, che si comporta come se fosse read-only. Per esempio se assegniamo alla variabile
di iterazione un nuovo valore lo stesso non sar assegnato al corrispettivo elemento dellarray
sorgente delliterazione. In effetti ci perfettamente comprensibile nel momento in cui pensiamo
che nellambito del for migliorato non abbiamo la possibilit di conoscere il corrente indice di
iterazione necessario per modificare lelemento di un array.

Vediamo infine il listato decompilato del file EnFor.class, che mostra chiaramente
che il compilatore ha trasformato la sintassi del for migliorato nella sintassi di un
comune ciclo for.
Decompilato 5.1 Classe EnFor.class.

...
int a[] = {1, 200, 400, 500};
int sum = 0;
int arr$[] = a;
int len$ = arr$.length;
for (int i$ = 0; i$ < len$; i$++)
{
int elem = arr$[i$];
sum += elem;
}
...
Istruzioni break, continue ed etichette
Le strutture iterative possono essere alterate durante il loro flusso esecutivo
mediante le istruzioni break e continue.
Listato 5.17 Classe Break.

package com.pellegrinoprincipe;

public class Break


{
public static void main(String[] args)
{
System.out.print("a = ");
for (int a = 1; a <= 10; a++) // finch a <= 10
{
if (a == 5)
{
break;
}
System.out.print(a + " ");
}
System.out.println();

int a = 1;

System.out.print("a = ");
while (a <= 10) // finch a <= 10
{
if (a == 5)
break;
System.out.print(a++ + " ");
}
}
}

Output 5.15 Dal Listato 5.17 Classe Break.

a = 1 2 3 4
a = 1 2 3 4

Listruzione break nel Listato 5.17 interrompe literazione sia del ciclo for sia del
ciclo while; infatti quando la variabile a uguale a 5 il programma esce
dalliterazione, pertanto saranno stampati solo i valori fino a 4.
Listato 5.18 Classe Continue.
package com.pellegrinoprincipe;

public class Continue


{
public static void main(String[] args)
{
System.out.print("a = ");
for (int a = 1; a <= 10; a++) // finch a <= 10
{
if (a == 5) // salta l'istruzione successiva se a == 5
continue;
System.out.print(a + (a != 10 ? ", " : ""));
}
System.out.println();

int a = 1;

System.out.print("a = ");
while (a <= 10) // finch a <= 10
{
if (a == 5) // salta l'istruzione successiva se a == 5
{
a++;
continue;
}
System.out.print(a + (a != 10 ? ", " : ""));
a++;
}
}
}

Output 5.16 Dal Listato 5.18 Classe Continue.


a = 1, 2, 3, 4, 6, 7, 8, 9, 10
a = 1, 2, 3, 4, 6, 7, 8, 9, 10

Listruzione continue, invece, salta le rimanenti istruzioni del corpo della struttura e
procede con la successiva iterazione. Nel Listato 5.18, nonostante i valori stampati
dal for e dal while saranno, ugualmente, da 1 a 10 eccetto il 5, i due cicli avranno un
differente flusso esecutivo. Infatti, per la sequenza del for avremo quanto segue.

1. Inizializza la variabile a = 1.
2. Controlla se a <= 10 e se lo va al punto 3 altrimenti, va al punto 6.
3. Controlla se a == 5 e se lo non esegue il punto 4 ma va al punto 5. Se,
viceversa, a == 5 false, allora va al punto 4.
4. Stampa a.
5. Incrementa a e va al punto 2.
6. Esce dal ciclo.

Per la sequenza del while, avremo invece quanto segue.

1. Controlla se a <= 10 e se lo va al punto 2, altrimenti va al punto 5.


2. Controlla se a == 5 e se lo non va al punto 3, incrementa a di 1 altrimenti il
ciclo diventa infinito e poi va al punto 1. Se, viceversa, a == 5 false, allora va al
punto 3.
3. Stampa a.
4. Incrementa a e va al punto 1.
5. Esce dal ciclo.

Talvolta si possono avere strutture iterative annidate dove, dalla struttura pi


interna, si pu avere la necessit di uscire, o di continuare, rispetto a unaltra struttura
contenitore pi esterna. Per fare ci si devono usare le istruzioni break o continue con
delle label, ovvero degli identificatori che rappresentano una sorta di segnaposto per
le istruzioni che intendono etichettare (per esempio il ciclo for contenitore esterno).
La sintassi la seguente.
Sintassi 5.8 Definizione di una label.

label: { statements; }

Qui indichiamo il nome delletichetta ( utilizzabile qualsiasi identificatore


consentito dal linguaggio), il carattere due punti : e unistruzione o un gruppo di
istruzioni tra parentesi graffe da etichettare.
Per lutilizzo effettivo di una label , invece, sufficiente (Sintassi 5.9) scrivere
listruzione break o continue seguita dallidentificatore delletichetta (senza i due punti).
Sintassi 5.9 Utilizzo di una label.
break label;
continue label;

Listato 5.19 Classe Label.


package com.pellegrinoprincipe;

public class Label


{
public static void main(String[] args)
{
String output = "";

nr: // label
for (int row = 1; row <= 5; row++)
{
for (int col = 1; col <= 10; col++)
{
if (col == 5) // se col == 5 termina il for etichettato nr
break nr;
output += "#";
}
}
System.out.println(output);
}
}

Output 5.17 Dal Listato 5.19 Classe Label.


####

Nel Listato 5.19, quando lespressione col == 5 verr eseguita listruzione break che
non interromper il ciclo interno, ma interromper il ciclo pi esterno etichettato
dalla label nr.
NOTA
importante ribadire che listruzione break del nostro programma termina listruzione etichettata
(il ciclo for esterno), non trasferendo quindi il flusso di esecuzione del codice alletichetta stessa.
Infatti, tale flusso trasferito allistruzione che segue listruzione etichetta e terminata, ovvero
System.out.println(output).

Nellutilizzo delle label importante tenere presente che non possibile andare
verso unetichetta non definita in un blocco circostante.
Listato 5.20 Classe LabelError.
package com.pellegrinoprincipe;

public class LabelError


{
public static void main(String[] args)
{
// FORMA CORRETTA!!!
label1:
label2:
{
System.out.println("Sono nella label2");
break label1;
}

// FORMA NON CORRETTA!!!


label_a:
{
System.out.println("Sono nella label_a");
}
label_b:
{
System.out.println("Sono nella label_b ");
break label_a; // ERRORE - undefined label: label_a
}
}
}

Il programma nel Listato 5.20 dar il seguente errore di compilazione perch non
possibile eseguire listruzione break label_a in quanto letichetta label_a non definita
per il corrispondente blocco di codice che ha invece come etichetta label_b.
Errore 5.1 Dal Listato 5.20 Classe LabelError.

...LabelError.java:23: error: undefined label: label_a break label_a; // ERRORE - undefined


label: label_a 1 error
Capitolo 6
Metodi

Un metodo un blocco di istruzioni che svolge un compito specifico e che pu


essere utilizzato pi volte allinterno del programma. La sua sintassi la seguente.
Sintassi 6.1 Definizione di un metodo.

[modificatori] tipo-ritorno | void nome-metodo ([tipo-var param1, tipo-var param2 , ...])


{
variabili
istruzioni
return
}

Leggendo da sinistra a destra abbiamo: una sezione modificatori, che consente di


assegnare delle caratteristiche aggiuntive al metodo; lindicazione delleventuale tipo
di ritorno del valore che il metodo ritorna al chiamante, oppure la clausola void se non
ritornato nulla; il nome del metodo; delle parentesi tonde ( ), al cui interno si
potranno indicare i parametri formali del metodo, ovvero le variabili o le costanti che
conterranno i valori passati allatto dellinvocazione del metodo, definiti come
parametri attuali o argomenti; un blocco di codice, tra le parentesi graffe { }, al cui
interno saranno scritte le istruzioni che il metodo elaborer.
DETTAGLIO
La sezione modificatori consente di utilizzare dei modificatori per il controllo dellaccesso al
metodo, indicati con le keyword public, private e protected, e dei modificatori di uso vario,
indicati con le keyword final, static, abstract, synchronized, native e strictfp.

Per esempio, un blocco di codice che calcola la radice quadrata di un numero potr
essere scritto come segue (utilizza, per semplicit, il metodo predefinito sqrt della
classe Math di Java).
Snippet 6.1 Metodo per il calcolo della radice quadrata.
// Usiamo la libreria matematica di Java e pertanto il nostro metodo solo un wrapper.
public double sqrt(double nr)
{
final int MAX = 11;
double val; // variabili
val = nr; // istruzioni
return Math.sqrt(val); // valore di ritorno
}

Nello Snippet 6.1 vediamo che il metodo ha:

il modificatore di accesso public;


un tipo di ritorno double;
il nome (identificatore) sqrt;
un parametro formale rappresentato dalla variabile nr;
una variabile val interna al metodo detta variabile locale;
una costante MAX locale al metodo, la quale ha come valore di inizializzazione 11.
Una costante locale a un metodo pu anche non essere inizializzata
contestualmente alla sua dichiarazione, se non la utilizzeremo allinterno del
medesimo metodo. Ovviamente in caso contrario si avr un errore di
compilazione;
delle istruzioni;
unistruzione return che consente di uscire dal metodo ritornando al metodo
chiamante un valore che deve essere dello stesso tipo specificato dal tipo di
ritorno. Listruzione return sempre obbligatoria se il metodo specifica un valore
di ritorno. Da un metodo si esce anche senza unistruzione return quando il
flusso esecutivo del codice raggiunge la parentesi graffa destra di chiusura del
metodo stesso.

Snippet 6.2 Metodo che non ritorna nulla.


public void foo(int nr)
{
int a = nr;
}

CURIOSIT
Lidentificatore foo utilizzato nella letteratura informatica per indicare un nome generico e non
significativo da attribuire a variabili, funzioni e cos via in porzioni di codice sorgente che hanno
lunico scopo di illustrare dei concetti didattici. Oltre allidentificatore foo possiamo utilizzare gli
identificatori bar, baz e foobar.

Se il metodo non ha parametri formali, lo si scriver indicando solo il suo nome


seguito da una coppia di parentesi tonde vuote.
Snippet 6.3 Metodo che non ha parametri formali.

public void bar()


{
int a = 15;
}

Un metodo, inoltre, non pu essere definito allinterno di un altro metodo e usa


come spazio di memoria lo stack (tale spazio noto come record di attivazione della
funzione), occupandolo finch non termina la sua attivit.
Infine, dobbiamo ricordare che non possibile definire metodi con parametri che
assegnano un valore alla variabile che rappresentano se allatto di invocazione del
metodo stesso il relativo argomento non specificato.
Snippet 6.4 Parametri con valori di default non permessi.

public void foo(int a = 10) // ERRORE - class, interface, or enum expected


{
int b = a + 4;
}
Utilizzo dei metodi
Un metodo viene utilizzato o invocato scrivendo, nellordine e in successione:

il suo oggetto (o classe) di appartenenza;


loperatore punto (.);
il suo identificatore;
loperatore di invocazione del metodo ();
se previsti, degli argomenti posti tra le parentesi tonde di invocazione.

Snippet 6.5 Invocazione di un metodo da un altro metodo della stessa classe.


public void foo(int a)
{
this.sqrt(a);
}

Nello Snippet 6.5 il metodo sqrt preceduto dalloperatore this che si riferisce
alloggetto corrente di utilizzo. Precisiamo che in questo caso loperatore this
facoltativo.
Snippet 6.6 Invocazione di un metodo di un oggetto.

MyMath math = new MyMath(); // riferimento math dell'oggetto MyMath


double val = math.sqrt(55.55); // chiamata del metodo

Nello Snippet 6.6 il metodo sqrt preceduto dalla variabile riferimento math che si
riferisce a un oggetto di tipo MyMath.
Snippet 6.7 Invocazione di un metodo di classe.
double val = MyMath.sqrt(55.55); // chiamata del metodo

Nello Snippet 6.7 il metodo sqrt preceduto dal nome della classe MyMath e non dal
nome di una variabile istanza di un oggetto.
Quando si invoca un metodo, i valori passati possono derivare da letterali, costanti,
variabili primitive, riferimenti ed espressioni. Gli argomenti devono concordare per
numero e per tipo dei parametri con la definizione del metodo stesso e, se sono di
tipo diverso, devono rispettare le regole di promozione dei tipi, altrimenti bisogna
prevedere, laddove necessario, un cast specifico. Per esempio, ricordiamo che
possibile assegnare una variabile di tipo int a una variabile di tipo double, ma per
assegnare una variabile di tipo double a una variabile di tipo int necessario
specificare loperatore di cast, causando la perdita della parte frazionaria.
Snippet 6.8 Invocazione di un metodo che accetta un int assegnato con un cast esplicito.

public static void foo(int nr)


{
System.out.println(nr);
}

// invocazione del metodo foo con cast esplicito perch il metodo sqrt
// ritorna un valore double incompatibile con l'argomento int del metodo foo.
foo((int) Math.sqrt(3.3)); // perdita di valore
Passaggio degli argomenti ai metodi
Java tratta tutti gli argomenti come tipi che passano una copia del valore che
contengono. Cos, se largomento una variabile primitiva, sar passata come valore
una copia del dato che direttamente conterr, mentre se largomento una variabile
riferimento sar passata come valore una copia del suo riferimento. Java, per
questultimo caso, non passer mai una copia dellintero oggetto e non permetter
mai di modificarne il riferimento.
Listato 6.1 Classe PassaggioArgomenti.

package com.pellegrinoprincipe;

class MyInt
{
public int val;
}

public class PassaggioArgomenti


{
public static void fooNonMod(int a_in_method)
{
a_in_method = 100; // qui a_in_method non modifica il valore dell'argomento
}

public static void fooMod(MyInt a_in_method_rif)


{
a_in_method_rif.val = 100; // qui a_in_method_rif modifica il valore
// dell'argomento
}

public static void main(String[] args)


{
MyInt an_int = new MyInt(); // oggetto di tipo MyInt
an_int.val = 200; // qui vale 200
fooMod(an_int); // invoco fooMod
System.out.println("an_int.val vale: " + an_int.val); // qui vale 100

int c = 200;
fooNonMod(c); // invoco fooNonMod
System.out.println("c vale: " + c); // qui vale ancora 200
}
}

Output 6.1 Dal Listato 6.1 Classe PassaggioArgomenti.

an_int.val vale: 100


c vale: 200

Nel Listato 6.1 abbiamo definito il metodo fooNonMod che ha come parametro la
variabile primitiva a_in_method, di tipo int, che conterr una copia del valore della
variabile c, anchessa di tipo int, passata come argomento. La variabile a_in_method
allatto dellinvocazione del metodo conterr il valore 200, che sar subito sostituito
dal valore 100, risultato di unespressione di assegnamento che per, nel contempo,
non modificher affatto il valore della variabile passata come argomento. Il metodo
fooMod, invece, ha come parametro la variabile riferimento a_in_method_rif, che conterr

un riferimento a un oggetto di tipo MyInt. Anchessa, tuttavia, conterr una copia del
valore della variabile passata come argomento, che in questo caso sar una copia
dellindirizzo di memoria dove stato allocato loggetto passato. In questo caso,
per, sia la variabile argomento an_int sia la variabile parametro a_in_method_rif
punteranno allo stesso riferimento e pertanto ogni modifica effettuata dal parametro
si rifletter nellargomento. Infatti, allinterno del metodo fooMod listruzione di
assegnamento a_in_method_rif.val = 100 modificher il valore della variabile val
delloggetto passato come argomento (an_int), il cui valore prima del passaggio era
200.

Per meglio comprendere questo importante concetto utile osservare la


rappresentazione grafica nella Figura 6.1.

Figura 6.1 Manipolazione dei riferimenti e delle variabili primitive.

Nella Figura 6.1 si vede chiaramente che, nel caso dei riferimenti, dopo
linvocazione del metodo entrambe le variabili (an_int e a_in_method_rif) puntano allo
stesso oggetto; pertanto il valore di val viene modificato e tale modifica si ripercuote
nella variabile dellargomento. Nel caso delle variabili primitive, invece, dopo
linvocazione del metodo le variabili c e a_in_method non sono in alcun modo collegate
e infatti il valore 100 non sar riflesso nella variabile c passata come argomento.

IMMODIFICABILIT DEI RIFERIMENTI PASSATI COME ARGOMENTI


Una variabile riferimento passata come argomento non sar mai, essa stessa, modificabile dal
parametro, poich, lo ripetiamo, viene passata una copia del riferimento alla memoria delloggetto
puntato e non lindirizzo di memoria dellargomento stesso. In altri linguaggi di programmazione
possibile far diventare largomento e il parametro la stessa identit (alias), con la conseguenza che,
per esempio, se al parametro assegniamo un riferimento differente, anche largomento avr lo
stesso riferimento.

Listato 6.2 Classe ArgomentoImmodificabile.

package com.pellegrinoprincipe;

class AClass // classe base


{
public void printMe() {}
}
class AClass_child_1 extends AClass // classe figlia di AClass
{
public void printMe() { System.out.println("child 1"); }
}
class AClass_child_2 extends AClass // classe figlia di AClass
{
public void printMe() { System.out.println("child 2"); }
}

public class ArgomentoImmodificabile


{
public static void aMethod(AClass a_class)
{
// cambiamo il riferimento del parametro che punter ad AClass_child_2
// senza che questo cambi il riferimento dell'argomento
a_class = new AClass_child_2();
}

public static void main(String[] args)


{
AClass an_object = new AClass_child_1();
aMethod(an_object); // passiamo un oggetto che ha un riferimento
// ad AClass_child_1
an_object.printMe(); // qui an_object punter sempre all'oggetto
// della classe AClass_child_1
}
}

Output 6.2 Dalla Classe ArgomentoImmodificabile.


child 1

Il Listato 6.2 mette in evidenza come non sia mai possibile far diventare
largomento e il parametro degli alias. Infatti, la variabile riferimento an_object, sia
prima sia dopo il suo passaggio come argomento al metodo aMethod, conterr sempre
un riferimento a un oggetto di tipo AClass_child_1, nonostante allinterno del metodo il
parametro abbia poi cambiato il suo riferimento verso la classe AClass_child_2.

MODIFICABILIT DEI RIFERIMENTI PASSATI COME ARGOMENTI


La modificabilit dellindirizzo di un riferimento pu essere attuabile, invece, in altri linguaggi di
programmazione come, per esempio, C#. Infatti, nel caso del linguaggio di Microsoft consentito il
passaggio di riferimenti dei riferimenti tramite la keyword ref, come illustrato nel Listato 6.3, che
compilabile ed eseguibile con gli strumenti della piattaforma .NET.

Listato 6.3 Classe ArgomentoModificabile (scritto in C#).

using System;

namespace com.pellegrinoprincipe
{
class AClass // classe base
{
public virtual void printMe() {}
}
class AClass_child_1 : AClass // classe figlia di AClass
{
public override void printMe() { Console.WriteLine("child 1"); }
}
class AClass_child_2 : AClass // classe figlia di AClass
{
public override void printMe() { Console.WriteLine("child 2");}
}

class ArgomentoModificabile
{
static void aMethod(ref AClass a_class)
{
// cambiamo il riferimento del parametro che punter ad AClass_child_2
// ma questo cambier anche il riferimento dell'argomento
a_class = new AClass_child_2();
}

static void Main(string[] args)


{
AClass an_object = new AClass_child_1();
aMethod(ref an_object); // passiamo un oggetto che ha un riferimento
// ad AClass_child_1
an_object.printMe(); // qui an_object punter ora all'oggetto della classe
// AClass_child_2
}
}
}

Output 6.3 Dalla Classe ArgomentoModificabile (scritta in C#).


child 2

LOutput 6.3 mostra chiaramente che an_object non punta pi a un oggetto di tipo
AClass_child_1 , ma a un oggetto di tipo AClass_child_2, mentre la Figura 6.2 evidenzia la
differenza tra Java e C# in merito ai riferimenti.

Figura 6.2 Differenze tra Java e C# nella manipolazione di un riferimento passato come argomento.
Argomenti a lunghezza variabile
Un metodo pu essere definito con la possibilit di ricevere una lista di argomenti
il cui numero non noto a priori, poich pu variare a ogni invocazione.
Sintassi 6.2 Metodo con parametri a lunghezza variabile.

[modificatori] tipo-ritorno | void nome-metodo (tipo-var param1)


{
variabili
istruzioni
return
}

La Sintassi 6.2 evidenzia che il parametro che potr essere variabile viene definito
con la scrittura del suo tipo seguito dai punti di sospensione ... (ellissi).

Quando si progetta un metodo che pu avere un parametro a lunghezza variabile,


bisogna ricordare che lo stesso deve essere scritto solo una volta e deve essere posto
solo alla fine di una lista di parametri.
Listato 6.4 Classe ArgomentoVariabile.
package com.pellegrinoprincipe;

public class ArgomentoVariabile


{
public static void doSum(int c)
{
int sum = 0;
for (int i : c)
sum += i;

System.out.println("La somma e': " + sum);


}

public static void main(String[] args)


{
int one[] = {22, 33, 55};
int two = 111, three = 444;

doSum(one); // passo un array


doSum(two); // passo una sola variabile
doSum(two, three); // passo due variabili
}
}

Output 6.4 Dal Listato 6.4 Classe ArgomentoVariabile.


La somma e': 110
La somma e': 111
La somma e': 555

Il Listato 6.4 mostra che per Java, in effetti, un parametro a lunghezza variabile
altro non che un array; infatti il metodo doSum ne ricava gli elementi con un semplice
ciclo for.

La conferma di quanto affermato rilevabile analizzando il seguente listato


proveniente dalla classe ArgomentoVariabile.class decompilata.
Decompilato 6.1 File ArgomentoVariabile.class.

...
public static transient void doSum(int c[])
{
...
}

public static void main(String args[])


{
...
doSum(new int[] {two});
doSum(new int[] {two, three});
}
...

Il Decompilato 6.1 mostra chiaramente che:

la definizione nel metodo doSum del parametro int c stata sostituita dalla
definizione dello stesso come array di int, ovvero come int c[];
nel main, linvocazione del metodo doSum avvenuta sostituendo largomento con
una definizione di un array che contiene come elemento largomento stesso.
Infatti, per esempio, doSum(two, three) stato sostituito da doSum(new int[] {two,
three}).
Parametri di tipo array
In un metodo un parametro di tipo array si indica scrivendolo come se lo stessimo
dichiarando. Per esempio, lo Snippet 6.9 dichiara un parametro che una variabile
denominata k ed di tipo array di int.
Snippet 6.9 Un array come parametro di un metodo.

public void passing(int k[], int n, float f) {}

Nellinvocazione di un metodo, un tipo array si passa scrivendone semplicemente


il nome. Infatti, lo Snippet 6.10 crea un array di 3 interi chiamato c e lo passa come
argomento al metodo passing.
Snippet 6.10 Passaggio di un array a un metodo.

int c[] = new int[3];


passing(c, 44, 55.66f);

Allatto della dichiarazione di un array come parametro di un metodo si pu


anteporre la keyword final per far s che allo stesso non si possano, in seguito,
assegnare altri array (Snippet 6.11). Tale keyword si pu utilizzare anche con
parametri che sono oggetti o variabili primitive; anche in questo caso, come si visto
per gli array, il valore del parametro non potr poi essere modificato allinterno del
metodo.
Snippet 6.11 Dichiarazione di un array final.

public void passing(final int k[])


{
int j[] = new int[2];
k = j; // ERRORE - final parameter k may not be assigned
}
Ricorsione
Un metodo rappresentato, come abbiamo visto, da una serie di istruzioni
racchiuse in un blocco di codice che eseguono un compito specifico, ed invocato in
modo gerarchico: in un punto di un programma si invoca il metodo, esso esegue le
operazioni ivi indicate e poi termina.
Tuttavia, per la risoluzione di alcuni problemi si possono costruire dei metodi che
invocano se stessi tante volte quante sono necessarie per risolvere il compito
designato. Tale modalit di invocazione dei metodi detta ricorsione.
La progettazione di un metodo ricorsivo richiede che vi sia un punto di uscita
terminale detto caso base e uninvocazione a se stesso detto passo ricorsivo.
Vediamo subito un esempio che chiarir meglio quanto detto, illustrando il
concetto matematico di fattoriale e vedendo come possiamo scrivere un metodo che
ne effettua il calcolo.

IL FATTORIALE DI UN NUMERO
In matematica il calcolo del fattoriale un procedimento mediante il quale dato un numero intero
positivo si deve trovare quel valore che il prodotto di tutti i numeri interi positivi minori o uguali del
numero stesso. Lo si pu trovare usando la formula iterativa: N! = N * (N1) * (N2) * * 1 oppure
quella ricorsiva, N! = N * (N-1)! considerando che, in entrambi i casi, 0! = 1 e 1! = 1. Per esempio,
con la formula iterativa il fattoriale di 5 5*4*3*2*1, mentre con quella ricorsiva il fattoriale di 5 5 *
(5 - 1)!. In entrambi i casi, ovviamente, il risultato sar uguale a 120.

Snippet 6.12 Metodo ricorsivo per il calcolo del fattoriale.

public long factorial(long number)


{
if (number < 0)
return 0; // attenzione, argomento non corretto!!!
else if (number <= 1) // caso base
return 1;
else // passo ricorsivo
return number * factorial(number - 1);
}

Lo Snippet 6.12 crea il metodo factorial che calcola il fattoriale di un numero


passato come argomento. Esso mostra chiaramente che il caso base permette, di fatto,
di uscire dal metodo (se lo omettessimo avremmo una ricorsione infinita), e nel
nostro esempio per il calcolo di un fattoriale il caso base si verifica quando number
minore o uguale a 1 (se number minore di 0 il programma termina subito, a indicare la
non correttezza del valore dellargomento passato al metodo). Il passo ricorsivo,
invece, permette di invocare nuovamente il metodo factorial (ovvero se stesso)
quando number maggiore di 1, passando come argomento number - 1. In effetti il passo
ricorsivo permette di risolvere una parte pi piccola del problema fino a far
convergere il metodo verso il suo caso base e far cos ritornare, in successione, tutti i
metodi invocati (Figura 6.3).

Figura 6.3 Calcolo del fattoriale di 4.

La Figura 6.3 mostra la sequenza di azioni che avvengono quando invochiamo il


metodo factorial passandogli come argomento il numero 4. Per i passi ricorsivi
avremo, infatti:

1. number vale 4 e non minore o uguale al numero 1, cosicch viene invocato


nuovamente factorial con argomento 4 1;
2. number vale 3 e non minore o uguale al numero 1, cosicch viene invocato
nuovamente factorial con argomento 3 1;
3. number vale 2 e non minore o uguale al numero 1, cosicch viene invocato
nuovamente factorial con argomento 2 1;
4. number vale 1 ed minore o uguale al numero 1, cosicch viene ritornato lo stesso
numero 1 e i passi ricorsivi terminano.

Per i ritorni dai metodi, invece, avremo:

1. il risultato 1;
2. il risultato 2 * 1;
3. il risultato 3 * 2;
4. il risultato 4 * 6;
5. il risultato 24 che sar ritornato al chiamante.
Overloading dei metodi
Loverloading (sovraccaricamento) dei metodi una caratteristica implementativa
che consente di definire metodi con lo stesso nome ma con diversa segnatura.
TERMINOLOGIA
La segnatura (o firma) di un metodo costituita da un insieme di informazioni che identificano
univocamente il metodo stesso fra quelli della sua classe di appartenenza. Tali informazioni
includono il nome del metodo, il numero, il tipo e lordine dei suoi parametri e mai il tipo del suo
valore restituito (detto tipo di ritorno).

Due metodi sono in overloading qualora abbiano lo stesso nome ma si scrivano i


parametri:

di tipo differente;
in numero differente;
in un ordine diverso.

Loverloading utile quando si devono scrivere metodi che hanno una logica di
comportamento simile, ma che differiscono tra di essi solo per alcuni dettagli. Si
pensi, per esempio, a un metodo che esegue una serie di operazioni aritmetiche. Si
vuole anche decidere se il risultato deve essere stampato a video oppure salvato in un
file. Normalmente, senza loverloading, si scriverebbero due metodi distinti: uno per
la stampa a video, identificato per esempio con il nome printResultOnVideo, e uno per il
salvataggio in un file, identificato per esempio con il nome saveResultOnFile.
Ovviamente tale implementazione, seppur corretta, porta lo svantaggio che occorre
ricordare il nome di due metodi che tutto sommato fanno la stessa cosa ma
differiscono solo per come si deve processare il risultato. Con loverloading, invece,
basterebbe scrivere i due metodi con lo stesso nome significativo e farli differire in
base a un parametro: se questo presente, il nome del file dove scrivere il risultato,
mentre se assente, significa che il risultato, per default, verr visualizzato a video.
Potremmo quindi avere i due metodi seguenti: void result() per la stampa a video e
void result(String file_name) per il salvataggio in un file.

Si pensi ancora, come ulteriore esempio, a metodi che eseguono calcoli aritmetici
(diciamo di moltiplicazione) su tipi differenti. Anche qui, anzich scrivere tanti
metodi come multInt(int a, int b), multDouble(double a, double b) e cos via, potremmo
scriverli come mult(int a, int b) e mult(double a, double b).

Quando si devono scrivere metodi che risolvono problemi come quello appena
descritto, si pu utilizzare il paradigma della programmazione generica, con cui
anche il tipo del parametro pu essere parametrizzato evitando, di fatto, la necessit
di scrivere tanti metodi con lo stesso nome che eseguono la stessa logica di
funzionamento su tipi differenti.
Possiamo pertanto affermare che loverloading dei metodi si utilizza per soddisfare
altre necessit progettuali di un applicativo, come vedremo nel caso dei costruttori in
overloading di una classe o in altri casi dove le differenze stanno nel numero di
parametri o nel posto che essi occupano nella segnatura.
In conclusione ricordiamo che non possibile distinguere i metodi definiti in
overloading esclusivamente in base al loro tipo di ritorno (questo proprio perch,
tecnicamente, il tipo di ritorno in Java non fa parte della segnatura di un metodo).
Listato 6.5 Classe Overloading.

package com.pellegrinoprincipe;

import java.io.*;

public class Overloading


{
private static int a = 10, b = 20;

public static void result()


{
System.out.println("La somma di 10 e 20 e' " + (a + b));
}

public static void result(String file_name) throws IOException


{
if (file_name != null && file_name.length() > 0)
{
try (BufferedWriter out = new BufferedWriter(new FileWriter(file_name)))
{
out.write("La somma di 10 e 20 e' " + (a + b));
}
}
}

public static void main(String[] args) throws IOException


{
result("result.txt"); // scrivi su file
}
}

Output 6.5 Dal file result.txt.

La somma di 10 e 20 e' 30

Il Listato 6.5 mostra la definizione di due metodi in overloading denominati result,


di cui uno ha come parametro un tipo stringa e un altro senza parametri. Nel nostro
caso, nel metodo main abbiamo scelto di invocare il metodo result passandogli il nome
di un file dove verr scritto il risultato di unespressione.
NOTA
Se abbiamo eseguito il programma con lIDE NetBeans, il file result.txt si trover nella directory
radice del progetto relativo (nella cartella Overloading), mentre se labbiamo eseguito mediante il
comando java, in accordo con le regole citate nellIntroduzione, tale file si trover nella cartella
MY_JAVA_CLASSES.
Capitolo 7
Programmazione basata sugli oggetti

La programmazione basata sugli oggetti ha come obiettivo principale la creazione


di nuovi tipi di dato denominati classi. Le classi sono progettate con lo scopo di
modellare in astratto degli oggetti del mondo reale. Al loro interno sono definite
(incapsulate) delle propriet e delle operazioni, che nel loro insieme ne rappresentato
i membri.
Le propriet sono in concreto rappresentate da variabili e costanti, mentre le
operazioni dai metodi. Questi ultimi sono definiti anche come interfacce, poich
consentono a un client di comunicare con la classe in cui sono definiti.
TERMINOLOGIA
Il concetto di interfaccia va qui inteso come elemento di comunicazione verso lesterno. Non va
quindi confuso con il costrutto interfaccia (interface), discusso nel Capitolo 8.

Rispetto ai linguaggi non ad oggetti, come C, dove la programmazione si


concentra prima sulle funzioni e poi sui dati che manipolano, la programmazione
basata sugli oggetti si concentra sulla creazione dei dati e poi sui metodi e le variabili
che vi agiscono. In altre parole, mentre in un linguaggio procedurale come C la
modularit di un programma viene fondamentalmente descritta dalle procedure che
manipolano i dati, nella programmazione ad oggetti la modularit viene descritta
dalle classi che incapsulano al loro interno sia i dati, sia i metodi. Per questa ragione
si suole dire che nel mondo ad oggetti la dinamica (metodi) subordinata alla
struttura (classi).
La classe rappresenta, quindi, un modello da cui si creano degli oggetti che sono le
loro istanze.
TERMINOLOGIA
Un client , generalmente, definibile come un generico utilizzatore di classi fornito di un metodo
main dove sono creati gli oggetti da manipolare.
Classi
Una classe ha la seguente struttura sintattica.
Sintassi 7.1 Definizione di una classe.

[modifiers] class ClassName [extends Class implements Interface1, Interface2, ... InterfaceN]
{
data members
member methods
}

La Sintassi 7.1 mostra che la definizione di una classe ha:

una sezione (non obbligatoria) definita modifiers che consente di specificare


delle direttive a cui la classe deve sottostare. Per esempio, se vogliamo che il
nome del file che contiene la classe abbia lo stesso nome della classe, e che la
stessa sia visibile ovunque, allora dobbiamo utilizzare il modificatore public
prima della keyword class. Precisiamo per che, se in un file di codice sorgente
definiamo pi classi, solo una di esse pu avere il modificatore public;
la keyword class seguita dal nome di un identificatore;
delle keyword opzionali, extends e implements, che consentono rispettivamente di
estendere una classe e di implementare delle interfacce (si veda il Capitolo 8 per
una descrizione del costrutto interface);
un corpo, delimitato dalle parentesi graffe { }, al cui interno porremo i dati
membro e i metodi.

Listato 7.1 Classe Time.

package com.pellegrinoprincipe;

public class Time extends Object


{
// variabili di istanza private
private int ora;
private int minuti;
private int secondi;

public Time() // costruttore


{
ora = minuti = secondi = 0;
}

public void setTime(int o, int m, int s) // metodo per impostare un tempo


{
ora = (o < 24 && o >= 0) ? o : 0;
minuti = (m < 60 && m >= 0) ? m : 0;
secondi = (s < 60 && s >= 0) ? s : 0;
}

public String getTime() // metodo per ottenere un tempo


{
return ora + ":" + minuti + ":" + secondi;
}

public String toString() // stampa una rappresentazione leggibile di un oggetto Time


{
return "Orario corrente: " + getTime();
}
}

Listato 7.2 Classe Time_Client.

package com.pellegrinoprincipe;

public class Time_Client


{
public static void main(String[] args)
{
Time t = new Time(); // istanza di Time

System.out.println("Time con i valori di default: " + t.getTime());


t.setTime(14, 30, 56); // imposto nuovi valori per un tempo
System.out.println("Time con i valori impostati: " + t);
}
}

Output 7.1 Dal Listato 7.2 Classe Time_Client.


Time con i valori di default: 0:0:0
Time con i valori impostati: Orario corrente: 14:30:56

Il Listato 7.1 mostra la definizione di una classe denominata Time con modificatore
public e che deriva (discende) dalla classe Object.

DISCENDENZA DI UNA CLASSE


Ricordiamo brevemente che la discendenza un concetto legato allereditariet, che affronteremo
nel Capitolo 8. Per ora basti sapere che, quando una classe usa la keyword extends dopo il suo
nome, altro non fa che specificare quale altra classe il suo genitore (detta anche classe base o
superclasse) da cui essa, il figlio (detto anche classe derivata o sottoclasse), eredita i metodi e i
dati pubblici e protetti. Specifichiamo, inoltre, che la derivazione dalla classe Object non
obbligatoria poich, nel caso non vi provvedessimo noi, il compilatore la effettuerebbe
automaticamente.

Allinterno del corpo della classe Time abbiamo definito diversi dati membro e vari
metodi, preceduti dallindicazione di determinate clausole definite specificatori di
accesso, che dettano le norme sulla visibilit e lutilizzo dei membri medesimi
allesterno della classe.
Tali specificatori sono indicati usando le seguenti keyword.

public : indica che i membri sono utilizzabili da membri di altre classi esterne alla
classe dove sono stati definiti. Tali membri vengono usati da un client
utilizzatore scrivendo il loro nome e il nome delloggetto separati dalloperatore
punto (.).
private: indica che i membri sono accessibili soltanto da altri membri allinterno

della classe medesima. Tali membri sono specificati dai metodi allinterno della
classe senza qualificazioni, direttamente.
protected : indica che i membri sono accessibili soltanto da altri membri
allinterno della classe medesima e dalle sue classi derivate.

Se un membro di una classe non ha uno specificatore di accesso, sar visibile


esclusivamente nella sua classe e in classi appartenenti allo stesso package (concetto
che studieremo in un capitolo apposito in cui amplieremo le regole summenzionate).
Continuando lo studio del listato della classe Time, notiamo la presenza di un
metodo, definito costruttore, che ha lo stesso nome della classe e che non ha
lindicazione del tipo di ritorno. Tale metodo costruttore chiamato in causa quando
si crea un oggetto di una classe. Nel nostro caso listruzione seguente:
Snippet 7.1 Creazione di un oggetto.

Time t = new Time();

creer un oggetto t di tipo Time chiamando con loperatore new il suo costruttore Time().
Quando si invocher tale metodo, il compilatore allocher una quantit di memoria
idonea a contenere loggetto Time e ritorner nella variabile t il suo riferimento.

Si pu dunque dire che il processo di creazione di un oggetto si attua in due


passaggi:

1. con una dichiarazione dove si dice che un identificatore di un certo tipo e che
sar, per lappunto, capace di contenere un oggetto di quel tipo;
2. con una definizione dove grazie alloperatore new si allocher dinamicamente
uno spazio di memoria che conterr loggetto invocato e che ne restituir un
riferimento per i futuri accessi (Figura 7.1).

Figura 7.1 Fasi di creazione di un oggetto, che allatto della dichiarazione avr il valore speciale null.
ATTENZIONE
Se non viene creato esplicitamente un metodo costruttore, il compilatore provveder in
autonomia a crearne uno, definito costruttore di default.

Il costruttore ha anche lo scopo di inizializzare le variabili di istanza delloggetto


creato, al fine di porle in uno stato consistente. Nel nostro esempio Time() inizializza
le sue variabili con il valore 0; tuttavia, se non si esplicita un valore da dare alle
variabili di istanza, il compilatore provveder automaticamente a inizializzarle con i
seguenti valori di default: 0 per tipi primitivi; false per i tipi booleani; null per i
riferimenti, valori di default dei tipi dichiarati per gli array.
Nella classe Time presente un metodo denominato toString() che viene chiamato
implicitamente dal compilatore quando si usa il riferimento delloggetto da solo nella
valutazione di unespressione. Infatti, nel Listato 7.2 lespressione: "Time con i valori
impostati: " + t crea una stringa formata dallunione di "Time con i valori impostati: "
con il risultato dellinvocazione del metodo toString() del riferimento t.

Generalmente si scrive un metodo toString() per avere una rappresentazione


leggibile delloggetto qualora lo si ponga in unespressione da valutare. Tuttavia, se
non si scrive un proprio metodo toString(), il compilatore utilizzer quello ereditato
dalla classe base Object, che stamper un valore indicante il nome della classe di cui
lattuale oggetto istanza pi il simbolo @ e lhash code delloggetto stesso (Snippet
7.2).
Snippet 7.2 Valore predefinito del metodo toString.

com.pellegrinoprincipe.Time@69b332

In conclusione ricordiamo che si pu scrivere un metodo che ha lo stesso nome


della classe e che ritorna un valore, ma esso non sar interpretato dal compilatore
come costruttore, bens come un qualsiasi altro metodo.
Listato 7.3 Classe Time_REV_1.
...
public class Time_REV_1 extends Object
{
...
// metodo non costruttore che ritorna semplicemente il valore passato al parametro
public int Time_REV_1(int a)
{
return a;
}
...
}

Listato 7.4 Classe Time_Client_REV_1.


...
public static void main(String[] args)
{
Time_REV_1 t = new Time_REV_1(5); // ERRORE cannot find symbol
}

Errore 7.1 Dal Listato 7.4 Classe Time_Client_REV_1.

...Time_Client_REV_1.java:7: error: cannot find symbol Time_REV_1 t = new Time_REV_1(5); //


ERRORE - cannot find symbol 2 errors
DallErrore 7.1 si nota che il compilatore non ha compilato il programma, poich
abbiamo cercato di invocare il metodo Time_REV_1(int a) come metodo costruttore,
mentre, come appena detto, ci non possibile, dato che i costruttori sono metodi che
hanno lo stesso nome della classe ma nessun tipo di ritorno.
Visibilit e controllo di accesso
Tutti i membri interni di una classe sono subito accessibili senza particolari
referenziazioni e indipendentemente dagli specificatori di accesso, pertanto hanno
visibilit di classe. Viceversa, la loro visibilit al di fuori della classe di appartenenza
segue le regole dettate dagli specificatori di accesso.
ATTENZIONE
Occorre ricordare che le variabili locali, dichiarate allinterno di un metodo o di un blocco di
codice, hanno visibilit di metodo o di blocco e non sono accessibili neppure agli altri metodi della
stessa classe. Se una variabile locale a un metodo ha lo stesso nome di una variabile di istanza,
questultima nascosta e per poterla utilizzare necessario anteporle la keyword this seguita
dalloperatore punto (.) e dal suo nome. Vedremo in seguito che this altro non che un
riferimento alloggetto corrente (se stesso).
Listato 7.5 Classe Time_Client_Private_Access.

...
public static void main(String[] args)
{
Time t = new Time();
t.setTime(14, 30, 56);
t.ora = 15; // ERRORE - ora has private access
}

Errore 7.2 Dal Listato 7.5 Classe Time_Client_Private_Access.

...Time_Client_Private_Access.java:9: error: ora has private access in Time t.ora = 15; //


ERRORE - ora has private access 1 error

La compilazione del Listato 7.5 provocher un errore con cui il compilatore ci


comunica che il campo ora non direttamente visibile (e pertanto accessibile) al
nostro client; questo perch un membro private , per lappunto, un dato privato della
classe ben occultato e generalmente di servizio per gli altri metodi della stessa classe.
Quando si vuole rendere disponibile al client un dato privato (per esempio una
variabile), opportuno farlo attraverso due metodi pubblici: uno di tipo set, con cui si
imposta il suo valore, e uno di tipo get, con cui si legge il medesimo valore.
Ovviamente questa metodologia ha il vantaggio che allinterno del metodo si possono
utilizzare dei controlli oppure dei filtraggi per rendere consistente il dato manipolato.
APPROFONDIMENTO
Un altro vantaggio legato alluso di dati privati che si impedisce a una classe client di
referenziarli (nominarli) direttamente, aumentando il livello generale di accoppiamento tra classi.
Ci si rivela estremamente utile qualora si debba un giorno modificare la dichiarazione di un dato,
per esempio cambiandone il nome. Si dovr in tal caso modificare la sola classe che definisce il
dato, anzich tutti i codici client che lutilizzano attraverso un metodo accessorio di tipo get o set.
Listato 7.6 Classe Time_REV_2.
...
public class Time_REV_2 extends Object
{
...
public int getOra() // ottengo l'ora
{
return ora;
}

public int getMinuti() // ottengo i minuti


{
return minuti;
}

public int getSecondi() // ottengo i secondi


{
return secondi;
}
...
}

Listato 7.7 Classe Time_Client_REV_2.


...
public static void main(String[] args)
{
Time_REV_2 t = new Time_REV_2();

t.setTime(14, 30, 56);


// ottengo singolarmente le ore, i minuti e i secondi
System.out.println("Ora: " + t.getOra() + " Minuti: " + t.getMinuti()
+ " Secondi: " + t.getSecondi());
}

Output 7.2 Dal Listato 7.7 Classe Time_Client_REV_2.


Ora: 14 Minuti: 30 Secondi: 56
Costruttori: ripasso e dettagli
Un costruttore, come abbiamo gi visto, un metodo della classe che ha il suo
stesso nome e non ha tipi o valori di ritorno. Il suo scopo quello di creare un nuovo
oggetto di una classe e viene invocato automaticamente. Al suo interno prassi
comune far inizializzare le variabili di istanza.
Se una classe non prevede un costruttore, il compilatore ne crea automaticamente
uno di default senza argomenti, e allatto della creazione di un oggetto di quella
classe, invocher il costruttore di default senza argomenti della sua superclasse (e
cos via per tutta leventuale catena di ereditariet), quindi effettuer le
inizializzazioni con i valori visti in precedenza. Tuttavia, se si scrivono dei costruttori
e non si prevede un costruttore senza argomenti, necessario prestare attenzione al
fatto che, ove si cerchi di creare loggetto senza passare argomenti, il compilatore
non creer automaticamente il suo costruttore di default, generando un messaggio di
errore.
I costruttori possono essere sovraccaricati, ovvero si possono scrivere dei metodi
con lo stesso nome ma con elenchi di parametri differenti per tipo, per numero e per
posizione. Ci consente di creare loggetto passando una variet di inizializzatori; a
seconda del numero, tipo e ordine degli argomenti, il compilatore invocher il
corretto metodo costruttore con lo stesso numero, tipo e ordine dei parametri. Questo
approccio polimorfo definito come paradigma uninterfaccia, pi metodi; infatti,
grazie a esso il programmatore si potr riferire a un metodo ricordandosi
semplicemente il suo unico nome. A seconda degli argomenti passati (per differenza
di tipo, numero o posizione) sar il compilatore a invocare quello giusto.
Listato 7.8 Classe Time_REV_3.

...
public class Time_REV_3
{
public Time_REV_3() // costruttore senza argomenti
{
setTime(0, 0, 0);
}

public Time_REV_3(int h) // costruttore che inizializza solo l'ora


{
setTime(h, 0, 0);
}

public Time_REV_3(int h, int m) // costruttore che inizializza ora e minuti


{
setTime(h, m, 0);
}

public Time_REV_3(int h, int m, int s) // costruttore che inizializza ora,


// minuti e secondi
{
setTime(h, m, s);
}
// costruttore che inizializza un oggetto Time_REV_3
// attraverso un altro oggetto Time_REV_3
public Time_REV_3(Time_REV_3 t)
{
setTime(t.ora, t.minuti, t.secondi);
}
...
}

Nel Listato 7.8 notiamo che i vari costruttori sono stati scritti in overloading:
ognuno inizializza loggetto Time_REV_3 corrente a seconda di ci che passiamo come
argomento. Tra i vari costruttori interessante esaminare quello che ha come
parametro un oggetto di tipo Time_REV_3. Infatti, in questo caso il nostro oggetto
Time_REV_3 corrente avr come dati per lorario i dati delloggetto passato come
argomento, e tali dati saranno ricavati mediante un accesso diretto alle sue variabili
private. Ci possibile, e non si vola il principio delloccultamento delle variabili
private, poich quando si utilizza allinterno di una classe di un tipo un oggetto dello
stesso tipo, i suoi membri privati sono direttamente accessibili alla classe.
Listato 7.9 Classe Time_Client_REV_3.

...
public static void main(String[] args)
{
// invocazione dei vari costruttori di Time_REV_3
Time_REV_3 t = new Time_REV_3(4);
Time_REV_3 t2 = new Time_REV_3(18, 30);
Time_REV_3 t3 = new Time_REV_3(t2);

System.out.print("[t = " + t.getOra() + ":" + t.getMinuti() + ":"


+ t.getSecondi());
System.out.print("] [t2 = " + t2.getOra() + ":" + t2.getMinuti() + ":"
+ t2.getSecondi());
System.out.println("] [t3 = " + t3.getOra() + ":" + t3.getMinuti() + ":"
+ t3.getSecondi() + "]");
}

Output 7.3 Dal Listato 7.9 Classe Time_Client_REV_3.

[t = 4:0:0] [t2 = 18:30:0] [t3 = 18:30:0]


Metodi set e get: ripasso e dettaglio
Abbiamo gi visto che, quando si progetta una classe, opportuno fare in modo
che i membri con specificatore di accesso private vengano manipolati dal client
attraverso metodi pubblici detti convenzionalmente di get (interrogazione) e di set
(modifica). La progettazione di metodi get permette di far restituire al client i dati
cercati in una formattazione personalizzata e leggibile, mentre la progettazione di
metodi set consente di far impostare i dati eseguendo dei controlli su quali valori essi
possono accettare, effettuando, in pratica, un controllo di integrit e coerenza.
Listato 7.10 Classe Time_REV_4.

...
public class Time_REV_4
{
...
public void setTime(int o, int m, int s)
{
setOra(o);
setMinuti(m);
setSecondi(s);
}

public void setOra(int o) // imposto il campo ora


{
ora = (o < 24 && o >= 0) ? o : 0;
}

public void setMinuti(int m) // imposto il campo minuti


{
minuti = (m < 60 && m >= 0) ? m : 0;
}

public void setSecondi(int s) // imposto il campo secondi


{
secondi = (s < 60 && s >= 0) ? s : 0;
}
...
}

NOTA
La soluzione corrente adottata nellimplementazione dei metodi setOra, setMinuti e setSecondi
non certamente la migliore possibile, poich in caso di errore produce leffetto collaterale di
azzerare lora, cosa che potrebbe non essere corretta. Un approccio pi robusto alla gestione
degli errori verr discusso nel Capitolo 11, introducendo le asserzioni e le eccezioni.
Listato 7.11 Classe Time_Client_REV_4.

...
public static void main(String[] args)
{
Time_REV_4 t = new Time_REV_4();

// imposta singolarmente ora, minuti e secondi


t.setOra(18); t.setMinuti(30); t.setSecondi(25);
System.out.println("t = " + t.getOra() + ":" + t.getMinuti() + ":" + t.getSecondi());
}

Output 7.4 Dal Listato 7.11 Classe Time_REV_4.

t = 18:30:25
Dal Listato 7.10 si pu notare come sia cambiata limplementazione del metodo
setTime, che ora non esegue pi il controllo delle variabili al suo interno, ma lo delega

ad altri metodi set separati. Tuttavia, linterfaccia di setTime restata invariata nei
confronti del client, per il quale ci che accade dietro le quinte non deve avere
importanza, poich gli interessa solo il risultato che vuole ottenere quando utilizza i
metodi della classe. Questo approccio alla programmazione un buon esempio di
ingegneria del software dove si deve sempre cercare di mantenere uguale linterfaccia
di un metodo agendo (laddove possibile) sulla sua implementazione.
Membri di una classe costanti
I dati membro possono essere dichiarati come costanti e diventare, pertanto, non
pi modificabili dopo la loro inizializzazione. Per fare ci si deve usare il
qualificatore final prima del tipo del membro. Dichiarando dati membro privati e
costanti si rafforza il principio del minor privilegio, rendendoli di fatto membri
totalmente oscurati al client e assolutamente non modificabili. Un dato membro
costante pu essere inizializzato contestualmente alla sua dichiarazione, oppure dal
costruttore della sua classe.
Listato 7.12 Classe Time_REV_5.

...
public class Time_REV_5
{
...
private final String MSG = "Orario corrente: ";
...
public String toString()
{
return MSG + getTime();
}
}

Listato 7.13 Classe Time_Client_REV_5.

...
public static void main(String[] args)
{
Time_REV_5 t = new Time_REV_5(new Time_REV_5(20, 0, 0));
System.out.println(t);
}

Output 7.5 Dal Listato 7.13 Classe Time_Client_REV_5.

Orario corrente: 20:0:0

Nel Listato 7.12 abbiamo creato la costante MSG e labbiamo inizializzata


contestualmente alla sua dichiarazione con il valore "Orario corrente: ". Se non
avessimo inizializzato la costante MSG contestualmente alla sua dichiarazione,
avremmo dovuto obbligatoriamente farlo nei metodi costruttori, altrimenti avremmo
ottenuto il seguente errore di compilazione.
Errore 7.3 Mancata inizializzazione di una costante.
...Time_REV_5.java:15: error: variable MSG might not have been initialized5 errors
Oggetti come elementi di array
Si possono creare degli oggetti di una classe che sono rappresentati come elementi
di un array del suo tipo. Pertanto, riferendoci sempre alla nostra classe Time_REV_5,
potremmo creare un array di oggetti Time_REV_5 nel seguente modo.
Listato 7.14 Classe Time_Client_Array.

package com.pellegrinoprincipe;

public class Time_Client_Array


{
public static void main(String[] args)
{
// array di Time_REV_5
Time_REV_5 array_t1[] =
{
new Time_REV_5(10, 00, 00), new Time_REV_5(11, 00, 00),
new Time_REV_5(12, 00, 00)
};

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


{
System.out.print("{t1[" + i + "] " + array_t1[i].getOra() + ":"
+ array_t1[i].getMinuti() + ":"
+ array_t1[i].getSecondi() + "} ");
}
}
}

Output 7.6 Dal Listato 7.14 Classe Time_Client_Array.

{t1[0] 10:0:0} {t1[1] 11:0:0} {t1[2] 12:0:0}


Oggetti come membri di una classe
Una classe pu avvalersi di variabili di istanza che si riferiscono ad altri oggetti.
Questa capacit detta composizione ed uno dei modi di utilizzare la
programmazione orientata agli oggetti (e con essa il riuso del software). Quando un
oggetto viene usato come membro di un altro oggetto, ed entrambi sono parte dello
stesso package, allora loggetto che lo utilizza non lo deve importare. Il compilatore e
poi linterprete, quando incontreranno un riferimento delloggetto inserito per
composizione, cercheranno automaticamente il suo .class nella stessa directory dove
si trova il .class della classe che lo utilizza.
ATTENZIONE
Quando delle classi appartengono allo stesso package, i membri delloggetto riferito potranno
essere manipolati direttamente senza metodi appropriati di set e get, se non si esplicitano per
essi degli specificatori di accesso (public, private o protected). Ovviamente consigliabile
evitare questo approccio per soddisfare sempre e pienamente il principio delloccultamento delle
informazioni.
Listato 7.15 Classe Man.

package com.pellegrinoprincipe;

public class Man


{
private String cognome;
private String nome;
private Time_REV_5 working_time; // oggetto di tipo Time_REV_5

public Man(String c, String n, int o)


{
cognome = c;
nome = n;
working_time = new Time_REV_5(o); // impostazione dell'orario
}

public String toString()


{
return cognome + " " + nome + " va a lavorare alle ore: "
+ working_time.getOra();
}
}

Listato 7.16 Classe Man_Client.

package com.pellegrinoprincipe;

public class Man_Client


{
public static void main(String[] args)
{
Man a_man = new Man("Principe", "Pellegrino", 8);
System.out.println(a_man);
}
}

Output 7.7 Dal Listato 7.16 Classe Man_Client.

Principe Pellegrino va a lavorare alle ore: 8


Notiamo che nella classe Man stata dichiarata una variabile di istanza privata che si
riferisce a un oggetto di tipo Time_REV_5. Nel programma Man_Client, invece, si creato
un oggetto di tipo Man a cui si e passato, tra gli altri, anche un valore che rappresenter
un orario con cui sar inizializzato lorario delloggetto di tipo Time_REV_5 creato
allinterno del costruttore della classe Man.
Oggetti come parametri
Gli oggetti possono essere passati come argomenti ai metodi e possono anche
essere restituiti. In entrambi i casi il metodo potr modificare loggetto passato
perch, ricordiamo, gli oggetti vengono passati per riferimento.
Listato 7.17 Classe Time_Modify_Args.

package com.pellegrinoprincipe;

public class Time_Modify_Args


{
public static void modify(Time_REV_5 t_p)
{
t_p.setTime(11, 00, 00);
}

public static void main(String args[])


{
Time_REV_5 t = new Time_REV_5(18, 30, 0);
System.out.print("{t = " + t.getOra() + ":" + t.getMinuti() + ":"
+ t.getSecondi() + "} ");

modify(t); // modifichiamo

System.out.println("{t modificato = " + t.getOra() + ":" + t.getMinuti() + ":"


+ t.getSecondi() + "}");
}
}

Output 7.8 Dal Listato 7.17 Classe Time_Modify_Args.

{t = 18:30:0} {t modificato = 11:0:0}


La keyword this
Ogni oggetto creato ha una sorta di variabile, identificata con la keyword this, che
ha come valore un riferimento alloggetto stesso. Quando usiamo un metodo o una
variabile allinterno di una classe, implicitamente il compilatore vi antepone tale
keyword. Si pu anche esplicitare la keyword this, se si ritiene che in questo modo il
codice risulti pi leggibile, oppure se allinterno di un metodo si deve usare una
variabile di istanza che ha lo stesso nome di una sua variabile locale.
La keyword this si pu utilizzare anche per permettere un meccanismo detto di
chiamate di metodo a cascata.
Listato 7.18 Classe Time_REV_6.

...
public class Time_REV_6
{
...
public Time_REV_6 setOra(int o) // imposto l'ora e ritorno il riferimento this
{
ora = (o < 24 && o >= 0) ? o : 0;
return this;
}

public Time_REV_6 setMinuti(int m) // imposto i minuti e ritorno il riferimento this
{
minuti = (m < 60 && m >= 0) ? m : 0;
return this;
}

public Time_REV_6 setSecondi(int s) // imposto i secondi e ritorno il riferimento this


{
secondi = (s < 60 && s >= 0) ? s : 0;
return this;
}
...
}

Nel Listato 7.18 i metodi setOra, setMinuti e setSecondi sono stati modificati per
ritornare un riferimento alloggetto corrente istanziato.
Listato 7.19 Classe Time_Client_REV_6.
...
public static void main(String[] args)
{
Time_REV_6 time1 = new Time_REV_6();
System.out.println(time1.setOra(18).setMinuti(30).setSecondi(20)); // imposto
// a cascata
// l'orario
}

Output 7.9 Dal Listato 7.19 Classe Time_Client_REV_6.


Orario corrente: 18:30:20

Dal Listato 7.19 si pu notare come, quando si manda in stampa lorario


delloggetto time1, tutti gli attributi dellorario siano stati valorizzati, poich, quando
si invoca time1.setOra(18), il metodo ritorna loggetto time1 su cui si sta agendo; poi
sullo stesso oggetto viene invocato setMinuti(30) come time1.setMinuti(30), che ritorna
ancora time1, sui cui si invoca setSecondi(20) come time1.setSecondi(20).

Questa tecnica di accesso alloggetto corrente in cascata resa possibile, senza


forzare uneventuale assegnazione del risultato, poich loperatore punto (.)
associando da sinistra verso destra ritorna sempre il riferimento this che poi usato
per la valutazione dellespressione successiva.
ATTENZIONE
La keyword this pu essere utilizzata solamente nellambito di metodi di istanza, costruttori,
inizializzatori di istanza e inizializzatori di variabili di istanza.
TERMINOLOGIA
Un inizializzatore di variabile di istanza unespressione che valorizza una variabile di istanza,
mentre un inizializzatore di istanze, definito anche blocco per linizializzazione delle istanze, un
blocco di codice, eseguito solo una volta quando viene creata unistanza di una classe, al cui
interno sono poste delle istruzioni che inizializzano le variabili di istanza ed eseguono
espressioni.
Listato 7.20 Classe Initializers.

package com.pellegrinoprincipe;

class A_Class
{
public int number = 3233; // il valore 3233 l'inizializzatore di istanza
private int x;
private int y;
private int z;

// blocco di codice per inizializzare pi istanze


{
x = 11;
y = 12;
z = 13;
number = x + y + z + foo();
}

public A_Class(int val) // costruttore


{
number += val * 2;
}

public A_Class(int val1, int val2) // altro costruttore


{
number += (val1 + val2) * 2;
}

public int foo()


{
return 100;
}
}

public class Initializers


{
public static void main(String[] args)
{
// creo un oggetto di tipo A_Class invocando il costruttore a un argomento
A_Class an_obj = new A_Class(3);
System.out.print(an_obj.number);

// creo un oggetto di tipo A_Class invocando il costruttore a due argomenti


A_Class an_obj_2 = new A_Class(3, 2);
System.out.println(" e " + an_obj_2.number);
}
}

Output 7.10 Dal Listato 7.20 Classe Initializers.

142 e 146

Il Listato 7.20 mostra che il blocco di inizializzazione delle variabili di istanza


esegue le inizializzazioni delle variabili x, y e z e inoltre valuta unespressione il cui
risultato posto nella variabile number. La stessa variabile number poi utilizzata, in
ogni costruttore della classe A_Class, per lesecuzione di altri calcoli.
NOTA
Il codice di inizializzazione delle variabili di istanza, cos come quello per il calcolo di espressioni,
pu certamente essere scritto nei costruttori, ma un blocco di inizializzazione consente di scrivere
una sola volta del codice che altrimenti si dovrebbe scrivere in tutti i costruttori che ne volessero
far uso, e viene sempre eseguito indipendentemente dalla presenza dei medesimi costruttori. Nel
nostro esempio, infatti, se non avessimo avuto a disposizione un blocco di inizializzazione,
avremmo dovuto scrivere lespressione number = x + y + z + foo() in tutti i costruttori, creando
uninutile ridondanza nel codice.
Membri di una classe statici
Una classe pu avere, oltre che dei membri di istanza, anche dei membri definiti
come statici, che esistono indipendentemente dalloggetto che ne istanza.
La principale discriminante tra i due tipi risiede nel fatto che mentre ogni oggetto
avr una sua copia privata dei membri di istanza, i membri di una classe statici
saranno invece condivisi da tutti gli oggetti sua istanza. Ci significa che, se una
classe ha un membro non statico denominato per esempio color, ogni sua istanza avr
una copia di quel membro che potr assumere differenti valori, ognuno indipendente
dai valori degli altri oggetti. Se, invece, la stessa classe ha un membro statico
denominato per esempio where_to_buy, allora tutti gli oggetti ne condivideranno il
valore, e se qualcuno lo modificher, tale modifica sar accessibile anche agli altri
oggetti.
Sintassi 7.2 Definizione di un membro statico.

public static int var_name; // variabile di classe


public static void foo() {}; // metodo di classe

La Sintassi 7.2 mostra che, per definire un membro di una classe come statico,
dobbiamo utilizzare la keyword static, prima della specificazione del tipo di dato nel
caso di una variabile e prima del tipo di ritorno nel caso di un metodo.
Un membro statico si utilizza, invece, scrivendo il nome della classe di
appartenenza, loperatore punto (.) e il suo identificatore.
Snippet 7.3 Accesso a un membro statico.

Math.random();

Lo Snippet 7.3 utilizza il metodo statico random appartenente alla classe Math.
NOTA
Si pu accedere a un membro statico anche attraverso un riferimento a un oggetto istanza della
sua classe. Comunque preferibile accedere a un membro statico attraverso il nome della sua
classe di appartenenza, al fine di rendere pi evidente la sua natura.

Quando utilizziamo dei membri statici dobbiamo prestare attenzione alle seguenti
regole.

Se un membro qualificato come private e static, vi si potr accedere solo


tramite un apposito metodo public e static.
Un metodo static non pu invocare un metodo o utilizzare una propriet non
static e utilizzare il riferimento this o super, poich tali membri esistono solo se
esiste il loro oggetto.

TERMINOLOGIA
La keyword super utilizzata per fare riferimento, allinterno di una classe derivata, alla sua
classe base.
Listato 7.21 Classe Man_REV_1.
...
public class Man_REV_1
{
...
public static int how_many; // membro statico

public Man_REV_1(String c, String n, int o)


{
...
// incrementa la variabile per sapere quanti oggetti di tipo Man_REV_1
// sono stati creati
how_many++;
}
...
}

Nella classe Man_REV_1 del Listato 7.21 viene aggiunta la variabile di classe how_many,
che terr traccia di quanti oggetti di tipo Man_REV_1 vengono creati, perch a ogni
creazione delloggetto di tipo Man_REV_1 il costruttore incrementer la stessa variabile
how_many di una unit.
Listato 7.22 Classe Man_Client_REV_1.

...
public static void main(String[] args)
{
// creo tre istanze di Man_REV_1
Man_REV_1 a_man1 = new Man_REV_1("Principe", "Pellegrino", 8);
Man_REV_1 a_man2 = new Man_REV_1("Rizzo", "Stefano", 9);
Man_REV_1 a_man3 = new Man_REV_1("Rossi", "Alberto", 10);

// stampo quante istanze sono state create


System.out.println("Sono stati creati " + Man_REV_1.how_many + " uomini");
}

Output 7.11 Dal Listato 7.22 Classe Man_Client_REV_1.

Sono stati creati 3 uomini

Oltre a metodi e propriet statiche, abbiamo anche la possibilit di definire interi


blocchi di codice statici, dove in genere vengono eseguite istruzioni di
inizializzazione per i dati statici di una classe.
Listato 7.23 Classe StaticBlock.
package com.pellegrinoprincipe;

class StaticClass
{
static int a = 12;
static int b;
final static String msg;

static void foo(int x)


{
System.out.print("{x = " + x + " a = " + a + " a/b = " + (a / b) + "} ");
}

static // blocco static


{
msg = "Inizializzazione: ";
System.out.print(msg);
b = 4;
}
}

public class StaticBlock


{
public static void main(String[] args)
{
StaticClass.foo(42);
StaticClass.foo(45);
}
}

Output 7.12 Dal Listato 7.23 Classe StaticBlock.

Inizializzazione: {x = 42 a = 12 a/b = 3} {x = 45 a = 12 a/b = 3}

Il Listato 7.23 definisce la classe StaticClass che ha un blocco statico, creato con la
keyword static, al cui interno stato scritto del codice che sar eseguito quando la
classe sar caricata in memoria. Tale codice, tra le altre cose, inizializzer la costante
msg e la variabile b. Linizializzazione della variabile b di fondamentale importanza

per il nostro programma, poich successivamente il programma client utilizzer il


metodo foo della classe StaticClass, dove verr eseguita unoperazione di divisione tra
la variabile a e la variabile b. Se non avessimo provveduto a inizializzare la variabile b
con un valore congruo, essa sarebbe stata automaticamente inizializzata con il valore
0, che avrebbe poi generato uneccezione di divisione per 0.

ATTENZIONE
Le costanti statiche devono essere inizializzate contestualmente alla loro dichiarazione o in un
blocco static.

Rileviamo, infine, che il blocco static stato eseguito solo una volta (quando
stata caricata in memoria la classe), e infatti le successive invocazioni del metodo foo
non faranno pi eseguire il codice del blocco static tra cui c la stampa del
messaggio "Inizializzazione".
Classi annidate
Una classe si pu definire allinterno di unaltra classe. In tal caso la classe interna
(annidata) pu riferirsi direttamente ai membri della classe che la contiene, ma la
classe contenitore non ha accesso ai membri della medesima classe annidata. Se la
classe interna static, allora potr accedere direttamente solo ai membri static della
classe contenitore.
Listato 7.24 Classe InnerClass.

package com.pellegrinoprincipe;

class Host
{
private String show1 = "Valore di 'a' della classe LC definita nel metodo local"
+ " della classe Host = ";
private String show2 = "Valore di 'abc' del metodo local della classe Host mostrato"
+ " dal metodo show\n" + "della classe LC definita nel metodo"
+ " Local della classe Host = ";
private int outer_x = 100;

void doIt()
{
Guest inner = new Guest(); // istanza classe annidata
inner.display();
}

void local()
{
final int abc = 11;

// definizione di una classe locale a una funzione


class LC
{
int a;
LC() { a = 1000; }
void show() {System.out.println(show1 + a + "\n" + show2 + abc + "\n");}
}

LC a_local = new LC();


a_local.show();
}

// DEFINIZIONE classe ANNIDATA


class Guest
{
private String show1 = "Valore 'outer_x' di Host mostrato dal metodo display"
+" della classe" + " Guest ad esso annidata = ";

void display()
{
System.out.print(show1 + outer_x + "\n");
}
}
}

public class InnerClass


{
public static void main(String args[])
{
Host outer = new Host();
outer.doIt();
outer.local();
}
}

Output 7.13 Dal Listato 7.24 Classe InnerClass.


Valore 'outer_x' di Host mostrato dal metodo display della classe Guest ad esso annidata = 100
Valore di 'a' della classe LC definita nel metodo local della classe Host = 1000
Valore di 'abc' del metodo local della classe Host mostrato dal metodo show
della classe LC definita nel metodo local della classe Host = 11

Dallanalisi strutturale del Listato 7.24 vediamo che abbiamo definito una classe
denominata Host al cui interno stata definita una classe annidata denominata Guest.
Abbiamo poi definito nel metodo local della classe Host unaltra classe denominata LC,
a dimostrazione che allinterno di una classe si pu definire ovunque una classe
annidata.
Dallanalisi pratica del Listato 7.24 vediamo che il metodo main crea un oggetto
(outer) di tipo Host e ne invoca il metodo doIt(), il quale crea unistanza della classe
Guest e poi invoca il suo metodo display() che accede direttamente alla variabile outer_x
della classe Host. Successivamente, il medesimo metodo main invoca anche il metodo
local di outer, che crea loggetto a_local di tipo LC e ne invoca il metodo show che
stampa i valori della variabile abc locale al metodo local e della variabile a, variabile
di istanza di a_local medesimo. Precisiamo anche che loggetto a_local non visibile
agli altri client, ma solo allinterno del metodo local dove stato definito.

Quando si progetta una classe interna necessario prestare attenzione a quanto


segue.

La compilazione di una classe che contiene classi interne genera dei file .class
separati per ognuna di queste. Nel caso di classi non anonime i file avranno
nomi come NomeClasseEsterna$NomeClasseInterna.class, mentre nel caso di classi
anonime avranno nomi del tipo: NomeClasseEsterna$#.class, dove il simbolo # un
numero che parte da 1 ed incrementato per ogni classe anonima incontrata, in
successione.
Il riferimento this della classe esterna NomeClasseEsterna.this.
Per creare un oggetto di una classe annidata direttamente tramite un riferimento
di una classe che la contiene si deve usare una sintassi come quella che segue.

Sintassi 7.3 Creazione di un oggetto di classe interna.


NomeClasseEsterna.NomeClasseInterna i = ref.new NomeClasseInterna()

dove ref un riferimento a un oggetto della classe contenitore.


Una classe interna pu essere dichiarata statica scrivendo il modificatore static
prima della keyword class; in questo modo si ottiene una classe accessibile
direttamente senza passare per un riferimento alla sua classe esterna, poich tale
modificatore la fa divenire una top-level class.
Tipi enumerati
I tipi enumerati, o enumerazioni, sono stati introdotti a partire dalla versione 5 di
Java e possono essere definiti, in prima approssimazione, come dei tipi di dato che
hanno come membri un insieme di identificatori che rappresentano valori costanti.
Un tipo enumerato, nella sua forma pi semplice, si crea utilizzando la seguente
sintassi.
Sintassi 7.4 Tipo enumerato: forma semplice.

enum Name
{
Value1,
Value2
}

Alla keyword enum fa seguito il nome dellenumerazione e poi nel suo corpo vanno
dichiarati i valori che essa rappresenta.
Unenumerazione , come gi detto, un nuovo tipo di dato, e come tale pu essere
considerata come una sorta di classe o di interfaccia. Infatti, al suo interno possiamo
creare, oltre ai valori costanti, anche dei metodi, dei costruttori e delle variabili
ordinarie.
Sintassi 7.5 Tipo enumerato: forma complessa.
enum Name
{
Value1,
Value2;
Name(int p){} // costruttore
public int var = 10; // variabile di istanza
public void foo(){} // altro metodo
}

Quando si progetta unenumerazione bisogna considerare che:

implicitamente final e pertanto non pu essere estesa da unaltra classe;


convertita automaticamente dal compilatore in una classe che estende la
superclasse Enum del package java.lang;
non si pu creare unistanza di essa con loperatore new;
una variabile di tipo enumerazione pu aver assegnato come valore solo i nomi
delle costanti dellenumerazione stessa;
i suoi valori sono implicitamente final, public e static e non possono essere
dichiarati altrimenti;
i suoi valori non sono paragonabili a valori interi e pertanto non possibile
effettuare comparazioni o assegnamenti con essi;
i valori costanti devono essere scritti sempre come prime istruzioni. Non
possibile, per esempio, scrivere nel corpo di un enum prima dei metodi e poi i
valori costanti;
ogni valore costante nella realt un oggetto del tipo di enumerazione dove
stato dichiarato;
la dichiarazione dellultimo valore costante deve terminare con un punto e
virgola se sono presenti altri dati membro o metodi.

Ora spieghiamo i punti precedenti esaminando il codice sorgente decompilato


dellenumerazione del Listato 7.25.
Listato 7.25 Classe EnumInAction.

package com.pellegrinoprincipe;

enum OS
{
WINDOWS("XP"),
LINUX("RedHat"),
MAC("Jaguar"); // qui punto e virgola IMPORTANTE

private final String title;

OS(String t) { title = t; }

public String getTitle() { return title; }


}

public class EnumInAction


{
public static void main(String[] args)
{
System.out.print("Tipi di OS: ");

for (OS tmp : OS.values()) // eseguiamo un ciclo nell'array di OS


System.out.print("[ " + tmp + " titolo " + tmp.getTitle() + " ]");

System.out.print("\nOS scelto: ");

OS a_s = OS.MAC; // assegniamo un valore di OS


switch (a_s) // stampiamo quello scelto
{
case WINDOWS:
System.out.println("Windows ");
break;
case LINUX:
System.out.println("Linux ");
break;
case MAC:
System.out.println("Mac ");
break;
default:
break;
}
}
}

Output 7.14 Dal Listato 7.25 Classe EnumInAction.

Tipi di OS: [ WINDOWS titolo XP ][ LINUX titolo RedHat ][ MAC titolo Jaguar ]
OS scelto: Mac

Decompilato 7.1 File OS.class.


final class OS extends Enum
{
public static OS[] values() { return (OS[])$VALUES.clone(); }

public static OS valueOf(String name)


{
return (OS)Enum.valueOf(com/pellegrinoprincipe/OS, name);
}

private OS(String s, int i, String t)


{
super(s, i);
title = t;
}
public String getTitle() { return title; }
public static final OS WINDOWS;
public static final OS LINUX;
public static final OS MAC;
private final String title;
private static final OS $VALUES[];

static
{
WINDOWS = new OS("WINDOWS", 0, "XP");
LINUX = new OS("LINUX", 1, "RedHat");
MAC = new OS("MAC", 2, "Jaguar");
$VALUES = (new OS[] { WINDOWS, LINUX, MAC });
}
}

Il Decompilato 7.1 evidenzia le seguenti trasformazioni e aggiunte effettuate dal


compilatore sullenumerazione scritta nel Listato 7.25:

la keyword enum stata sostituita dalla definizione di una classe di nome OS che
estende la classe base astratta Enum;
sono state create delle costanti, statiche e pubbliche, di tipo OS, una per ogni
identificatore;
stata creata una variabile di tipo array di oggetti OS denominata $VALUES[];
il costruttore OS stato modificato in un costruttore che ha due parametri in pi
rispetto a quello definito dallenumeratore stesso. Infatti, il primo parametro e il
secondo parametro servono per invocare il costruttore della classe base Enum e
indicano rispettivamente il nome della costante enumerativa e la sua posizione
ordinale;
stato creato un metodo statico values che ritorna un array di oggetti di tipo OS e
che ha tanti elementi quante sono le costanti enumerative;
stato creato un metodo statico valueOf che ritorna un oggetto di tipo OS a
seconda della stringa passata come argomento. Ci significa che se scriviamo
unistruzione come: OS o = OS.valueOf("WINDOWS"), il riferimento o conterr un
oggetto di tipo OS che rappresenter lidentificatore WINDOWS;
stato scritto linizializzatore static al cui interno sono stati creati tanti oggetti OS
quante sono le costanti enumerative.
Dalla disamina dei suddetti punti possiamo affermare, in sintesi, che
unenumerazione altro non che una classe Java e che ogni valore costante dichiarato
mappato in un oggetto dellenumerazione stessa. Per quanto attiene alluso
dellenumerazione notiamo come nel metodo main della classe EnumInAction utilizziamo
un ciclo for per scorrere tutti gli oggetti di tipo OS, e per ciascuno ne otteniamo il
titolo. Vediamo, inoltre, che una variabile di tipo OS pu contenere come valore solo
un valore uguale alle costanti dichiarate (OS a_s = OS.MAC) e ci perch, ripetiamo, ogni
valore costante , in effetti, un oggetto dello stesso tipo dellenumeratore definito.
Infatti, MAC altro non che un oggetto di tipo OS e, pertanto, lecitamente assegnabile
alla variabile a_s anchessa di tipo OS.

Infine, interessante rilevare, come il costrutto switch sia in grado di valutare


lenumerazione e confrontarla con le relative etichette case.
Capitolo 8
Programmazione orientata agli
oggetti

La programmazione orientata agli oggetti (Object-Oriented Programming, OOP)


permette di creare relazioni gerarchiche tra classi. Essa si fonda su due importanti
concetti: lereditariet e il polimorfismo. Lereditariet permette a una classe di avere,
come se fossero i suoi, i dati membro e i metodi di unaltra classe. Infatti, la classe da
cui si ereditano tali membri detta superclasse o classe base, mentre la classe che
eredita detta sottoclasse o classe derivata. Dalla superclasse si possono ereditare
solo i membri definiti public, protected e senza specificatore di accesso se la classe
derivata fa parte dello stesso package. In Java supportata solo lereditariet singola:
una classe pu avere una sola superclasse. Lereditariet multipla supportata
attraverso le interfacce, concetto che sar discusso pi avanti.
Il polimorfismo, invece, consente di scrivere codice generico grazie a meccanismi
dinamici di riconoscimento del tipo di classe che sta invocando un determinato
metodo. In breve possiamo dire che tra la classe base e la classe derivata esiste una
relazione grazie alla quale una classe derivata anche una classe base (relazione is-a)
e pu sovrascrivere (overriding) dei metodi ereditati da una classe base.
Combinando questi concetti otteniamo che una classe base pu contenere il
riferimento di una sua classe derivata e attraverso il suo riferimento (della classe
base) invocare un metodo che sar chiamato in fase di run-time sulla classe derivata
riferita. Questo modo di agire, per lappunto polimorfo (poich la classe base pu
assumere le forme delle sue diverse classi derivate), viene attuato grazie a tecniche
sofisticate dette di binding dinamico.
Gerarchie di classi ed ereditariet
Lereditariet, come gi detto, permette di creare delle gerarchie di classi: una
classe definita come A pu diventare sottoclasse di unaltra classe, definita come B,
che la sua superclasse, direttamente oppure indirettamente se la stessa sottoclasse A
derivata a un livello pi basso. Presentiamo subito un esempio relativo alla
creazione di una gerarchia di classi che prende spunto dalle forme geometriche.
Si pu partire definendo una classe base chiamata Shape. Poi si possono avere due
sottoclassi specializzate, chiamate TwoDShape e ThreeDShape. Per TwoDShape si possono avere
le classi derivate Circle, Rectangle (da cui deriva ulteriormente la classe Square) e
Triangle, mentre per ThreeDShape si possono avere le classi derivate Cube e Sphere (Figura
8.1).

Figura 8.1 Gerarchia di forme geometriche.

Nella Figura 8.1 le frecce indicano la relazione. In questo caso la classe Circle
una sottoclasse della classe TwoDShape, che a sua volta una sottoclasse della classe
Shape. Si pu anche dire che la classe Circle indirettamente una sottoclasse della
classe Shape, mentre la classe TwoDShape direttamente una sottoclasse della classe Shape.

Inoltre, sempre prendendo come riferimento la classe Circle, essa eredita tutti i
membri pubblici e protetti sia della classe Shape sia della classe TwoDShape. Infine,
ricordiamo che tutte le classi Java derivano implicitamente dalla classe Object.

Vediamo come si implementa nella pratica la struttura gerarchica della Figura 8.1,
prendendo come esempio la gerarchia della struttura 2D e non considerando, per ora,
le classi Shape e TwoDShape.
Listato 8.1 Classe Point2D.

package com.pellegrinoprincipe;

public class Point2D


{
private int x;
private int y;

// costruttori
public Point2D() { x = y = 0; }
public Point2D(int x, int y)
{
this.x = x;
this.y = y;
}

protected void finalize() {} // invocato dal garbage collector
protected int getX() { return x; }
protected int getY() { return y; }

// overriding di toString da Object


public String toString()
{
return "[ " + x + " , " + y + " ]";
}
}

Il Listato 8.1 definisce la classe Point2D, che rappresenta un punto generico nel
piano con due variabili di istanza di tipo intero denominate x e y. Essa ha come sua
superclasse Object, che ereditata per default, ed esegue loverriding del metodo
toString ereditato da Object stessa, al fine di stampare loggetto che rappresenta in
modo intellegibile e significativo.
importante sottolineare che questa classe, nonostante non faccia parte della
gerarchia delle forme geometriche, indispensabile per le altre classi, perch usata
per composizione (per esempio dalla classe Rectangle) al fine di impostare o ottenere le
coordinate x e y della figura che si intende manipolare e disegnare.

Ricordiamo che la composizione, detta anche has-a relationship (relazione ha un),


unaltra tecnica di programmazione per il riuso del codice che consente a una classe
di avere al suo interno dei dati membro che sono dei riferimenti a oggetti di altre
classi.

OVERRIDING
Loverriding, o sovrascrittura, una procedura mediante la quale, in una classe derivata, si
ridefinisce un metodo di una classe base cambiandone limplementazione. La ridefinizione avviene,
in pratica, scrivendo nella classe derivata il metodo della classe base, con lo stesso tipo di ritorno,
lo stesso nome e gli stessi parametri che, ricordiamo, nel complesso (eccetto per il tipo di ritorno)
ne rappresentano la segnatura.
Continuando lesame del listato notiamo come, nel costruttore Point2D(int x, int y),
lassegnamento degli argomenti x e y venga fatto alle corrispondenti variabili di
istanza con this.x = x e this.y = y, e non semplicemente con x = x e y = y. Luso di this
necessario poich ricordiamo che i parametri di un metodo sono considerati come
variabili locali al metodo stesso, pertanto un assegnamento come x = x non farebbe
altro che assegnare il valore del parametro x a se stesso.

Infine, interessante rilevare la presenza (non obbligatoria) di un metodo


denominato finalize che ha lo scopo di eseguire operazioni di finalizzazione (per
esempio la chiusura di una risorsa di sistema utilizzata) prima che loggetto venga
distrutto dal garbage collector ( un meccanismo software che si occupa di gestire
automaticamente la memoria liberandola dalle risorse inutilizzate, ovvero dagli
oggetti che non sono pi referenziati).
Listato 8.2 Classe Rectangle.

package com.pellegrinoprincipe;

public class Rectangle


{
protected int width;
protected int height;
protected Point2D upperleftCoords;

// costruttori
public Rectangle()
{
width = height = 1; // rettangolo con larghezza e altezza di un'unit
upperleftCoords = new Point2D(0, 0); // posizione di default
}

public Rectangle(Point2D upperleftCoords, int width, int height)


{
this.width = width;
this.height = height;
this.upperleftCoords = upperleftCoords;
}

protected void finalize() {} // invocato dal garbage collector
public int getWidth() { return width; }
public int getHeight() { return height;}
public Point2D getCoords() { return upperleftCoords; }
public int area() { return width * height; }
public int perimeter() { return 2 * width + 2 * height; }

public String toString()
{
return "RETTANGOLO { " + upperleftCoords + " --> Larghezza: " + width + ", "
+ "Altezza: " + height + " } "; }
}

Il Listato 8.2 definisce la classe Rectangle, che rappresenta un generico rettangolo,


costruibile attraverso un costruttore di default che lo crea alle coordinate x = 0 e y = 0
e con altezza e larghezza pari a ununit, e con un costruttore che lo crea con le
coordinate x, y e laltezza e la larghezza uguali ai valori ricavati dai rispettivi
parametri. Notiamo, inoltre, come vi siano dei metodi di tipo get (getWidth, getHeight e
getCoords), che consentono di reperire i valori dei rispettivi membri privati, e i metodi
area e perimeter, che permettono invece di ritornare i valori dellarea e del perimetro di
un rettangolo.
Listato 8.3 Classe Square.

package com.pellegrinoprincipe;

public class Square extends Rectangle


{
public Square() { width = 1;}
public Square(Point2D upperleftCoords, int side)
{
super(upperleftCoords, side, side);
}

protected void finalize() {} // invocato dal garbage collector


public int getSide() { return width; }

public String toString()


{
return "QUADRATO { " + upperleftCoords + " --> Lato: " + width + " }";
}
}

Il Listato 8.3 definisce la classe Square che eredita dalla classe Rectangle. Tale
relazione di ereditariet viene creata grazie allausilio della keyword extends che
indica, per lappunto, quale deve diventare la classe base della classe che la utilizza.
Tale ereditariet fa s che la classe Square abbia, come se fossero suoi, tutti i membri
public e protected della classe genitrice Rectangle (e indirettamente anche quelli della
classe Object da cui Rectangle deriva).
ATTENZIONE
importante rammentare che, in una relazione di ereditariet, se un metodo di una sottoclasse
ha lo stesso nome di un metodo della sua superclasse, ma non gli stessi parametri, si avr non
loverriding, bens loverloading.

Continuando lesame della definizione della classe Square vediamo che essa ha due
costruttori: uno di default senza argomenti e un altro che prende come argomenti le
coordinate del piano e il valore del lato del quadrato. Ricordiamo che ogni classe
deve avere dei costruttori; se non si scrivono, il compilatore provveder
automaticamente a fornire costruttori impliciti di default senza argomenti. Quando vi
una relazione di ereditariet, per assicurare una corretta inizializzazione delle
istanze di classe derivata bisogna prestare attenzione a che il costruttore della classe
derivata richiami come prima istruzione il costruttore equivalente della sua classe
base.
Nel caso di costruttori di default dobbiamo ricordare che:
se si scrive un costruttore di default nella classe derivata, il compilatore non
scriver automaticamente nella classe base lequivalente costruttore di default;
pertanto, se non si provvede manualmente, si avr un errore in fase di
compilazione;
se entrambe le classi hanno costruttori di default, il compilatore provveder
automaticamente a invocare come prima istruzione il costruttore della classe
base; nel caso della classe Square, infatti, il metodo costruttore di default non ha
linvocazione esplicita al costruttore di default della classe Rectangle.

Nel caso di costruttori con argomenti, linvocazione deve essere esplicita; infatti,
nel caso della classe Square, nel metodo costruttore con argomenti avremo come prima
istruzione super(upperleftCoords, side, side) con cui invochiamo, per lappunto, il
costruttore della classe base Rectangle utilizzando la keyword super che rappresenta il
riferimento alla classe base di Square.
APPROFONDIMENTO
La keyword super importante, soprattutto, perch quando una sottoclasse sovrascrive un
metodo di una superclasse, tale metodo non direttamente accessibile nella sottoclasse. Per
accedere al metodo sovrascritto dobbiamo usare la keyword super, che ritorner un riferimento
alla classe base, e poi mediante questo invocare il metodo desiderato.
Listato 8.4 Classe Square_Client.

package com.pellegrinoprincipe;

public class Square_Client


{
public static void main(String args[])
{
Square a_square = new Square(new Point2D(22, 10), 10);
System.out.println("[X = " + a_square.getCoords().getX() + " Y = "
+ a_square.getCoords().getY()
+ "] --> Area = " + a_square.area() + ", Perimetro = "
+ a_square.perimeter());
}
}

Output 8.1 Dal Listato 8.4 Classe Square_Client.

[X = 22 Y = 10] --> Area = 100, Perimetro = 40

Il Listato 8.4 mostra un esempio di utilizzo di un oggetto di tipo Square.


Analizzando il codice sorgente vediamo che il riferimento a_square accede al metodo
getCoords definito nella sua classe base Rectangle, come se fosse il suo; ci possibile
proprio grazie alla relazione di ereditariet.

ECCEZIONI ALLEREDITARIET
Lereditariet pu subire delle eccezioni applicando il modificatore final sul costrutto di classe
oppure sul costrutto di metodo. Nel primo caso si utilizza final se si vuole che una classe non
possa essere estesa o ereditata (non potr mai avere delle sottoclassi); tutti i metodi della classe,
inoltre, saranno implicitamente final. Nel secondo caso si utilizza final se si vuole che un metodo
non possa essere sovrascritto. Inoltre, quando si indica un metodo come non sovrascrivibile si
manifesta la volont che lo stesso non potr subire delle modifiche nelle classi derivate
consentendo, generalmente, al compilatore di effettuare delle ottimizzazioni con cui il metodo
stesso verr reso inline. Ci significa che a ogni invocazione del metodo, il codice necessario per
effettuare la chiamata verr sostituito direttamente dal codice del metodo stesso (binding precoce).
Questo comportamento differisce da ci che avviene normalmente, quando linvocazione di un
metodo provocher un jump (salto) del run-time allindirizzo di memoria dove stato posto il codice
del metodo da eseguire (binding tardivo).

Sintassi 8.1 Keyword final su di una classe.

public final class A_Class {}

Sintassi 8.2 Keyword final su di un metodo.

public final void A_method() {}

NOTA
Se utilizziamo la keyword static su un metodo, di fatto lo rendiamo sia metodo di classe sia
metodo non sovrascrivibile.
Polimorfismo e binding dinamico
Listato 8.5 Classe Polimorfismo_Client.
package com.pellegrinoprincipe;

public class Polimorfismo_Client


{
public static void main(String args[])
{
Rectangle r = new Rectangle(new Point2D(10, 10), 5, 3); // un oggetto Rectangle
Square s = new Square(new Point2D(50, 50), 3); // un oggetto Square

Rectangle r2;
Square s2;

String output = "Un tipo Rectangle: " + r + " " + "\nUn tipo Square: " + s + "\n";
r2 = s; // assegno un tipo Square a un tipo Rectangle
output += "Un tipo Square tramite un riferimento di un tipo Rectangle: " + r2;
System.out.println(output);

output = "Verifichiamo il binding dinamico: ";


if (r2 instanceof Square) // r2 in effetti un tipo Square e lo riassegno
{
output += "*** r2 a run-time un tipo Square riassegniamolo a un tipo Square ***";
s2 = (Square) r2;
}
else
output += "r2 non un tipo Square!";
System.out.println(output);
}
}

Output 8.2 Dal Listato 8.5 Classe Polimorfismo_Client.


Un tipo Rectangle: RETTANGOLO { [ 10 , 10 ] --> Larghezza: 5, Altezza: 3 }
Un tipo Square: QUADRATO { [ 50 , 50 ] --> Lato: 3 }
Un tipo Square tramite un riferimento di un tipo Rectangle: QUADRATO { [ 50 , 50 ] --> Lato: 3 }
Verifichiamo il binding dinamico: *** r2 a run-time un tipo Square riassegniamolo a un tipo
Square ***

Nel Listato 8.5 si creano gli oggetti r, r2 di tipo Rectangle e s, s2 di tipo Square.
Successivamente in r2 = s si assegna un riferimento di un oggetto Square a un
riferimento di un oggetto Rectangle. Tale assegnamento possibile poich sempre
consentito assegnare un riferimento di una classe derivata a un riferimento di una
classe base (anche se linverso non automaticamente consentito), considerato che
una classe derivata sicuramente anche una classe base (contiene sicuramente
almeno i suoi membri derivabili).
APPROFONDIMENTO
Un riferimento a un oggetto di classe derivata pu sostituire in modo sicuro un riferimento a un
oggetto di classe base solamente se viene rispettato un principio tecnico detto principio di
sostituibilit di Liskov, dal nome della ricercatrice Barbara Liskov che per prima lo formalizz. Il
principio di sostituibilit dice che, per un qualsiasi programma, se S un sottotipo di T, allora
oggetti dichiarati di tipo T possono essere sostituiti con oggetti di tipo S senza alterare la
correttezza dei risultati del programma. Questo principio, interpretando le classi derivate
(sottoclassi) come sottotipi delle classi base (superclassi), pone alcuni importanti vincoli sulle
modalit con cui le prime ereditano o ridefiniscono i metodi delle seconde in una gerarchia
dereditariet. Pi in particolare:
i prerequisiti (precondizioni) richiesti a un metodo di superclasse devono essere
almeno altrettanto vincolanti di quelli richiesti al corrispondente metodo nelle
sottoclassi;
le garanzie (postcondizioni) fornite da un metodo in una sottoclasse devono
essere almeno altrettanto vincolanti di quelle fornite dal corrispondente metodo
nelle superclassi;
gli invarianti, ossia le propriet dello stato di una classe che devono sempre
valere, in una sottoclasse devono essere almeno altrettanto vincolanti di quelli
delle sue superclassi.
Il principio non ammette la presenza di eccezioni nelle sottoclassi; tutte le
buone gerarchie orientate agli oggetti dovrebbero rispettarlo.

Per quanto attiene alla nostra relazione di forme geometriche, soprattutto in riferimento alle classi
Square e Rectangle, in letteratura precisato che esse violano il principio di sostituibilit di Liskov
solo se sono previsti dei metodi di tipo set che ne cambiano le dimensioni. In effetti, se abbiamo
un oggetto di tipo Rectangle al quale cambiamo, per esempio, solo laltezza, ci non dovrebbe
provocare anche un cambiamento della sua larghezza. Se invece passiamo un riferimento di tipo
Square a un riferimento di tipo Rectangle e poi ne cambiamo laltezza, ci dovrebbe causare un
cambiamento anche della sua larghezza (un quadrato tale perch ha sempre altezza e
larghezza uguali); pertanto si vola il principio di Liskov dato che non possiamo sostituire in modo
sicuro un quadrato a un rettangolo. Nel nostro caso, comunque, non vi alcuna violazione
perch sono previsti solo metodi di tipo get che rendono, di fatto, immutabili gli oggetti delle classi
citate.

Dopo lassegnamento citato, listruzione: output += "Un tipo Square tramite un


riferimento di un tipo Rectangle " + r2 concatena alla stringa letterale il risultato del
metodo toString di r2 invocato implicitamente. Grazie al binding dinamico, il sistema
di run-time di Java invocher il metodo toString delloggetto riferito da r2, che non
sar un oggetto di tipo Rectangle, ma un oggetto di tipo Square (s). importante
precisare che in fase di compilazione verr anche effettuato un controllo per
verificare se il riferimento r2 conterr il metodo toString che sar poi invocato.

Continuando lesame del Listato 8.5 vediamo che lassegnamento di un riferimento


di una classe base a una classe derivata si pu effettuare solo previo esplicito cast,
passando come operando la classe derivata che si vuole assegnare. Nel nostro caso
avremo s2 = (Square) r2, dove loggetto r2 di tipo Rectangle viene convertito in un
oggetto di tipo Square e passato al riferimento s2 di tipo Square. Il cast precisiamo
necessario poich un riferimento di una classe base pu, grazie al polimorfismo,
riferire pi oggetti di classi derivate. Tuttavia, prima di effettuare il cast abbiamo
usato loperatore instanceof, con cui abbiamo chiesto al riferimento r2 di che tipo
fosse listanza che punta.
NOTA
Il problema che, usando gli oggetti di classe derivata attraverso un riferimento a una classe
base, non sappiamo a priori quale specifica classe derivata stiamo invocando. Il cast, unito
allistruzione instanceof, necessario per assicurarsi di effettuare la conversione corretta da
classe base a classe derivata. La conversione in senso opposto in generale sempre sicura,
purch valga il principio di sostituibilit di Liskov, per cui in quel caso i cast negli assegnamenti
non servono. Intuitivamente facile capire la differenza: un riferimento di classe derivata, per
esempio un Circle, dovrebbe poter essere sempre trattato come un generico oggetto Shape,
mentre non necessariamente tutti gli oggetti Shape possono essere trattati come se fossero
oggetti di tipo Circle.

Nel nostro caso, infatti, listruzione: if (r2 instanceof Square) ci assicura che
lassegnamento a un riferimento di tipo s2 = (Square) r2 verr effettuato solo se il
riferimento r2 , a run-time, effettivamente di tipo Square.

Spieghiamo ulteriormente i concetti di polimorfismo e di binding dinamico


esaminando le operazioni che dovremmo compiere se volessimo progettare un
sistema che consentisse alle nostre classi delle forme geometriche di disegnarsi sullo
schermo ciascuna con la propria differente logica. A tal fine usiamo anche il tipo Shape
(che per ora considerato una classe, ma la cui corretta implementazione verr
studiata pi avanti), al cui interno stato definito un metodo draw.

1. Definiamo nella classe Rectangle un suo specifico metodo di disegno denominato


draw (che andr a sovrascrivere il metodo draw della classe Shape ereditato
indirettamente tramite il tipo TwoDShape che eredita a sua volta da Shape).
2. Definiamo nella classe Square un suo specifico metodo di disegno denominato
draw (che andr a sovrascrivere il metodo draw della classe Rectangle ereditato).
3. Creiamo un oggetto di tipo Shape (per esempio s2D) e gli assegniamo un
riferimento di tipo Rectangle.
4. Invochiamo sul riferimento di tipo Shape (s2D) il metodo draw.
5. Assegniamo poi a s2D un riferimento di tipo Square.
6. Invochiamo sul riferimento di tipo Shape (s2D) il metodo draw.

Leggendo in ordine i punti precedenti vediamo che per creare un sistema


polimorfo dobbiamo per prima cosa definire in una classe derivata i metodi della
classe base che vogliamo sovrascrivere. Nel nostro caso, al punto 1 e al punto 2
definiamo un metodo draw che conterr la logica specifica di disegno di un oggetto di
tipo Rectangle e di un oggetto di tipo Square.
Come passo successivo assegniamo a un oggetto di una classe base un riferimento
a un oggetto di una sua classe derivata. Nel nostro caso il punto 3 e il punto 5
assegnano al riferimento s2D (che quindi assume differenti forme) prima un
riferimento di tipo Rectangle e poi un riferimento di tipo Square.

Infine, sul riferimento della classe base invochiamo il metodo desiderato. Nel
nostro caso, al punto 4 e al punto 6, il sistema di run-time di Java, grazie al binding
dinamico, scoprir qual loggetto di classe derivata riferito dalla sua classe base e
ne invocher il giusto metodo, ovvero il metodo draw, prima su un oggetto di tipo
e poi su un oggetto di tipo Square.
Rectangle
Classi astratte
Le classi astratte sono classi che non possono essere istanziate direttamente,
ovvero cui non si possono creare i relativi oggetti come si farebbe normalmente con
le classi concrete sin qui esaminate. Le classi astratte hanno generalmente, ancorch
non esclusivamente, dei metodi che sono anchessi definiti astratti e che sono privi di
un corpo di definizione: sono solo dichiarati, ma non forniscono alcuna
implementazione. Una classe astratta pu comunque essere derivata, e le classi che
derivano da essa devono implementarne gli eventuali metodi astratti sovrascrivendoli
con una propria logica specifica.
Sintassi 8.3 Classe astratta con un metodo astratto.

[modifiers] abstract class ClassName [extends implements]


{
public abstract void abstractMethod();
}

Dalla Sintassi 8.3 si vede che per definire una classe astratta si deve usare la
keyword abstract e che un metodo astratto se, oltre a essere stato definito come tale
(anchesso con la keyword abstract) anche solo dichiarato.

Di seguito elenchiamo alcuni punti importanti da ricordare per le classi astratte.

Una sottoclasse di una classe astratta deve obbligatoriamente implementarne gli


eventuali metodi astratti e, se non vi provvede, allora essa stessa deve divenire
classe astratta.
Una classe abstract pu avere anche variabili di istanza e metodi non abstract.
un errore di sintassi creare oggetti di una classe abstract. Se ne pu solo creare
un riferimento nullo a cui assegnare poi riferimenti di sue sottoclassi.

Nella Figura 8.2 mostriamo un esempio di ereditariet con una classe astratta
riferendoci a unazienda dove si trovano impiegate diverse figure professionali.

Figura 8.2 Gerarchia di impieghi.


Nella figura vediamo una classe Employee che sar la superclasse astratta (avr un
metodo astratto per il calcolo della paga denominato earning) e poi tre sottoclassi che
da essa deriveranno (Engineer, Technician e Laborer) e che definiranno in modo
specializzato il metodo earning. Infatti, un Engineer avr uno stipendio mensile che sar
dato da un importo fisso pi una percentuale, un Technician avr uno stipendio mensile
dato da un importo fisso pi un quantum in base ai pezzi lavorati e un Laborer avr
uno stipendio mensile dato da un importo a ore pi una percentuale su un numero
variabile di pezzi lavorati.
Da quanto detto appare chiaro che la classe Employee pu essere solo astratta: che
senso avrebbe renderla concreta e istanziare un generico impiegato? Avrebbe (e ha)
sicuramente pi senso definire tante classi derivate che specializzano un generico
impiegato e, per ciascuna di esse, definire il proprio metodo di calcolo dello
stipendio.
Listato 8.6 Classe Employee.

package com.pellegrinoprincipe;

public abstract class Employee


{
private String nome;
private String cognome;

public Employee(String n, String c)


{
nome = n;
cognome = c;
}

protected String getNome() { return nome; }


protected String getCognome() { return cognome; }

public String toString()


{
return cognome + " " + nome;
}

public abstract int earning(); // metodo astratto


}

Listato 8.7 Classe Engineer.

package com.pellegrinoprincipe;

public class Engineer extends Employee


{
private int percentage;
private int fisso;

public Engineer(String n, String c, int p, int f)


{
super(n, c);
setPercentage(p);
setFisso(f);
}

public void setFisso(int f) // imposto il fisso come paga


{
fisso = f > 0 ? f : 0;
}
public void setPercentage(int p) // imposto la percentuale
{
percentage = p > 0 ? p : 0;
}

public int earning() // calcolo specializzato del guadagno


{
return fisso + (fisso * percentage / 100);
}

public String toString()


{
return super.toString() + " guadagna ";
}
}

Il Listato 8.7 evidenzia come la classe Engineer sia una classe specializzata della
classe base astratta Employee; infatti esegue loverriding del metodo earning ereditato per
il calcolo della paga. Inoltre, poich un Engineer anche un Employee, dal suo costruttore
invochiamo il costruttore di Employee per inizializzarne i dati (nome e cognome). Questo
un buon esempio di come lereditariet permetta di estendere il software riutilizzando
parti di una classe. Infatti, non stato necessario definire variabili di istanza del nome e
del cognome allinterno della classe Engineer, poich esse sono state gi create ed
ereditate da Employee. Infine, anche per le classi Technician (Listato 8.8) e Laborer (Listato
8.9) vale lo stesso discorso fatto sin qui per la classe Engineer.
Listato 8.8 Classe Technician.

package com.pellegrinoprincipe;

public class Technician extends Employee


{
private int quantum = 5;
private int pezzi;
private int fisso;

public Technician(String n, String c, int f, int p)


{
super(n, c);
setFisso(f);
setPezzi(p);
}

public void setFisso(int f) // imposto il fisso della paga


{
fisso = f > 0 ? f : 0;
}

public void setPezzi(int p) // pezzi da lavorare


{
pezzi = p > 0 ? p : 0;
}

public int earning() // specializzazione della paga


{
return fisso + (quantum * pezzi);
}

public String toString()


{
return super.toString() + " guadagna ";
}
}
Listato 8.9 Classe Laborer.

package com.pellegrinoprincipe;

public class Laborer extends Employee


{
private int percentage[] = {2, 5, 8, 11};
private int ora_paga = 8;
private int ore_lavorate;
private int pezzi;

public Laborer(String n, String c, int p, int o)


{
super(n, c);
setPezzi(p);
setOreLavorate(o);
}

public void setOreLavorate(int o) // imposto le ore lavorate


{
ore_lavorate = o > 0 ? o : 0;
}

public void setPezzi(int p) // imposto i pezzi da lavorare


{
pezzi = p >= 0 && p <= 3 ? p : -1;
}

public int earning() // specializzazione del calcolo della paga


{
int p = 0;
if (ore_lavorate > 0)
{
p = ore_lavorate * ora_paga;
if (pezzi != -1)
p += (p * percentage[pezzi] / 100);
return p;
}
else
return 0;
}

public String toString()


{
return super.toString() + " guadagna ";
}
}

Listato 8.10 Astratte_Client.

package com.pellegrinoprincipe;

public class Astratte_Client


{
public static void main(String args[])
{
Employee e;
Engineer eng = new Engineer("Pellegrino", "Principe", 10, 1000);
Technician tec = new Technician("Paolo", "Canali", 800, 3);
Laborer lab = new Laborer("Aldo", "Falco", 2, 44);

e = eng; // ora un Engineer


System.out.print(e.toString() + e.earning());

e = tec; // ora un Technician


System.out.print(" | " + e.toString() + e.earning());

e = lab; // ora un Laborer


System.out.println(" | " + e.toString() + e.earning());
}
}

Output 8.3 Dal Listato 8.10 Classe Astratte_Client.


Principe Pellegrino guadagna 1100 | Canali Paolo guadagna 815 | Falco Aldo guadagna 380

Dal Listato 8.10 vediamo che nel metodo main si crea il riferimento e del tipo della
classe astratta Employee e poi tanti riferimenti (eng, tec e lab) quante sono le classi da
essa derivate. Successivamente assegniamo in sequenza tali riferimenti al riferimento
e di tipo Employee e da esso invochiamo, sempre sequenzialmente, il metodo earning per

visualizzare lo stipendio delloggetto che sta in quel momento referenziando.


Interfacce
Uninterfaccia una sorta di classe astratta che dichiara, principalmente, dei
metodi ( presente solamente la loro segnatura) che le classi che la implementano (o
realizzano) devono poi definire. Pertanto, essa contiene, soprattutto, una serie di
metodi astratti (sono implicitamente abstract) e pu anche contenere dei dati membro
che devono essere inizializzati con un valore, poich il compilatore li tratta
automaticamente come final e static ovvero come dati costanti. Inoltre, sia i metodi
astratti sia i dati costanti, sono implicitamente public.
IMPORTANTE
Dalle versione 8 di Java, le interfacce possono avere anche dei metodi con unimplementazione
(default methods) e tali metodi possono essere anche statici. In ogni caso ne parleremo
approfonditamente allorquando tratteremo nel Capitolo 10 la programmazione funzionale; questo
perch tali caratteristiche evolutive sono documentate nella stessa specifica delle lambda
expression, ovvero la JSR 335, e dunque in quel contesto che la loro disamina appare
maggiormente opportuna e organica.
Sintassi 8.4 Definizione di uninterfaccia.

[modifiers] interface MyInterface {}

Sintassi 8.5 Implementazione di uninterfaccia.

public class MyClass implements MyInterface {}

Come si vede nella Sintassi 8.4, uninterfaccia si definisce utilizzando la keyword


interface prima del suo identificatore; nella Sintassi 8.5 si vede, invece, che usando la

keyword implements seguita dal nome dellinterfaccia si dichiara la volont di


implementare i servizi di questultima.
Quando uninterfaccia contiene solo dati membro, la classe che la implementa
erediter tali membri nel suo spazio dei nomi come se fossero state dichiarate, per
lappunto, al suo interno delle costanti (Listato 8.11).
Listato 8.11 Classe Interfaccia_Di_Var_Client.
package com.pellegrinoprincipe;

interface OnlyVar
{
int NO = 0;
int YES = 1;
}

public class Interfaccia_Di_Var_Client implements OnlyVar


{
public static void main(String args[])
{
System.out.println("NO = " + NO + " YES = " + YES);
}
}

Output 8.4 Dal Listato 8.11 Classe Interfaccia_Di_Var_Client.


NO = 0 YES = 1

CLASSI ASTRATTE O INTERFACCE?


La decisione se progettare delle classi astratte o delle interfacce risiede nella scelta di una
progettazione di unereditariet per implementazione (nel caso di classi astratte) oppure di
unereditariet per interfaccia. Nel primo caso si definiscono (oltre alla dichiarazione di metodi
astratti) anche dei metodi nella parte alta della gerarchia. Nel secondo caso si dichiarano nella
parte alta della gerarchia solo metodi astratti, delegando alle altre classi, della parte bassa della
gerarchia, la loro effettiva realizzazione (definizione). Nel gergo della OOP si dice anche che le
classi che implementano uninterfaccia stipulano con essa una sorta di contratto con cui si
impegnano a definirne i metodi. Infatti, se una classe che implementa uninterfaccia non definisce i
metodi ereditati, il compilatore generer un messaggio di errore. Tuttavia, se la medesima classe
non implementa i metodi dellinterfaccia, ma vuole delegare alle sue sottoclassi tale eventuale
definizione, allora la stessa classe dovr essere definita come abstract.

Vediamo ora come si implementa una parte della struttura gerarchica vista per le
figure geometriche (Figura 8.1), considerando come interfacce il tipo Shape, che
fornisce il metodo astratto draw, e il tipo TwoDShape, che fornisce i metodi astratti area e
perimeter che saranno, tutti, specificamente implementati dalle figure geometriche che
le realizzeranno.
Listato 8.12 Interfaccia Shape.

package com.pellegrinoprincipe;

public interface Shape


{
public void draw(); // metodo per il disegno
}

Listato 8.13 Interfaccia TwoDShape.

package com.pellegrinoprincipe;

public interface TwoDShape extends Shape


{
public int area(); // per il calcolo dell'area
public int perimeter(); // per il calcolo del perimetro
}

Il Listato 8.13 mostra che ogni interfaccia pu ereditare da altre interfacce (usando
sempre la keyword extends) e che, ovviamente, la classe che la implementa dovr
fornire una definizione di tutti i metodi astratti della gerarchia di interfacce.
NOTA
importante precisare che abbiamo dichiarato il metodo area nellinterfaccia TwoDShape e non
nellinterfaccia Shape perch linterfaccia ThreeDShape ne avr uno suo specializzato per le figure
geometriche in tre dimensioni, denominato surface_area. Avr anche la dichiarazione di un
metodo denominato volume.

Listato 8.14 Classe Rectangle_REV_1.


...
public class Rectangle_REV_1 implements TwoDShape
{
...
public int area() { return width * height; }
public int perimeter() { return 2 * width + 2 * height; }
public void draw() { System.out.println("DISEGNO DEL RETTANGOLO"); };
...
}

Nel Listato 8.14 la classe Rectangle_REV_1 dichiara con listruzione implements TwoDShape
la volont di definire i metodi astratti dellinterfaccia TwoDShape e anche di quello
dellinterfaccia Shape da cui TwoDShape stessa deriva. Infatti, allinterno del corpo di
Rectangle_REV_1 definiamo specificamente i metodi area, perimeter e draw.
Listato 8.15 Classe Square_REV_1.

...
public class Square_REV_1 extends Rectangle_REV_1
{
...
public void draw() { System.out.println("DISEGNO DEL QUADRATO"); };
...
}

Nel Listato 8.15 vediamo che la classe Square_REV_1 eredita dalla classe
Rectangle_REV_1 dando una sua definizione in overriding del metodo draw, il quale,
quindi, stato ereditato dalla classe Rectangle_REV_1, che ne ha dato una propria
definizione poich tale classe, a sua volta, ha implementato linterfaccia Shape.
Listato 8.16 Shape_Client.
package com.pellegrinoprincipe;

public class Shape_Client


{
public static void main(String args[])
{
TwoDShape tds;
Rectangle_REV_1 r = new Rectangle_REV_1(new Point2D(10, 10), 5, 4);
Square_REV_1 s = new Square_REV_1(new Point2D(35, 40), 9);

tds = r; // TwoDShape ora un tipo Rectangle


tds.draw();

tds = s; // TwoDShape ora un tipo Square


tds.draw();
}
}

Output 8.5 Dal Listato 8.16 Classe Shape_Client.


DISEGNO DEL RETTANGOLO
DISEGNO DEL QUADRATO
Classi anonime
Nel capitolo precedente abbiamo parlato delle classi interne (o locali) e di come
esse possano essere create sia nel corpo della classe ospite, sia nel corpo di un
metodo. In entrambi i casi le classi avevano sempre un nome. Tuttavia esiste anche la
possibilit di creare classi anonime, ovvero prive di nome.
Sintassi 8.6 Classe anonima definita a partire da una classe base.

new MySuperClass() {}

Sintassi 8.7 Classe anonima definita a partire da uninterfaccia.

new MyInterface() {}

Dalla Sintassi 8.6 si vede come una classe anonima si crei utilizzando loperatore
new seguito dal nome di una classe base da estendere. Segue poi la definizione del

corpo della classe, ove si potranno scriverne i consueti membri. Ovviamente il nome
della superclasse anche il nome del costruttore della stessa, pertanto, se si vuole
costruire una classe anonima con un costruttore che accetta argomenti, nulla vieta di
utilizzarlo. La Sintassi 8.7, invece, permette la creazione di una classe anonima a
partire da uninterfaccia da implementare.
Quando si progettano le classi anonime occorre considerare quanto segue.

Nel corpo della classe non vi possono essere metodi costruttori. Infatti, se una
classe anonima, quale nome mai potrebbe avere il suo costruttore, visto che il
nome del metodo costruttore riflette il nome della classe?
Loperatore new crea unistanza di una classe il cui riferimento viene utilizzato
nel contesto di valutazione dellespressione dove si trova tale creazione.
Le classi anonime non possono mai essere static e abstract.
Sono automaticamente classi locali e final.

Listato 8.17 Classi_Anonime.

package com.pellegrinoprincipe;

public class Classi_Anonime


{
public static void doShape(TwoDShape s) // mostra l'area e il perimetro
// di un oggetto TwoDShape
{
int a, p;
a = s.area();
p = s.perimeter();
System.out.println("Area: " + a + " Perimetro: " + p);
}
public static void doEmployee(Employee e) // mostra quanto guadagna un Employee
{
System.out.println(e + " vorrebbe guadagnare " + e.earning() + "");
}
public static void main(String args[])
{
doShape(new TwoDShape() // classe anonima che implementa l'interfaccia TwoDShape
{
public int area() { return 0; }
public int perimeter() { return 0; }
public void draw() { System.out.println("DrawX"); }
});
class A_Shape implements TwoDShape // metodo alternativo
// con l'uso di una classe locale
{
public int area() { return 1; }
public int perimeter() { return 1; }
public void draw() { System.out.println("DrawY"); }
}
A_Shape i = new A_Shape();
doShape(i);
doEmployee(new Employee("Pellegrino", "Principe") // classe anonima che eredita
// dalla classe Employee
{
public int earning() { return 40000;}
});

class An_Employee extends Employee // metodo alternativo


// con l'uso di una classe locale
{
public An_Employee(String nome, String cognome) { super(nome, cognome); }
public int earning() { return 60000; }
}
An_Employee e = new An_Employee("Pellegrino", "Principe");
doEmployee(e);
}
}

Output 8.6 Dal Listato 8.17 Classe Classi_Anonime.


Area: 0 Perimetro: 0
Area: 1 Perimetro: 1
Principe Pellegrino vorrebbe guadagnare 40000
Principe Pellegrino vorrebbe guadagnare 60000

Nel Listato 8.17 abbiamo scritto i seguenti metodi:

doShape che accetta come argomento un oggetto istanza di una classe che
implementa uninterfaccia di tipo TwoDShape;
doEmployee che accetta come argomento un oggetto istanza di una classe che
eredita dalla classe Employee.

Successivamente per ognuno di tali metodi abbiamo scritto due versioni di


invocazione, una che passa un oggetto istanza di classe anonima e laltra che passa un
oggetto istanza di una classe locale non anonima.
Nel primo caso si pu constatare come la sintassi della classe anonima
sicuramente pi concisa ed elegante. Nel secondo caso, invece, si vede come, per
adempiere allo stesso scopo, dobbiamo utilizzare pi passaggi: prima dobbiamo
definire la classe e poi dobbiamo crearne unistanza.
Ovviamente la scelta se utilizzare una definizione di classe locale oppure se
utilizzare direttamente una classe anonima dipende da quante volte dovremo
riutilizzare tale classe: se il suo utilizzo esplicito per una sola operazione allora
propenderemo per una classe anonima, viceversa per una classe locale non anonima.
Ereditariet multipla con le interfacce
Lereditariet multipla si ha quando una classe pu ereditare contemporaneamente
da pi superclassi, ovvero pu avere pi classi base. In Java una classe non pu
ereditare da pi classi e pertanto si pu affermare, in prima battuta, che lereditariet
multipla, nel senso comune che il termine assume nellambito della OOP, non
ammessa.
Una sorta di ereditariet multipla si pu tuttavia ottenere con le interfacce. Infatti,
consentito a una classe di implementare pi interfacce.
Sintassi 8.8 Ereditariet multipla con le interfacce.

public MyClass extends Bclass implements A, B, C

Come si vede dalla Sintassi 8.8, la classe MyClass eredita dalla classe Bclass e
implementa le interfacce A, B e C di cui dovr definire tutti i metodi astratti.
IMPORTANTE
Anche in questo caso, nel Capitolo 10, ritorneremo a parlare dellereditariet multipla con le
interfacce analizzando le eventuali cause di ambiguit che si possono presentare per effetto della
possibilit di definire anche dei metodi con unimplementazione (default methods).
Capitolo 9
Programmazione generica

La programmazione generica nasce con Java a partire dalla versione 5.0 del
linguaggio, secondo le specifiche dettate dalla Java Specification Request 14, Add
Generic Types To The Java Programming Language, e permette di scrivere classi e
metodi generici, ovvero che compiono una medesima operazione su un insieme di
tipi di dato differenti.

CHE COSA SONO LE JAVA SPECIFICATION REQUESTS


L e Java Specification Requests (JSRs) sono documenti ufficiali e formali che descrivono le
proposte di aggiunte o cambiamenti alla piattaforma Java nel suo complesso. Tutte le proposte
seguono un iter di analisi e discussione da parte dei membri facenti parte del Java Community
Process (JCP), che le condurr verso uneventuale approvazione che si realizzer con il rilascio dei
documenti nello stato di Final Release. I documenti approvati saranno accompagnati da
implementazioni reali della specifica, definite Reference Implementations, e da una suite di test e
verifica delle API sviluppate, definita Technology Compatibility Kit.
Metodi generici
Un metodo detto generico quando accetta diversi tipi di dato su cui esegue uno
stesso algoritmo. Se non vi fosse la possibilit di scrivere il metodo in forma
generica, per adempiere allo stesso scopo si dovrebbe ricorrere al meccanismo
delloverloading dei metodi, che comporta la necessit di scrivere tanti metodi che
eseguono lo stesso compito su tipi di dato differenti. Vediamo subito un esempio che
fa uso delloverloading.
Listato 9.1 Classi PrintArray e PrintArrayClient.

package com.pellegrinoprincipe;

class PrintArray
{
public PrintArray() {}

public void printArray(Integer el[]) // stampa un array di interi


{
for (Integer i : el)
System.out.print(i + " ");
}

public void printArray(Double el[]) // stampa un array di double


{
for (Double i : el)
System.out.print(i + " ");
}

public void printArray(Character el[]) // stampa un array di caratteri


{
for (Character i : el)
System.out.print(i + " ");
}
}

public class PrintArrayClient


{
public static void main(String[] args)
{
PrintArray pa = new PrintArray();

Double d[] = { 11.1, 11.2 };


Integer i[] = { 12, 13 };
Character c[] = { 'a', 'b'};

System.out.print("[ ");
pa.printArray(d);
pa.printArray(i);
pa.printArray(c);
System.out.print("]");
}
}

Il Listato 9.1 crea una classe denominata PrintArray con tre metodi in overloading
che permettono di stampare tutti gli elementi di un array di tipo differente. Nella
classe PrintArrayClient si creano un oggetto di tipo PrintArray e tre riferimenti di oggetti
array che contengono rispettivamente valori di tipo double, int e char.
Nellassegnamento dei valori agli array possiamo vedere unaltra utile caratteristica
offerta da Java, che consente di convertire automaticamente un valore primitivo nel
corrispondente oggetto (autoboxing) e di effettuare anche loperazione inversa
(autounboxing). Per esempio, nel nostro caso il valore primitivo 11.1 stato
lecitamente inserito come elemento di un array di oggetti di tipo Double.
Output 9.1 Dal Listato 9.1 Classi PrintArray e PrintArrayClient.

[ 11.1 11.2 12 13 a b ]

LOutput 9.1 mostra il risultato dellesecuzione dei metodi printArray, che sar
sempre differente perch, a seconda del tipo di valore passato come argomento, il
compilatore invocher il corrispettivo metodo (grazie alloverloading).
Ora scriviamo, ancora in modo non generico, unaltra classe con dei metodi che
calcolano il valore massimo fra 3 valori passati come argomenti.
Listato 9.2 Classi CalculateMax e CalculateMaxClient.
package com.pellegrinoprincipe;

class CalculateMax
{
public CalculateMax() {}

public double maximum(double a, double b, double c) // massimo tra valori double


{
double max = a;

if (b > max)
max = b;
if (c > max)
max = c;

return max;
}

public int maximum(int a, int b, int c) // massimo tra valori interi


{
int max = a;

if (b > max)
max = b;
if (c > max)
max = c;

return max;
}

public char maximum(char a, char b, char c) // massimo tra valori carattere


{
char max = a;

if (b > max)
max = b;
if (c > max)
max = c;

return max;
}
}

public class CalculateMaxClient


{
public static void main(String[] args)
{
CalculateMax cm = new CalculateMax();

Double d[] = { 11.1, 11.2, 9.6 };


Integer i[] = { 12, 13, 3 };
Character c[] = { 'n', 'b', 'z' };

System.out.print("Max (double): " + cm.maximum(d[0], d[1], d[2]));


System.out.print(" | Max (int): " + cm.maximum(i[0], i[1], i[2]));
System.out.println(" | Max (char): " + cm.maximum(c[0], c[1], c[2]));
}
}

Output 9.2 Dal Listato 9.2 Classi CalculateMax e CalculateMaxClient.


Max (double): 11.2 | Max (int): 13 | Max (char): z

Ora invece riscriviamo entrambe le classi con dei metodi generici, considerando la
seguente sintassi (Sintassi 9.1).
Sintassi 9.1 Definizione di metodi generici.

[modifiers] <Type1, Type2, ..., TypeN> return_type methodName(Type1 t1, Type2 t2, ..., TypeN tN)
{
Type1 n;
String s;
}

Qui vediamo che si deve scrivere, prima del tipo di ritorno, una sezione formata
dalle parentesi angolari < > con allinterno degli identificatori di tipo generico separati
dalla virgola (,) che sono definiti come variabili o parametri di tipo formale (formal
type parameters). I tipi attuali, effettivi degli argomenti passati a un metodo generico
sono invece definiti come argomenti di tipo attuale (actual type arguments).
Tali parametri di tipo si possono usare alla stessa stregua dei normali tipi non
parametrizzati ovvero come tipi per i parametri formali, tipi per i valori di ritorno e
tipi per le variabili locali.
Per convenzione i parametri di tipo (indicati con Type1, Type2 e cos via) sono
formalizzati mediante una sola lettera scritta in maiuscolo, che varia a seconda di ci
che parametrizzano; in particolare possiamo usare E per Element (tipo di un elemento
in una collezione), K per Key (tipo di una chiave in una mappa), V per Value (tipo di un
valore in una mappa), N per Number (tipo di un valore numerico), T per Type (un
qualsiasi altro tipo generico), S, U e cos via per ulteriori tipi generici.
Listato 9.3 Classi PrintArrayGeneric e PrintArrayGenericClient.
package com.pellegrinoprincipe;

class PrintArrayGeneric
{
public PrintArrayGeneric() {}

public <E> void printArray(E el[])


{
for (E i : el) // stampa in modo generico gli elementi dell'array di differente tipo
System.out.print(i + " ");
}
}

public class PrintArrayGenericClient


{
public static void main(String[] args)
{
PrintArrayGeneric pag = new PrintArrayGeneric();

Double d[] = { 11.1, 11.2 };


Integer i[] = { 12, 13 };
Character c[] = { 'a', 'b' };
String s[] = { "sono", "una", "stringa" };

System.out.print("[ ");
pag.printArray(d);
pag.printArray(i);
pag.printArray(c);
pag.<String>printArray(s); // sintassi alternativa di invocazione di un metodo
// generico
System.out.print("]");
}
}

Output 9.3 Dal Listato 9.3 Classi PrintArrayGeneric e PrintArrayGenericClient.


[ 11.1 11.2 12 13 a b sono una stringa ]

Il Listato 9.3 mostra come, definendo un metodo generico, si riduca drasticamente


il codice scritto. Abbiamo infatti scritto un solo metodo, rispetto ai tre del Listato 9.1,
che fa esattamente la stessa cosa, ovvero stampa gli elementi di un array che possono
essere di tipo differente.
In dettaglio, il metodo printArray ha, nella sezione di dichiarazione dei parametri di
tipo, il tipo E, che utilizzato poi come tipo del parametro formale nella lista dei
parametri del metodo (E el[]) e poi come tipo della variabile locale nel ciclo for (E i).

Notiamo, inoltre, che nella classe PrintArrayGenericClient abbiamo aggiunto un array


di oggetti String e labbiamo fatto stampare sempre dal metodo printArray.
Questaggiunta, tuttavia, non ha inficiato la corretta compilazione del programma.
Infatti, poich printArray un metodo generico, non stato necessario scrivere nella
classe PrintArrayGeneric il corrispondente metodo in overloading che accettasse un
argomento di tipo array di String (cosa che, invece, avremmo dovuto fare se il
linguaggio non avesse supportato i generici).
Infine, interessante evidenziare listruzione pag.<String>printArray(s), che mostra un
modo alternativo per invocare un metodo generico che si concretizza nel passare il
tipo effettivo tra le parentesi angolari < > prima del nome del metodo stesso. Tuttavia
tale sintassi non necessaria; infatti si pu tranquillamente omettere di scrivere il tipo
tra le parentesi angolari, poich il compilatore in grado di capirlo autonomamente
analizzando il tipo passato come argomento, come mostrato per le altre invocazioni
del metodo printArray (type inference).

MIGLIORAMENTI DELLA TYPE INFERENCE PER I METODI GENERICI


Con la versione 8 di Java si migliorata la capacit del compilatore di determinare
automaticamente il tipo di argomento di un metodo generico (Snippet 9.1). Infatti, grazie alle
indicazioni del documento di proposta accettato per la corrente versione di Java JDK
Enhancement Proposal (JEP) 101: Generalized Target-Type Inference il compilatore ora in
grado di inferire in autonomia il tipo di argomento quando il risultato di uninvocazione di metodo
passato come argomento a un altro metodo (inference in argument position o inference in method
context).

Snippet 9.1 Inference in method context.

static void printListElements(List<Integer> list)


{
for (Integer elem : list)
System.out.println(elem);
}

static <T> List<T> factorList(int capacity)


{
List<T> list = new ArrayList<>(capacity);
return list;
}

// inference in method context con Java 8 - OK - nessun errore di compilazione


// con Java 7 avremo, per, il seguente errore di compilazione:
// incompatible types: List<Object> cannot be converted to List<Integer>
printListElements(factorList(10));

NOTA
bene rammentare che non tutti i goal del JEP 101 sono stati raggiunti. Infatti, linference in
chained calls, ovvero linferenza del tipo di argomento quando si hanno chiamate a catena di
metodi generici, non stato implementato. Per esempio, listruzione Iterator<Integer> iterator
= new ArrayList<>().iterator() generer il seguente errore di compilazione: incompatible
types: Iterator<Object> cannot be converted to Iterator<Integer>.

Listato 9.4 Classi CalculateMaxGeneric e CalculateMaxGenericClient.

package com.pellegrinoprincipe;

class CalculateMaxGeneric
{
public <T extends Comparable<T>> T maximum(T a, T b, T c)
{
T max = a;

if (b.compareTo(max) > 0)
max = b;
if (c.compareTo(max) > 0)
max = c;

return max;
}
}

public class CalculateMaxGenericClient


{
public static void main(String[] args)
{
CalculateMaxGeneric cmg = new CalculateMaxGeneric();

Double d[] = { 11.1, 11.2, 9.6 };


Integer i[] = { 12, 13, 3 };
Character c[] = { 'n', 'b', 'z' };
String s[] = { "sono", "una", "stringa" };

Double d_max = cmg.maximum(d[0], d[1], d[2]);


Integer i_max = cmg.maximum(i[0], i[1], i[2]);
Character c_max = cmg.maximum(c[0], c[1], c[2]);
String s_max = cmg.maximum(s[0], s[1], s[2]);

// stampa del valore massimo trovato


System.out.print("Max (double): " + d_max);
System.out.print(" | Max (int): " + i_max);
System.out.print(" | Max (char): " + c_max);
System.out.println(" | Max (String): " + s_max);
}
}

Output 9.4 Dal Listato 9.4 Classi CalculateMaxGeneric e CalculateMaxGenericClient.


Max (double): 11.2 | Max (int): 13 | Max (char): z | Max (String): una

Il Listato 9.4 definisce la classe CalculateMaxGeneric con il metodo maximum che ha,
nella sezione dei parametri di tipo formali, un parametro di tipo T che estende (extends)
uninterfaccia generica di tipo Comparable<T>.
NOTA
Se si devono estendere pi classi o pi interfacce (nei generici la keyword extends si usa anche
per le interfacce) occorre utilizzare il carattere & (Snippet 9.2).
Snippet 9.2 Utilizzo del carattere &.

public static <T extends Object & Comparable<? super T>> T max(Collection<T> coll)

Per far meglio comprendere la sintassi del metodo maximum del Listato 9.4, utile
spiegare come i progettisti di Java hanno inteso realizzare i generici.
I generici sono realizzati in Java con una procedura che definita erasure,
mediante la quale il compilatore, prima di produrre il bytecode corrispondente,
effettua le seguenti operazioni.

1. Elimina la sezione di dichiarazione dei parametri di tipo.


2. Sostituisce ogni occorrenza dei parametri di tipo con il tipo Object laddove non
diversamente specificato.
3. Scrive, se necessario, dei cast espliciti per convertire i tipi Object o gli eventuali
altri tipi in tipi pi specifici.
4. Crea, eventualmente, dei metodi definiti bridged se, tra classi dove sussiste una
relazione di ereditariet, vi sono dei problemi legati alla sostituzione automatica
dei tipi tra metodi sovrascritti.

Ora spieghiamo in modo pratico, riferendoci al nostro programma (Listato 9.4), il


significato dei punti precedenti. Cominciamo con il seguente decompilato.
Decompilato 9.1 File CalculateMaxGeneric.class.
public class CalculateMaxGeneric
{
public CalculateMaxGeneric() { }

public Comparable maximum(Comparable a, Comparable b, Comparable c)


{
Comparable max = a;

if (b.compareTo(max) > 0)
max = b;
if (c.compareTo(max) > 0)
max = c;

return max;
}
}

Dal Decompilato 9.1 si vede subito come sia stata eliminata la sezione dei
parametri di tipo (punto 1). Il compilatore, poi, nel leggere tale sezione vede che
specificata una regola che indica quale deve essere loggetto da sostituire ai parametri
di tipo. Nel nostro caso scritto che T un parametro di tipo che deve riferirsi a
oggetti che implementano linterfaccia Comparable tramite la keyword extends. Pertanto,
loggetto che prender il posto di T Comparable e non Object (punto 2) e rappresenter
un upper bound, ovvero una sorta di limite superiore agli oggetti che potr accettare.
Per quanto attiene al punto 3 analizziamo, invece, il seguente decompilato.
Decompilato 9.2 File CalculateMaxGenericClient.class.

public class CalculateMaxGenericClient


{
public CalculateMaxGenericClient() {}

public static void main(String args[])


{
CalculateMaxGeneric cmg = new CalculateMaxGeneric();

Double d[] =
{
Double.valueOf(11.1D), Double.valueOf(11.199999999999999D),
Double.valueOf(9.5999999999999996D)
};
Integer i[] =
{
Integer.valueOf(12), Integer.valueOf(13), Integer.valueOf(3)
};
Character c[] =
{
Character.valueOf('n'), Character.valueOf('b'), Character.valueOf('z')
};
String s[] = { "sono", "una", "stringa" };

Double d_max = (Double) cmg.maximum(d[0], d[1], d[2]);


Integer i_max = (Integer) cmg.maximum(i[0], i[1], i[2]);
Character c_max = (Character) cmg.maximum(c[0], c[1], c[2]);
String s_max = (String) cmg.maximum(s[0], s[1], s[2]);
...
}
}

Dal Decompilato 9.2 si vede che il compilatore ha scritto autonomamente delle


istruzioni di cast per ogni invocazione del metodo maximum, al fine di garantire la
correttezza dellassegnamento al tipo corrispondente. La scrittura dei cast da parte del
compilatore evidenzia un vantaggio importante introdotto dai generici, ovvero che in
fase di run-time il codice sar pi robusto, poich non vi potranno essere errori legati
a eccezioni di tipo ClassCastException. Infatti, prima dellavvento dei tipi parametrici,
per scrivere codice generico dovevamo definire dei metodi con parametri di tipo
Object , con la necessit di scrivere poi manualmente il cast corretto, ricordando quale
fosse il tipo effettivamente puntato da Object in quel determinato contesto.

Con i generici, pertanto, il codice risulta maggiormente type-safe perch,


ripetiamo, il compilatore (a compile-time) che provvede a controllare se il codice
scritto risponde ai requisiti specificati dalle clausole dei tipi parametrici, generando
pertanto lopportuno codice.
Il punto 4 si verifica quando lerasure di un metodo di una classe base, che ha un
metodo sovrascritto nella sua classe derivata, produce un codice che cambia, per
esempio, il tipo di ritorno. Ora, come sappiamo, un metodo si definisce sovrascritto
se la sua segnatura esattamente uguale alla segnatura dello stesso metodo scritto
nella sua classe base. Pertanto, considerando che lerasure ora descritta cambia di
fatto la segnatura dei metodi, il compilatore, per ottenere un corretto overriding e per
garantire che in fase di run-time il polimorfismo funzioner in modo corretto, inserir
un metodo nella classe derivata, con la stessa segnatura del corrispondente metodo
della classe base, che far da ponte (bridge) verso il metodo che poi sar
effettivamente invocato.
TERMINOLOGIA
Un metodo bridge anche definito synthetic method (metodo sintetico). In linea generale,
qualsiasi costrutto introdotto in automatico dal compilatore Java che non ha, per, un
corrispettivo costrutto nel codice sorgente definibile come sintetico. Da questa terminologia
sono tuttavia esclusi i costruttori di default, il metodo di inizializzazione di classe (per i dati
membro statici) e i metodi values e valueOf della classe Enum.

Listato 9.5 Classe BridgeMethod.

package com.pellegrinoprincipe;

class Base<T> // classe base


{
T el;
public Base(T o) { el = o; }
T get() { return el; }
}

class Derived extends Base<String> // classe derivata


{
public Derived(String o) { super(o); }
String get() // metodo sovrascritto
{
System.out.print("Chiamato da Derived: ");
return el;
}
}

public class BridgeMethod


{
public static void main(String[] args)
{
Base<String> b = new Derived("bye bye !!!");
System.out.println(b.get());
}
}
Output 9.5 Dal Listato 9.5 Classe BridgeMethod.

Chiamato da Derived: bye bye !!!

Il Listato 9.5 crea due classi legate da una relazione di ereditariet (Base e Derived)
che hanno in comune il metodo get, il quale verr rielaborato dal compilatore nel
modo seguente.
Decompilato 9.3 File Base.class.

class Base
{
Object el;
public Base(Object o) { el = o; }
Object get() { return el; }
}

Decompilato 9.4 File Derived.class.


class Derived extends Base
{
public Derived(String o) { super(o); }
String get()
{
System.out.println("Chiamato da Derived: ");
return (String) el;
}
volatile Object get() // metodo bridge
{
return get();
}
// mostriamo anche il disassemblato di Object get() ottenuto con il comando javap
// dove evidente l'invocazione del metodo get con il tipo di ritorno String
//
// java.lang.Object get();
// Code:
// 0: aload_0
// 1: invokevirtual #7 // Method get:()Ljava/lang/String;
// 4: areturn
}

Il Decompilato 9.3 evidenzia come il metodo get abbia come tipo di ritorno un
Object che lupper bound usato dal compilatore in fase di erasure per il parametro di
tipo T. Nel Decompilato 9.4 il compilatore scrive un nuovo metodo get (che ,
ripetiamo, il metodo che fa da bridge) con la stessa segnatura del metodo get della
classe base al cui interno viene invocato il get che effettivamente deve essere
eseguito. Notiamo, inoltre, come nella classe derivata la presenza di due metodi che
sono differenziati per il tipo di ritorno non comporti alcun problema per la corretta
esecuzione del programma, perch per la virtual machine (a differenza del
compilatore) perfettamente lecito avere due metodi che differiscono per il tipo di
ritorno.
Infine, nellambito della progettazione di relazioni di ereditariet tra classi
generiche e non generiche necessario tenere presente che una classe generica pu
derivare da una classe non generica; una classe generica pu derivare da una classe
generica; una classe non generica pu derivare da una classe generica.
COVARIANZA DEL TIPO DI RITORNO
Dalla versione 5 del linguaggio Java possibile per una classe derivata fare loverriding di un
metodo di una classe base cambiando il tipo di ritorno, che per deve essere un sottotipo del tipo di
ritorno del metodo della classe base. Questa possibilit, definita in letteratura come covarianza del
tipo di ritorno, consente di scrivere metodi che sono in grado di ritornare direttamente un tipo pi
specifico, piuttosto che un tipo generico che dovr poi subire delle operazioni di cast. La covarianza
intuitiva perch analoga al polimorfismo. Riprenderemo questo concetto pi avanti nel capitolo
e vedremo come esso si applichi non solo al tipo di ritorno, ma anche nelle relazioni di ereditariet
fra i tipi generici. Vedremo inoltre le varianti possibili, introducendo i concetti di controvarianza e di
invarianza.

Per completezza vediamo anche il decompilato della classe PrintArrayGeneric, dove


si vede che lerasure effettuata dal compilatore ha sostituito il parametro di tipo E con
il tipo Object che, quindi, in mancanza di specifiche differenti lupper bound di
default.
Decompilato 9.5 File PrintArrayGeneric.class.
public class PrintArrayGeneric
{
...
public void printArray(Object el[])
{
Object aobj[];
int k = (aobj = el).length;
for (int j = 0; j < k; j++)
{
Object i = aobj[j];
System.out.print((new StringBuilder()).append(i).append(" ").toString());
}
}
}

Quando si progetta un metodo generico, necessario considerare quanto segue.

Un parametro di tipo pu rappresentare solo un tipo riferimento, ovvero non vi


possono essere parametri di tipo sostituiti dai tipi primitivi.
Lidentificativo scritto nella sezione dei parametri di tipo non pu essere
duplicato al suo interno, ma pu essere scritto pi volte come segnaposto nel
metodo.
Differenti metodi possono avere lo stesso identificativo del parametro di tipo
scritto nella sezione dei parametri di tipo.
Se stato scritto un metodo che ha come parametro un tipo esatto, verr sempre
invocato tale metodo e non quello generico.
Un metodo generico pu subire loverloading sia con un altro metodo generico
sia con uno non generico.

NOTA
Le prime due restrizioni appena elencate sono applicabili anche quando progettiamo delle classi
generiche. Cos, nel primo caso, avremo che un oggetto istanza di una classe generica potr
essere creato indicando come argomenti di tipo solo tipi di classi o interfacce (per esempio,
mentre sar legale scrivere List<Integer> list = new LinkedList<>(), scrivere List<int> list =
new LinkedList<>() dar un errore di compilazione). Nel secondo caso, invece, avremo che gli
identificatori dei parametri di tipo potranno trovarsi ripetuti nellambito del body di una classe per
indicare i tipi delle variabili di istanza, delle variabili locali ai metodi e cos via.
Classi generiche
Una classe generica progettata per permettere la creazione di oggetti di quella
classe indipendentemente dal tipo che deve manipolare. Un esempio pu essere dato
dalla struttura dati detta stack, che rappresenta una sorta di pila dove gli elementi
vengono inseriti e prelevati secondo una modalit detta LIFO (Last In First Out):
ogni elemento che viene inserito va in cima alla pila e pertanto lultimo inserito
anche il primo a uscire. Uno stack pu manipolare oggetti Integer, Double e cos via; se
non usassimo una classe generica, dovremmo progettare tante classi quanti sono i tipi
di dato che la stessa deve manipolare.
Sintassi 9.2 Definizione classe generica.

[modifiers] class ClassName<Type1, Type2, ..., TypeN> [extends implements]


{
Type1 el;
...
}

Dalla Sintassi 9.2 vediamo che per creare una classe generica si scrive la sezione
dei parametri di tipo dopo il nome della classe e poi tali parametri di tipo si scrivono
al suo interno, laddove necessario.
Listato 9.6 Classe StackGeneric.
package com.pellegrinoprincipe;

class StackGeneric<E>
{
private final int size;
private int top;
private E[] elems;

public StackGeneric() { this(5); }

public StackGeneric(int nr)


{
size = nr == 0 ? 5 : nr;
top = -1; // stack inizialmente vuoto
elems = (E[]) new Object[size];
}

public void push(E value) // mette un valore nello stack


{
if (top == size - 1)
System.out.println("Lo stack e' pieno!");
else
elems[++top] = value;
}

public E pop() // estrae un valore dallo stack


{
if (top == -1)
{
System.out.println("Lo stack e' vuoto!");
return null;
}
else
return elems[top--];
}
}
Listato 9.7 Classe StackGenericClient.

package com.pellegrinoprincipe;

public class StackGenericClient


{
public static void main(String[] args)
{
Double d[] = { 11.1, 11.2, 8.6 };
Integer i[] = { 12, 13, 5 };
Character c[] = { 'a', 'b', 'z' };

StackGeneric<Double> sd = new StackGeneric<>(3);


StackGeneric<Integer> si = new StackGeneric<>(3);
StackGeneric<Character> sc = new StackGeneric<>(3);

// test push
for (double e : d)
sd.push(e);
for (int e : i)
si.push(e);
for (char e : c)
sc.push(e);

// test pop
System.out.print("Valori Double: ");
for (int nr = 0; nr < 3; nr++)
{
Double d_tmp = sd.pop();
System.out.print(d_tmp + " ");
}

System.out.print(" | Valori Integer: ");


for (int nr = 0; nr < 3; nr++)
{
Integer i_tmp = si.pop();
System.out.print(i_tmp + " ");
}

System.out.print(" | Valori Character: ");
for (int nr = 0; nr < 3; nr++)
{
Character c_tmp = sc.pop();
System.out.print(c_tmp + " ");
}
}
}

Output 9.6 Dal Listato 9.7 Classe StackGenericClient.

Valori Double: 8.6 11.2 11.1 | Valori Integer: 5 13 12 | Valori Character: z b a

Analizzando il Listato 9.6 vediamo che il parametro di tipo E stato utilizzato


come tipo nella dichiarazione di un array, come tipo del parametro del metodo push e
come tipo del valore di ritorno del metodo pop. Inoltre importante considerare che
non possibile creare un array di un tipo generico direttamente usando unistruzione
come private E[] elems = new E[size], perch, per effetto dellerasure, a run-time il tipo E
non sar disponibile. Per ovviare a ci possiamo per scrivere listruzione private E[]
elems = (E[]) new Object[size] , la quale mostra che per avere un array di oggetti generici
dobbiamo creare un array di oggetti di tipo Object e poi effettuare un cast esplicito
verso il tipo array parametrico.
Notiamo tuttavia che questa operazione, nonostante sia permessa dal compilatore,
non type-safe, poich in un array di tipo Object possiamo inserire qualsiasi tipo
senza nessun controllo a compile-time. Infatti, nel nostro caso, dopo aver usato
lespressione di creazione dellarray, potremo lecitamente fare quanto evidenziato dal
seguente Snippet 9.3, che per potr creare a run-time dei problemi durante la
manipolazione dello stack (per esempio nelle operazioni di ottenimento
dellelemento), perch il tipo atteso dal chiamante potr essere differente da quello
trovato nellarray elems.
Snippet 9.3 Operazione non type-safe con un array di Object.

Object[] e = elems;
e[0] = 3;
e[1] = "data";

In ogni caso, quando si compiono operazioni non type-safe, possiamo far s che il
compilatore mostri un avviso dettagliato delle stesse durante la fase di compilazione
utilizzando il comando javac con il flag -Xlint:unchecked.
Snippet 9.4 Compilazione con dettaglio delloperazione non type-safe.
javac -Xlint:unchecked StackGenericClient.java

Warning 9.1 Messaggio di unchecked cast.


...StackGeneric.java:15: warning: [unchecked] unchecked cast
elems = (E[]) new Object[size];
required: E[] found: java.lang.Object[]
where E is a type-variable: E extends Object declared in class StackGeneric
1 warning

Vediamo ora il decompilato della classe generica presentata nel Listato 9.6.
Decompilato 9.6 File StackGeneric.class.

public class StackGeneric


{
...
public StackGeneric(int nr)
{
...
elems = new Object[size];
}

public void push(Object value)


{
...
}

public Object pop()


{
...
}
}

Dal Decompilato 9.6 notiamo che, come per i metodi generici, anche per le classi il
compilatore adotter le usuali regole di erasure gi esaminate, sostituendo in questo
caso il parametro di tipo E con il tipo Object e cancellando la sezione dei parametri di
tipo.
Per quanto attiene alla classe StackGenericClient notiamo che, per creare un oggetto
di una classe generica, basta porre subito dopo il nome della classe di riferimento, tra
le parentesi angolari < >, il tipo di oggetto (type argument) che si vuole utilizzare.
Tale argomento viene poi usato dal compilatore per fare il type checking a tempo di
compilazione e per le operazioni di cast eventualmente necessarie. Precisiamo che il
tipo reale delloggetto da utilizzare pu essere scritto solo nella parte della
dichiarazione di un riferimento (per esempio StackGeneric<Double> sd = ), poich nella
parte di creazione delloggetto (per esempio = new StackGeneric<>(3)) il compilatore,
se desumibile dal contesto, in grado di inferirlo automaticamente grazie allutilizzo
del diamond operator, indicato sempre dalle parentesi angolari vuote <> poste prima
delloperatore di invocazione del costruttore della classe di interesse.
Dopo la creazione degli oggetti di tipo StackGeneric, ne invochiamo i metodi push e
per inserire ed estrarre gli elementi dei relativi array.
pop

NOTA
importante ribadire che, quando per esempio invochiamo il metodo push come con listruzione
sd.push(e), di fatto inseriamo un riferimento a un elemento di tipo Double in un oggetto di tipo
Object, poich ricordiamo che elems di tipo Object[]; nonostante ci, il compilatore non
permetter di invocare push con oggetti di diverso tipo, grazie ai controlli di type-checking
effettuati a compile-time.
Tipi raw
Un tipo raw un tipo generico a cui manca lindicazione del tipo di argomento da
trattare. Si possono avere i tre casi seguenti.

1. Riferimento di tipo raw con oggetto creato di tipo raw.


2. Riferimento di tipo raw con oggetto creato non di tipo raw.
3. Riferimento non di tipo raw con oggetto creato di tipo raw.

Snippet 9.5 Tipo raw con oggetto creato di tipo raw.


StackGeneric sd = new StackGeneric(3);

Dallo Snippet 9.5 si deduce che il riferimento sd potr contenere qualunque oggetto
come Integer o String, poich il compilatore user Object implicitamente per ogni
riferimento del parametro di tipo trovato allinterno della classe generica.
Ovviamente tale approccio sconsigliato, poich non si avr da parte del compilatore
alcun controllo di type-safety e non saranno posti, laddove necessari, gli opportuni
cast.
Listato 9.8 Classe StackGenericRawTypeCase1.

package com.pellegrinoprincipe;

public class StackGenericRawTypeCase1


{
public static void main(String[] args)
{
Double d[] = { 11.1, 11.2, 8.6 };

// riferimento di tipo raw con oggetto creato di tipo raw


StackGeneric sd = new StackGeneric(3);

for (double e : d) // test push
sd.push(e);

System.out.print("Valori dello stack Double: "); // test pop
for (int nr = 0; nr < 3; nr++)
{
// ERRORE - incompatible types: Object cannot be converted to Double
Double d_tmp = sd.pop();
System.out.print(d_tmp + " ");
}
}
}

La compilazione del Listato 9.8 ci segnaler il seguente errore, perch ripetiamo


il compilatore non ha posto, in autonomia, alcun cast.
Errore 9.1 Compilazione Listato 9.8 Classe StackGenericRawTypeCase1.
...StackGenericRawTypeCase1.java:19: error: incompatible types: Object cannot be converted to
Double
Double d_tmp = sd.pop();
1 error

Snippet 9.6 Riferimento di tipo raw con oggetto creato non di tipo raw.
StackGeneric sd = new StackGeneric<Double>(3);

Nello Snippet 9.6 il riferimento sd, nonostante vi sia stato assegnato il riferimento a
un oggetto StackGeneric di tipo Double, sar di tipo raw. In ogni caso, tale assegnamento
comunque attuabile perch gli oggetti di tipo Double che lo stack manipoler sono
sicuramente sottotipi di Object (ricordiamo che il compilatore ha sostituito tutte le
occorrenze del parametro di tipo E con il tipo Object). Tuttavia, poich sd ancora un
tipo raw, anche in questo caso un suo utilizzo improprio generer errori in fase
compilazione.
Listato 9.9 Classe StackGenericRawTypeCase2.

package com.pellegrinoprincipe;

public class StackGenericRawTypeCase2


{
public static void main(String[] args)
{
Double d[] = { 11.1, 11.2, 8.6 };

// riferimento di tipo raw con oggetto creato non di tipo raw


StackGeneric sd = new StackGeneric<Double>(3);
...

for (int nr = 0; nr < 3; nr++)


{
// ERRORE - incompatible types: Object cannot be converted to Double
Double d_tmp = sd.pop();
System.out.print(d_tmp + " ");
}
}
}

La compilazione del Listato 9.9 ci segnaler lo stesso errore della compilazione del
Listato 9.8 poich, ripetiamo, sd ancora un tipo raw.
Errore 9.2 Compilazione Listato 9.9 Classe StackGenericRawTypeCase2.
...StackGenericRawTypeCase2.java:19: error: incompatible types: Object cannot be converted to
Double
Double d_tmp = sd.pop();
1 error

Snippet 9.7 Riferimento non di tipo raw con oggetto creato di tipo raw.
StackGeneric <Double>sd = new StackGeneric(3);

Nello Snippet 9.7 stiamo assegnando a sd un riferimento di un tipo raw e questo


far generare dal compilatore un warning di assegnamento unsafe (per un dettaglio di
tale warning ricordiamo di utilizzare in fase di compilazione il flag -Xlint:unchecked),
poich non vi la certezza che uno stack di Object conterr solo i tipi argomento
assegnati. In questultimo caso, tuttavia, il compilatore controller la correttezza dei
tipi, e infatti in sd non potranno essere inseriti oggetti di tipo differente dal tipo Double.
Listato 9.10 Classe StackGenericRawTypeCase3.
package com.pellegrinoprincipe;

public class StackGenericRawTypeCase3


{
public static void main(String[] args)
{
Double d[] = { 11.1, 11.2, 8.6 };

// riferimento non di tipo raw con oggetto creato di tipo raw


StackGeneric <Double>sd = new StackGeneric(3);
...

for (int nr = 0; nr < 3; nr++)


{
Double d_tmp = sd.pop();
System.out.print(d_tmp + " ");
}
}
}

Output 9.7 Dal Listato 9.10 Classe StackGenericRawTypeCase3.

Valori dello stack Double: 8.6 11.2 11.1


Tipi wildcard
Un aspetto importante da tenere presente quando si definiscono e utilizzano classi
generiche relativo alla mancata relazione di sottotipo tra i tipi generici. Ci
significa che se abbiamo le definizioni di classi class G<T> {}, class Bar {} e class Foo
extends Bar {} possiamo asserire che, mentre vero che Foo un sottotipo di Bar e
possiamo assegnare a un riferimento di tipo Bar un riferimento di tipo Foo, non
altrettanto vero che G<Foo> un sottotipo di G<Bar> e pertanto lassegnamento di un
oggetto di tipo G<Foo> non consentito a un riferimento di tipo G<Bar>.

Ancora, in modo meno astratto rispetto allesempio precedente, possiamo dire che
un ArrayList<Number> non mai superclasse di un ArrayList<Integer> anche se un Integer
un sottotipo di un Number e lo si pu assegnare a esso. Ci significa che, se volessimo
creare un metodo che sommi tutti i valori di un ArrayList<Integer> e poi di un
ArrayList<Double> , non potremmo scrivere il metodo medesimo con una segnatura cha
avesse come parametro un ArrayList<Number> perch, ripetiamo, la relazione di
ereditariet di un ArrayList di Integer o di Double verso un ArrayList di tipo Number non
ammessa; se lo fosse, potremmo assegnare, per esempio, senza la generazione di
errori a compile-time, un riferimento di tipo ArrayList<Integer> a un riferimento di tipo
ArrayList<Number> e poi tramite questultimo aggiungere alla lista anche numeri di tipo
double . Ovviamente, tuttavia, a run-time avremmo uneccezione di tipo
ClassCastException perch abbiamo tentato di aggiungere dei numeri in virgola mobile a
una lista che poteva contenere solo numeri interi. Per ovviare a questo problema il
metodo dovr essere scritto usando una notazione particolare che fa uso dei tipi con
carattere jolly (wildcard).
Sintassi 9.3 Tipo wildcard unbounded.

<?>

La Sintassi 9.3 indica, attraverso il carattere punto interrogativo ?, che il tipo


rappresentato da qualsiasi tipo di oggetto. In effetti si tratta di uno shortcut per la
forma <? extends Object>.
Sintassi 9.4 Tipo wildcard upper-bound.
<? extends Type>

La Sintassi 9.4 indica che il tipo un qualsiasi sottotipo di Type o Type stesso.
Sintassi 9.5 Tipo wildcard lower-bound.

<? super Type>


La Sintassi 9.5 indica che il tipo un qualsiasi supertipo di Type o Type stesso.

INVARIANZA, COVARIANZA E CONTROVARIANZA


Il tipo wildcard consente di aggirare elegantemente la limitazione descritta precedentemente di
mancanza di relazione di ereditariet tra i tipi generici, definita come invarianza. Grazie al tipo
wildcard possiamo infatti avere sia una relazione, definita come covarianza, con cui per esempio
dato un tipo List<S> potremo sempre lecitamente assegnare un suo riferimento a un riferimento di
tipo List<? extends T> se S un sottotipo di T, sia una relazione, definita come controvarianza, con
cui per esempio dato un tipo List<S> potremo sempre lecitamente assegnare un suo riferimento a
un riferimento di tipo List<? super T> se S un supertipo di T. Cos, ritornando al nostro ArrayList
potremo (covarianza) scrivere la seguente istruzione: List<? extends Number> ln_e = new
ArrayList<Integer>() cos come (controvarianza) List<? super Integer> ln_a = new
ArrayList<Number>().

Listato 9.11 Classi SumGenericArrayWildcard e SumGenericArrayWildcardClient.

package com.pellegrinoprincipe;

import java.util.ArrayList;

class SumGenericArrayWildcard
{
public SumGenericArrayWildcard() {}

// calcola la somma di ArrayList di tipi numerici derivati da Number


public double sum(ArrayList<? extends Number> al)
{
double total = 0;
for (Number nr : al)
total += nr.doubleValue();
return total;
}
}

public class SumGenericArrayWildcardClient


{
public static void main(String[] args)
{
SumGenericArrayWildcard sgw = new SumGenericArrayWildcard();

Double d[] = { 11.1, 11.2 };


Integer i[] = { 12, 13 };

ArrayList<Integer> ali = new ArrayList<>(); // ArrayList di interi


for (Integer i_e : i)
ali.add(i_e);

ArrayList<Double> ald = new ArrayList<>(); // ArrayList di double


for (Double d_e : d)
ald.add(d_e);

// mostriamo la somma
System.out.print("ArrayList<Integer> somma: " + sgw.sum(ali));
System.out.println(" | ArrayList<Double> somma: " + sgw.sum(ald));
}
}

Output 9.8 Dal Listato 9.11 Classi SumGenericArrayWildcard e SumGenericArrayWildcardClient.

ArrayList<Integer> somma: 25.0 | ArrayList<Double> somma: 22.299999999999997

Dal Listato 9.11 vediamo che nella classe SumGenericArrayWildcard stato definito il
metodo sum che permette di sommare gli elementi di un ArrayList di differente tipo
passato come argomento. Infatti, nella classe SumGenericArrayWildcardClient notiamo
come al metodo sum vengano passati senza alcun problema, e in modo indifferente, sia
un ArrayList di oggetti Integer sia un ArrayList di oggetti Double.

Vediamo infine, per completezza di trattazione, che cosa sarebbe accaduto se


avessimo scritto il metodo sum come nello Snippet 9.8 e avessimo compilato il
sorgente (Errore 9.3).
Snippet 9.8 Metodo sum che genera un errore.

public double sum(ArrayList<Number> al)


{
double total = 0;
for (Number nr : al)
total += nr.doubleValue();
return total;
}

Errore 9.3 Compilazione del Listato 9.11 con la modifica del metodo sum dello Snippet 9.8.

...SumGenericArrayWildcardClient.java:38: error: incompatible types:


ArrayList<Double> cannot be converted to ArrayList<Number>
System.out.println(" | ArrayList<Double> somma: " + sgw.sum(ald));
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
2 errors

Il messaggio dellErrore 9.3 ci indica chiaramente qual il problema, ovvero che


non possibile assegnare un ArrayList di Double a un ArrayList di Number (lo stesso vale
per lArrayList di Integer il cui caso, ovvero ArrayList<Integer> cannot be converted to
ArrayList<Number> - System.out.print("ArrayList<Integer> somma: " + sgw.sum(ali)) , non stato
mostrato nelloutput della compilazione in quanto lo stesso stato reso semplificato).
In conclusione, sebbene il tipo wildcard offra una flessibilit notevole, si deve
considerare quanto segue.

Non si pu usare come nome di tipo nel corpo del metodo.


Se non specificato per esso un upper bound, allora si possono invocare solo i
metodi di Object sui valori passati come argomenti.
Un metodo che lo usa nella lista dei suoi parametri, se ha come parametro
formale un tipo collezione (come un ArrayList), non pu in tale collezione
aggiungere nuovi elementi, tranne per se il wildcard utilizzato di tipo lower-
bound.
Non pu essere usato nella sezione dei parametri di tipo.
Capitolo 10
Programmazione funzionale

Lo scopo principale di un calcolatore elettronico, qualunque sia la sua complessit


costruttiva e potenza di elaborazione, quello di manipolare delle informazioni
simboliche che possono essere semplici, come un elenco di un insieme di impiegati
di unazienda, o complesse, come una serie di equazioni che descrivono un modello
matematico dei processi geofisici. Queste informazioni sono poi impiegate per
eseguire delle computazioni e per esprimere dei risultati come possono essere, per
ritornare ai nostri esempi, il numero totale degli impiegati dellazienda esaminata
oppure un diagramma della temperatura del nostro pianeta sino al 2050.
Dal punto di vista informatico, le computazioni sono formalizzate ed eseguite
avvalendosi di costrutti e istruzioni propri di linguaggi artificiali e formali, ovvero di
appositi linguaggi di programmazione che possono essere progettati e implementati
con differenti stili o paradigmi di programmazione come, per esempio, quello
orientato agli oggetti, quello logico o quello funzionale. Ciascuno di questi paradigmi
fornisce al programmatore differenti costrutti e diversi strumenti espressivi che gli
consentono di modellare il problema che intende risolvere.
Cos avremo che un software per un programmatore che si avvarr dello stile
object-oriented sar strutturato pensando primariamente agli oggetti che lo potranno
comporre, mentre un software per un programmatore che si avvarr dello stile logico
sar espresso utilizzando gli strumenti del sistema formale conosciuto come teoria
del primo ordine (first-order logic o first-order predicate calculus) ovvero lo stesso
sar composto da una serie di fatti che descrivono situazioni vere e una serie di
regole che consentono di dedurre nuove situazioni vere sulla base dei fatti a
disposizione.
Infine, un programmatore che ricorrer allo stile funzionale modeller un suo
programma avvalendosi principalmente della valutazione e combinazione di funzioni
che saranno, quindi, il suo principale strumento di composizione logico-algoritmico.
Dei paradigmi appena discussi, lobject-oriented oggi quello che ha riscosso
maggior successo ed adottato nei pi importanti linguaggi di programmazione come
Java, C++, C# e cos via.
Tuttavia, sebbene la OOP venga utilizzata per scrivere software per qualsiasi
dominio applicativo, in questi ultimi anni si sta assistendo a una forte rivalutazione
del paradigma funzionale e dei linguaggi funzionali o multiparadigma che ne
permettono lutilizzo.
Quanto detto dovuto soprattutto ad alcune caratteristiche peculiari di tale
modello, come per esempio la presenza di strutture di dati immutabili e lassenza di
side-effect nelle funzioni che, come analizzeremo meglio in seguito, consentono di
approcciare la programmazione concorrente, divenuta oggi un must per molte
applicazioni, in modo pi robusto, semplice e sicuro.
Possiamo dunque asserire che la metodologia della programmazione funzionale
finalmente uscita fuori dalle immacolate torri di avorio dei centri di ricerca
universitari, e i suoi fondamentali costrutti, cos come le sue rilevanti caratteristiche
di design, si stanno implementando, totalmente o in parte, nei maggiori linguaggi
imperativi mainstream tra cui Java nellattuale ottava versione.

UN EXCURSUS STORICO
Contrariamente a quanto si pu pensare, la programmazione funzionale e i suoi sottostanti concetti
hanno unorigine molto antica addirittura databile in unepoca dove i computer digitali, cos come
noi li conosciamo, non avevano ancora fatto la loro comparsa. Infatti, il suo seme germinale si deve
agli studi dei matematici e logici statunitensi Alonzo Church il quale negli anni Trenta del
Novecento svilupp il lambda calcolo (-calculus) con cui formalizzava una serie di regole per
definire e invocare (o applicare) le funzioni e Haskell Brooks Curry il quale, sempre nei primi
anni del 1930, svilupp la logica combinatoria, gi ideata in maniera indipendente dal matematico
russo Moses Shnfinkel negli anni Venti, come un modello teorico alternativo per effettuare
computazioni (in pratica tale modello esamina come i combinatori, che sono in pratica delle
funzioni, si possono combinare per rappresentare una computazione). Successivamente a quei
studi, il primo linguaggio funzionale a impiegarne alcuni dei costrutti fondamentali, come le funzioni
anonime e la ricorsione, fu certamente il Lisp (o LISP, LISt Processor), inventato nel 1958 da John
McCarty al MIT, cui seguirono negli anni molteplici dialetti tra cui i pi conosciuti sono Common
Lisp (sviluppato a partire dalle idee espresse nel libro Common Lisp the Language apparso nel
1984 a opera di Guy Lewis Steele, Scott Elliott Fahlman e altri e poi standardizzato con delle
differenze nel 1994 dal comitato ANSI X3J13) e Scheme (inventato nel 1975 da Guy Lewis Steele
e Gerald Jay Sussman). Arriviamo al 1966, quando Peter Landin ide il linguaggio ISWIM (If you
See What I Mean), il cui core principale era unestensione del -calculus puro con dati primitivi e
associate funzioni primitive. In effetti, nonostante Lisp fosse apparso prima di ISWIM, questultimo
poteva essere considerato, dal punto di vista della programmazione funzionale, pi puro e questo
perch, per esempio, rispetto a Lisp non permetteva laggiornamento di variabili (di fatto, Lisp era
considerabile anche imperativo, e ci lo rendeva un linguaggio multiparadigma).
Successivamente, nel 1978, lo scienziato americano John Warner Backus, gi ideatore nel 1954 di
FORTRAN, ritenuto il primo linguaggio di programmazione ad alto livello mai apparso, present, in
occasione del conferimento dellACM Turing Award, una storica lecture intitolata Can Programming
Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs, dove
esaltava la programmazione funzionale rispetto a quella imperativa spiegando i motivi per cui era
da preferirla (pi leggibile, pi affidabile, pi corretta e cos via). In pi, per inquadrare meglio le sue
argomentazioni, propose FP (Function Programming), un linguaggio funzionale puro senza variabili
dove le computazioni si eseguivano a partire da un set di funzioni primitive messe insieme da un
set di combinatori estremamente flessibili. Pi o meno nello stesso periodo (1973), linformatico
britannico Robin Milner ide ML (Meta Language), uno straordinario linguaggio funzionale impuro
con delle avanzate caratteristiche presenti oggi in molti linguaggi di programmazioni moderni (type
inference, static typing, parametric polymorphism, exception handling, garbage collection e cos
via). In conclusione, ML ebbe uninfluenza notevole per lo sviluppo di successivi linguaggi funzionali
(puri, impuri e multiparadigma) quali, solo per citarne i pi importanti in ordine di apparizione, SML
(Standard ML), nel 1983; Caml (Categorical Abstract Machine Language), nel 1985; Miranda nel
1985; Haskell nel 1990; OCaml (Objective Caml) nel 1996; Scala nel 2003; F# nel 2005.
Concetti propedeutici
La programmazione funzionale consente di approcciare la progettazione e lo
sviluppo del codice in un modo completamente differente rispetto alla consueta
programmazione imperativa.
Come prima considerazione discriminante, possiamo subito dire che lo stile
funzionale predilige la composizione e valutazione di espressioni, formate da
funzioni, laddove quello imperativo privilegia la composizione di istruzioni atte a
definire espliciti comandi operativi.
Lo stile funzionale, inoltre, permette di esprimere un problema algoritmico in un
modo comprensibile e dichiarativo, ossia non costringe a delineare il dettaglio
esecutivo necessario per raggiungere il risultato come, chiaramente, fa lo stile
imperativo, dove dobbiamo fornire step-by-step tutte le istruzioni occorrenti.
In pratica, il paradigma funzionale modella lalgoritmo indicando cosa si vuole
ottenere oppure, detto in altro modo, che cosa calcola la funzione, mentre il
paradigma imperativo lo modella indicando come ottenerlo oppure, detto in altri
termini, come deve calcolare la funzione.
Se dovessimo, per esempio, implementare una routine che ci mostrasse tutti gli
impiegati dellazienda di fantasia Acme assunti dopo il 1995 nel modo imperativo
dovremmo scrivere la seguente sequenza di comandi.

1. Fornisci un lista di impiegati dellazienda Acme.


2. Ottieni il successivo impiegato dalla lista.
3. Domanda: limpiegato stato assunto dopo il 1995? Se s allora mostrane un
dettaglio anagrafico e lavorativo.
4. Domanda: ci sono altri impiegati nella lista? Se s allora ritorna al punto 2.

Lo stesso algoritmo, in modo funzionale, lo potremmo invece scrivere nel seguente


modo.

Mostra dalla lista fornita il dettaglio anagrafico e lavorativo di ogni impiegato


dellazienda Acme assunto dopo il 1995.

Laddove notiamo come, in modo pi conciso, pi naturale e maggiormente


leggibile risolviamo lo stesso algoritmo di quello precedentemente visto con lo stile
imperativo.
In pratica, lengine funzionale, e non il programmatore, partendo delle espressioni
dichiarative descritte, le valuta e le risolve autonomamente per pervenire alla
computazione del risultato finale.
La programmazione funzionale, quindi, consente di istruire il calcolatore su cosa
fare, con un alto livello di astrazione, efficacia e naturalezza e senza la necessit di
specificare in modo imperativo dei comandi da eseguire. Star poi al runtime del
linguaggio funzionale decidere, in accordo con le specifiche, il migliore ordine di
valutazione delle espressioni, se una certa funzione deve essere parallelizzata, se una
funzione deve essere valutata totalmente e cos via.
NOTA
La programmazione funzionale, come visto, abbraccia lidea generale dello stile dichiarativo per
la formulazione di un programma. Questo stile pu essere notato in modo pi specifico ed
evidente nei linguaggi come, per esempio, SQL o HTML, dove ci che si desidera ottenere
espresso semplicemente attraverso una query (select * from impiegati where anno_assunzione
> 1995) oppure attraverso dei tag (<input type="text" id="impiegato001" value="Paolo Rocca"
/>), lasciando poi al relativo engine lonere di come selezionare gli impiegati o renderizzare la
casella di testo.

Passiamo, quindi, a descrivere un altro aspetto di rilievo della programmazione


funzionale che legato al concetto di immutabilit dello stato. Per spiegarlo
significativamente, opportuno, ancora una volta, partire dallapproccio al problem
solving della programmazione imperativa.
In pratica, con la programmazione imperativa, loperazione maggiormente
ricorrente quella riguardante lutilizzo delle variabili, ossia limpiego di celle di
memoria dove memorizzare dei valori, che rappresentano lo stato di un programma e
che cambiano durante il tempo di esecuzione del programma.
DETTAGLIO
In termini pi rigorosi potremmo definire lo stato come una sequenza finita di coppie nella forma
(J, v) dove v il valore della variabile J in un determinato tempo. In pratica, lo stato un modello
logico di storage che crea associazioni tra locazioni di memoria e valori laddove durante
lesecuzione di un programma abbiamo la generazione di sequenze di stati e la transizione da
uno stato al successivo determinata, principalmente, dalle operazioni di assegnamento.

Per esempio, se avessimo lespressione K = (a + j) / (z - m) un compilatore,


tipicamente, valuterebbe prima lespressione (a + j) e memorizzerebbe il relativo
risultato in un apposito indirizzo di memoria. Successivamente, valuterebbe
lespressione (z - m) e ne memorizzerebbe il risultato in unaltra area di memoria.
Infine, a partire dai valori intermedi precedentemente memorizzati, eseguirebbe la
valutazione dellespressione finale (ossia la divisione), e ne memorizzerebbe il
risultato nella locazione di memoria di cui la variabile K, che in seguito potrebbe
cambiare il suo stato a causa di un assegnamento di un altro valore.
Dunque, il paradigma della programmazione imperativa descrive le sue
computazioni in termini di stati del programma e di istruzioni che lo modificano in
tempi successivi.
Di converso, nella programmazione funzionale, e di conseguenza con un
linguaggio di programmazione funzionale puro, non si utilizzano delle variabili
modificabili e non si hanno delle istruzioni di assegnamento e pertanto non si opera
mai con i cambiamenti di stato.
Da quanto detto consegue che in tali linguaggi funzionali non esistono le consuete
istruzioni iterative proprie dei linguaggi imperativi (si pensi al costrutto for) perch
sono chiaramente controllate da variabili che cambiano stato; quindi, per eseguire le
ripetizioni ci si avvale del potente ed espressivo meccanismo della ricorsione. I
programmi funzionali sono dunque composti da un insieme di definizioni di funzioni
e, come si esprime in termini matematici, dalla loro applicazione ai relativi argomenti
oppure, detto in termini informatici, dalla loro invocazione con passaggio dei
parametri attuali.
TERMINOLOGIA
Nei linguaggi funzionali, in conseguenza di quanto sin qui detto, le funzioni sono dette side-effect
free, ovvero comunicano con lambiente solo attraverso lottenimento di input e la restituzione di
output senza modificare quindi alcuno stato, sia esso globale o di un qualche oggetto. In pi, le
funzioni side-effect free godono di una propriet detta di referential transparency che implica che
tali funzioni, in modo consistente, ritornano sempre lo stesso risultato dato lo stesso input e
indipendentemente da dove e quando sono invocate. In pratica non dipendendo dal contesto
in cui sono valutate, consentono di scrivere programmi pi facilmente comprensibili, ottimizzabili
e testabili.

MODELLI COMPUTAZIONALI A CONFRONTO


I linguaggi imperativi condividono un modello computazionale che rappresenta unastrazione del
sottostante calcolatore elettronico dove, in breve, la computazione procede modificando valori
memorizzati in locazioni di memoria. Questo modello definito von Neumann Architecture, dal
nome dello scienziato ungherese John von Neumann che, nel 1945, lo ide, e fu, ed ancora, alla
base del design e della costruzione dei computer e quindi dei linguaggi imperativi che vi si rifanno e
che sono dunque delle astrazioni della macchina di von Neumann. Nella sostanza, come illustra la
Figura 10.1, nel modello di von Neumann un computer costituito da una CPU (Central Processing
Unit) per il controllo e lesecuzione dellelaborazione, al cui interno si trovano lALU (Arithmetic and
Logic Unit) e una Control Unit; da celle di memoria identificate da un indirizzo numerico atte a
ospitare i dati coinvolti nellelaborazione; da dispositivi per linput e loutput dei dati da e verso
lelaboratore; da un bus di comunicazione tra le varie parti per il transito di dati, indirizzi e segnali di
controllo. In pi, abbiamo che sia i dati sia le istruzioni di programmazione sono memorizzate nella
memoria. In pratica nel modello di von Neumann abbiamo due elementi caratterizzanti: la memoria,
che archivia le informazioni anzidette, e il processore, che fornisce operazioni per modificarne il
contenuto ossia lo stato. I linguaggi funzionali, invece, prescindono dal modello di funzionamento
dellelaboratore (sono detti, infatti, non von Neumann languages) rifacendosi, principalmente e,
come abbiamo gi avuto modo di anticipare, alla teoria matematica del lambda calcolo ideata da
Alonzo Church e che in breve si basa sui concetti di funzione matematica, applicazione di funzioni,
variabili in senso matematico e ricorsione. Quindi, principalmente, in un linguaggio funzionale
avremo che: un programma unoperazione che associa un input con un output ovvero una
funzione; la definizione del programma avverr principalmente mediante lapplicazione delle
funzioni ai loro argomenti e la composizione di funzioni; il programma sar guidato dalla
valutazione di espressioni e non da comandi; le variabili (il loro nome) saranno associate ai
valori (value semantics) e non alle locazioni di memoria (storage semantics) e dunque saranno
oggetti non mutabili (il loro valore dopo il binding non potr cambiare e quindi agiranno come delle
costanti); dovremo utilizzare la ricorsione come struttura di controllo per le iterazioni non essendo
previste quelle che cambiano lo stato, come per esempio il for o il while.

Figura 10.1 Architettura semplificata di un computer basata sul modello di von Neumann.

Arriviamo, a questo punto, a dettagliare con un certo grado di approfondimento


cosa rappresenti una funzione, sia dal punto di vista prettamente matematico sia dal
punto di vista del lambda calcolo, in modo da poter comprendere e inquadrare meglio
la programmazione funzionale nel suo complesso.
Dal punto di vista matematico, una funzione una legge o regola che associa a
ogni elemento x di un insieme X di valori uno e un solo elemento y di un insieme Y
di valori (Sintassi 10.1).
Sintassi 10.1 Sintassi formale di una funzione matematica.
y = (x) oppure : X Y

Guardando alla Sintassi 10.1 abbiamo che linsieme X denominato come il


dominio di mentre linsieme Y denominato come il codominio di . In pi x in (x),
che rappresenta qualsiasi valore dellinsieme X, chiamata variabile indipendente,
mentre y dellinsieme Y, cos come definita dallequazione y = (x), chiamata
variabile dipendente.
In termini pi pratici possiamo dire che una funzione prende un valore in input
da un elemento di un insieme X (dominio), lo trasforma e lo restituisce in output
come elemento di un insieme Y (codominio).

Per esempio, data la seguente definizione di funzione (x) = x avremo che dato un
elemento dellinsieme X (dominio), diciamo 2, 3, 4 e cos via, la funzione ne calcoler
il cubo come elemento dellinsieme Y (codominio), ossia 8, 27, 64 e cos via. Oppure,
affermato in altri termini, la funzione (x) = x assocer qualsiasi numero reale di X al
numero x.

Partendo da questa nozione basilare di funzione, e dallo studio delle funzioni pi in


generale, Alonzo Church, ha costruito la potente teoria matematica del lambda
calcolo unitamente a una serie di regole sintattiche e notazionali (lambda notation)
con cui definire, combinare e applicare le funzioni. Cos, per esempio, una funzione
come (x) = x + 5 che aggiunge 5 a ogni argomento fornito pu essere scritta secondo
la notazione di Church come quella presentata nello Snippet 10.1:
Snippet 10.1 Una definizione di funzione scritta secondo la notazione di Church.

(x.x + 5)

che indica la definizione di una funzione anonima (senza un nome), espressa dalla
lettera greca lambda (), dotata di un singolo parametro x, cui fa seguito, dopo il
simbolo punto (.),il corpo della funzione, che nel nostro caso dato da x + 5, e che
rappresenta lespressione che viene valutata per fornire un correlativo valore di
output.
Inoltre, secondo la terminologia di Church, tutta la funzione anonima (x.x + 5) pu
essere denominata lambda expression.
NOTA
Nel formalismo di Church tutto definito come una funzione (numeri, operatori e cos via). Per
evitare di assegnare un nome a ognuna di esse (sarebbe stato oggettivamente impraticabile),
Church decise di introdurre la notazione in precedenza illustrata, che permetteva di scriverle
senza attribuirvi un nome.

Continuando con il nostro esempio, se volessimo assegnare come argomento alla


predetta funzione il valore di 50 dovremmo scrivere come nello Snippet 10.2.
Snippet 10.2 Applicazione di una funzione secondo la notazione di Church.

(x.x + 5) 50
Lapplicazione della funzione eseguita scrivendo la funzione stessa seguita
dallargomento. Dopo di ci, la stessa funzione viene valutata come 50 + 5 = 55,
ovvero avviene la sostituzione di ogni occorrenza della variabile di cui il parametro,
ossia x, con il valore dellargomento fornito, ossia 50; infine viene invocata la
funzione di cui loperatore + e poi viene fornito il risultato finale, ossia 55.

Per rendere meno astratto quanto esemplificato dagli Snippet 10.1 e 10.2
proponiamo una conversione della relativa sintassi con la sintassi propria di
JavaScript (Snippet 10.3), il quale, contrariamente a quanto si possa pensare, un
avanzato linguaggio di programmazione multiparadigma che oggi, tra le altre cose,
diventato un indispensabile strumento di scrittura delle moderne applicazioni web.
Snippet 10.3 Conversione della funzione (x.x + 5) e sua applicazione.

// esempio di definizione di una funzione anonima


function(x) { return x + 5; }

// esempio di applicazione di una funzione anonima


(function(x) { return x + 5; }) (50) // risultato 55

Mostriamo ora un altro esempio di lambda expression (Snippet 10.4) che evidenzia
unaltra caratteristica di rilievo del lambda calcolo (che altres un aspetto
fondamentale della programmazione funzionale), ossia quella per cui le funzioni sono
trattate come dei valori di prima classe (first-class values) e quindi possono essere
passate come argomenti ad altre funzioni, possono essere restituite come risultato da
altre funzioni oppure possono essere assegnate come valori ad altre varabili.
TERMINOLOGIA
Nella programmazione funzionale si incontra sovente il termine higher-order function (funzione di
ordine superiore), che sta a indicare una funzione che pu prendere come argomenti altre
funzioni e/o restituire una funzione come risultato della sua computazione.
Snippet 10.4 Definizione di una funzione che prende come argomento unaltra funzione.

(op.x(op x x))

Prima di spiegare come interpretare la funzione di cui lo Snippet 10.4 occorre


subito precisare che il lambda calcolo non fornisce un supporto integrato per la
scrittura di funzioni che accettano argomenti multipli, e pertanto ci consente di
raggiungere comunque il risultato desiderato mediante luso di funzioni di ordine
superiore che ritornano altre funzioni come risultato.
Per esempio, supponendo di avere unespressione come x + y, laddove le variabili x
e y debbano essere sostituite dai valori forniti dagli argomenti m ed n, non potremmo
scrivere la lambda expression come ((x, y).x + y) m n ma, invece, la dovremmo
scrivere come (x.y.x + y) m n, ovvero come una funzione che prende largomento m
in x e ritorna unaltra funzione che prende largomento n in y e ritorna la loro somma
come risultato.
TERMINOLOGIA
La trasformazione di una funzione con argomenti multipli in una funzione che prende un
argomento e ritorna unaltra funzione che prende un altro argomento, e cos via in una lunga
catena di ritorni/invocazioni per quanti sono gli argomenti necessari, detta currying, in onore del
matematico americano Haskell Brooks Curry, che rese popolare tale idea e che fu uno dei
pionieri del lambda calcolo. Per completezza di trattazione, utile ricordare che, in effetti, la citata
tecnica fu descritta per la prima volta dal matematico e logico tedesco Gottlob Frege nel 1893 e
poi, successivamente, nel 1924, da Moses Schnfinkel (il currying infatti anche denominato
schnfinkeling).

Ritornando quindi al nostro Snippet 10.4, se volessimo applicare la relativa


funzione con i seguenti argomenti (Snippet 10.5):
Snippet 10.5 Applicazione di una higher-order function.
(op.x(op x x)) (+) 10

avremmo lesecuzione dei seguenti passi computazionali.

1. Sar applicata la funzione passando al parametro op largomento + che una


funzione per eseguire la somma tra due numeri.
2. Sar ottenuto come risultato unaltra funzione ossia x.((+) x x)) 10.
3. Sar applicata la funzione ritornata al punto 2 passando al parametro x il valore
10.
4. Sar ottenuto come risultato lespressione (+) 10 10, che rappresenta una
notazione alternativa per indicare la somma tra due numeri e che dar, a sua
volta, come risultato il valore 20.

Snippet 10.6 Conversione della funzione (op.x(op x x)) e sua applicazione.


function plus(o1)
{
// chiaramente la definizione della funzione + built-in nel linguaggio
return function(o2) { return o1 + o2; }
}

(function(op)
{
return function(x)
{
// currying
return op(x)(x);
}
}) (plus)(10)

Vediamo ora un altro esempio di lambda expression (Snippet 10.7) che introduce
ai concetti di variabili legate (bound variable) e variabili libere (free variable).
Snippet 10.7 Definizione di una funzione con una variabile libera.
(x.x * y)

Dato lo Snippet 10.7 possiamo asserire che la relativa lambda expression definisce
una funzione anonima che ha la variabile x legata al body della funzione stessa (x *
y), ossia ha visibilit (scope) solo in quel corpo, e ha la variabile y libera, ossia che
stata dichiarata altrove, cio al di fuori dello scope di x.

In pratica, per fare un paragone con la terminologia delle funzioni di un qualsiasi


linguaggio di programmazione, data una funzione, un suo parametro formale oppure
una variabile a essa locale, definita cio nel suo blocco, denominabile come
variabile legata; una variabile a essa non locale, come una variabile globale oppure
una variabile definita nel blocco di una funzione che contiene la definizione di tale
funzione (che dunque nidificata) denominabile come variabile libera.
Quanto appena detto ha unimplicazione fondamentale nei linguaggi funzionali
perch strettamente legato al concetto di closure, che stato descritto per la prima
volta da Peter Landin in uno scritto del 1964 intitolato The Mechanical Evaluation of
Expressions, il quale, in una forma apparentemente criptica, asser: We represent the
value of -expression by a bundle of information called a closure, comprising the -
expression and the environment relative to which it was evaluated. We must therefore
arrange that such a bundle is correctly interpreted whenever it has to be applied to
some argument.
In poche parole, una closure rappresentata da una funzione o, in modo
generalizzato, da un sottoprogramma, e dal suo referencing environment, laddove tale
environment diventa essenziale se la funzione stessa pu essere invocata
arbitrariamente in qualsiasi parte del programma.
TERMINOLOGIA
I l referencing environment unastrazione software che contiene linsieme di tutti i nomi che
associano variabili e che sono visibili in un determinato punto di un programma, ovvero
utilizzabili. In pratica, esso identifica una collezione di scopes (ambiti di risoluzione o di visibilit)
che vengono esaminati al fine di trovare il binding nome-variabile ricercato. Nei linguaggi con
scope statico (static-scoped), tale insieme include tutti i nomi delle variabili definite localmente e i
nomi di quelle definite negli eventuali blocchi contenitori (i suoi ancestor). Nei linguaggi con scope
dinamico (dynamic scoped), tale insieme contiene tutti i nomi delle variabili definite localmente e i
nomi di quelle delle funzioni, in uneventuale pila di chiamate, ancora in esecuzione (cio
correntemente attive e non terminate).

STATIC O LEXICAL SCOPE E DYNAMIC SCOPE


Come pi volte detto, lo scope di una variabile dato da quellinsieme di righe di codice in cui essa
visibile, e quindi referenziabile, oppure, in altri termini, da quella regione di un programma dove
un determinato binding nome-variabile attivo. I linguaggi di programmazione possono adottare
una regola di scope definita come statica o lessicale, che basata sulla struttura testuale del
programma ( detta anche spaziale, basata cio sul pi vicino blocco nidificato), o dinamica, che
basata sullordine in cui le funzioni sono invocate ( detta anche temporale, basata cio sulla pi
recente associazione). In pratica, con lo scope lessicale, il binding del nome a una variabile
determinato prima dellesecuzione del relativo programma (a compile time) leggendo il codice
sorgente e senza considerare il flusso di esecuzione computazionale che avviene a run-time. Con
lo scope dinamico, invece, il binding del nome a una variabile determinato solo durante
lesecuzione di un programma. Infatti, quando si raggiunge una statement che accede al nome di
una variabile, lultima dichiarazione raggiunta dallesecuzione del programma sar quella che
determiner il binding tra il nome e la variabile riferita. Dato, per esempio, il blocco di codice di cui
lo Snippet 10.8 avremo che:

con lo scope lessicale, la funzione bar, non trovando una dichiarazione locale della variabile x
utilizzer, per lassegnamento del relativo valore alla variabile z, quella trovata nel suo pi
prossimo blocco contenitore ascendente, ossia quello della funzione foo dove x = 10;
con lo scope dinamico la funzione bar, non trovando una dichiarazione locale della variabile x
utilizzer, per lassegnamento del relativo valore alla variabile z, quella trovata nella sua
precedente funzione chiamante (dynamic parent), ossia la funzione foo dove x = 10;
con lo scope lessicale, la funzione baz, non trovando una dichiarazione locale della variabile k
utilizzer, per lassegnamento del relativo valore alla variabile j, quella trovata nel suo pi
prossimo blocco contenitore ascendente, ossia quello della funzione foo dove k = 20;
con lo scope dinamico la funzione baz, non trovando una dichiarazione locale della variabile k
utilizzer, per lassegnamento del relativo valore alla variabile j, quella trovata nella sua
precedente funzione chiamante (dynamic parent), ossia la funzione foobar dove k = 10.

In definitiva, per entrambi, la ricerca di un binding per una variabile partir sempre dal blocco
contenitore locale ma, se esso non sar trovato, allora: per lo scope statico, la ricerca proseguir
nei successivi blocchi contenitori ascendenti (static parent) cos come lessicalmente scritti nel
codice; per lo scope dinamico, la ricerca proseguir con le precedenti funzioni chiamanti (dynamic
parent) in base al loro ordine di invocazione.

Snippet 10.8 Scope lessicale e scope dinamico.

function foo()
{
var x = 10;
var k = 20;
function bar()
{
var z = x;
var k = 1000;

// scope dinamico = 10; scope statico = 10
print(z);
foobar();
}
function baz()
{
var j = k;

// scope dinamico = 10; scope statico = 20
print(j);
}

function foobar()
{
var k = 10;
baz();
}

bar();
}
Approfondiamo, quindi, il concetto di closure attraverso una spiegazione prima
piuttosto pratica e immediata, e poi sicuramente pi formale laddove entrambe
consentiranno di avere una visione chiara e precisa di questa importante funzionalit.
Quando delle funzioni possono essere definite in modo innestato queste sono in
grado di utilizzare le variabili a esse locali e anche le variabili dichiarate nella loro
funzione contenitrice. Se per queste funzioni annidate possono essere passate come
argomenti di altre funzioni o possono essere altres passate come valori di ritorno,
ecco che allora sorge il problema di mantenere in vita le variabili della loro
funzione container che normalmente sono distrutte quando la funzione stessa termina
il suo compito elaborativo (in pratica quando si esce dalla funzione sia perch si
raggiunge la parentesi graffa di chiusura del suo blocco costitutivo sia perch si
invoca listruzione return).

Un linguaggio di programmazione che supporta le closure garantisce che le


variabili definite in una funzione contenitrice non cessano di esistere al termine di
tale funzione quando sono utilizzate da una funzione contenuta, ovvero persistono e
hanno un lifetime relativo al ciclo di vita del programma stesso (unlimited extent).
Quanto detto, quindi, implica che una funzione che pu essere invocata in qualsiasi
punto del programma deve avere sempre a disposizione il suo referencing
environment completo per portare a termine con successo il suo processo elaborativo.
Chiariamoci meglio le idee studiando lo Snippet 10.9, dove definiamo due
funzioni, la prima denominata multiplyBy e la seconda, in essa innestata, anonima e che
ritorna il risultato della computazione tra il parametro m di multiplyBy e il parametro n
della funzione anonima stessa.
Snippet 10.9 Un esempio pratico di closure.

function multiplyBy(m)
{
return function(n) { return m * n; }
}

multiplyBy(10)(5); // 50
multiplyBy(100)(9); // 900

Nello Snippet 10.9 la closure la funzione anonima definita nella funzione


multiplyBy il cui parametro m conservato in memoria poich impiegato dalla

closure per leffettuazione delloperazione di moltiplicazione con il suo parametro n.

Dopo la definizione delle funzioni summenzionate procediamo:

allinvocazione di multiplyBy(10) che ritorna una closure con associato al


parametro m il valore di 10;
allapplicazione della closure ritornata passando il valore 5 al suo parametro (n)
che moltiplicato per il valore 10 (la cui variabile contenitrice, ossia m, ancora
attiva) e restituisce il valore 50;
allinvocazione di multiplyBy(100) che ritorna una closure con associato al
parametro m il valore di 100;
allapplicazione della closure ritornata passando il valore 9 al suo parametro (n)
che moltiplicato per il valore 100 (la cui variabile contenitrice, ossia m, ancora
attiva) e restituisce il valore 900.

In modo pi formale possiamo dire che dal punto di vista della closure la sua
variabile locale n la variabile legata (bound variable), mentre la variabile della
funzione multiplyBy la variabile libera (free variable).

A questo punto, possiamo asserire che una funzione che nel momento della sua
creazione ha chiuso o catturato nel suo ambito le variabili (free variable) di una
funzione a essa esterna facendole vivere fino al termine della sua esecuzione
definibile closure.
In termini ancora pi rigorosi e generici possiamo dire che una closure si riferisce
a una lambda expression dove le free variable sono state chiuse (collegate, bind to) in
uno scope definito lessicalmente e che ha dato come risultato una closed expression.
Una closure, dunque, consiste nel body di una lambda function e dello scope (o
environment) dove tale lambda stata definita.
TERMINOLOGIA
Una lambda expression che non ha free variable definita closed term, mentre una lambda
expression che ha free variable definita open term.

In conclusione di questo breve excursus teorico possiamo sintetizzare dicendo che,


nellambito della programmazione funzionale, un programma immaginabile come
una descrizione di una determinata computazione laddove, se ignoriamo i dettagli
della computazione e ci concentriamo solo sul risultato computato, avremo che, nella
sostanza, il programma stesso diventa una sorta di scatola nera virtuale che
trasforma un qualche input in un qualche output (da questo punto di vista possiamo
infatti asserire che un programma fondamentalmente equivalente a una funzione
matematica).

Lo stile dichiarativo: un esempio


Uno stile di programmazione dichiarativo permette di esprimere la composizione
di un programma in un modo molto naturale grazie allutilizzo di costrutti che
indicano chiaramente il cosa si desidera ottenere piuttosto del come fare per ottenerla.
I seguenti snippet (10.10 e 10.11) mostrano la definizione di un form (con alcuni
widget tipo una label, un campo di testo, un pulsante e cos via), scritta sia in modo
dichiarativo utilizzando la tecnologia JavaFX e il linguaggio FXML, sia in modo
tradizionale o programmatico attraverso le API di Swing.
APPROFONDIMENTO
JavaFX una piattaforma software, creata sempre in seno a Oracle, che consente, in modo
semplificato, di progettare e sviluppare applicazioni di tipo RIA (Rich Internet Applications) che
siano compatibili in modo consistente su pi piattaforme. FXML invece un linguaggio di markup,
basato sullXML e incluso a partire da JavaFX 2.0, che consente la definizione delle interfacce
grafiche in modo dichiarativo e con una netta separazione del layer di presentazione dalla logica
applicativa.
Snippet 10.10 Creazione di un form in modo programmatico.

...
label_insert = new javax.swing.JLabel();
separator = new javax.swing.JSeparator();
lable_name = new javax.swing.JLabel();
text_name = new javax.swing.JTextField();
button_ok = new javax.swing.JButton();

setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
getContentPane().setLayout(new java.awt.GridLayout(5, 1));

label_insert.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
label_insert.setText("Inserisci i tuoi dati");
getContentPane().add(label_insert);
getContentPane().add(separator);

lable_name.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
lable_name.setText("NOME");
getContentPane().add(lable_name);
getContentPane().add(text_name);

button_ok.setText("OK");
getContentPane().add(button_ok);

pack();
...

Snippet 10.11 Creazione di un form in modo dichiarativo.

...
<GridPane prefHeight="283.0" prefWidth="480.0" AnchorPane.bottomAnchor="0.0"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Separator prefWidth="200.0" GridPane.columnIndex="0" GridPane.rowIndex="1" />
<Label text="Insrisci i tuoi dati" GridPane.columnIndex="0" GridPane.halignment="CENTER"
GridPane.rowIndex="0" />
<Label text="NOME" GridPane.columnIndex="0" GridPane.halignment="CENTER"
GridPane.rowIndex="2" />
<Button mnemonicParsing="false" prefWidth="369.0" text="OK" GridPane.columnIndex="0"
GridPane.halignment="CENTER" GridPane.rowIndex="4" />
<TextField prefWidth="445.0" GridPane.columnIndex="0" GridPane.halignment="CENTER"
GridPane.rowIndex="3" />
</children>
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="480.0" minWidth="10.0" prefWidth="480.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="240.0" minWidth="0.0" prefWidth="0.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
</GridPane>
...

Lo Snippet 10.10 mostra, in modo inequivocabile, come la definizione del form


presupponga la scrittura di una serie di istruzioni che definiscono tutti gli aspetti
necessari per creare e visualizzare il form stesso (creazione degli oggetti istanze delle
classi della GUI, chiamata di appositi metodi per impostare le propriet dei widget,
per aggiungerli al container e cos via), mentre lo Snippet 10.11 mostra come le
predette operazioni vengano fatte semplicemente scrivendo una serie di tag che
definiscono in modo molto naturale cosa desideriamo che venga renderizzato a video,
senza costringerci a conoscere tutti i sottostanti dettagli tecnici di creazione di un
form.

Immutabilit dello stato: un esempio


Limmutabilit dello stato un building-block della programmazione funzionale
ma pu essere ottenuta anche con il linguaggio Java al fine di avere diversi benefici,
da quelli pi specifici, come il caso della programmazione multithreading dove
limpiego di valori immutabili consente di eliminare i problemi legati alla
sincronizzazione (pi thread non possono modificare lo stesso valore condiviso
perch , per lappunto, immutabile), a quelli pi generici, come il caso di una
maggiore chiarezza del comportamento del codice e delle relative operazioni di
testing e debugging (se una variabile pu essere modificata in pi punti del
programma sicuramente pi difficoltoso scorrere migliaia di righe di codice per
scovare dove tale modifica sia avvenuta).
NOTA
La classe java.lang.String un ottimo esempio di un tipo di dato immutabile. Dato, infatti, un
oggetto di tipo String, quando utilizziamo un metodo come replace per sostituire una sequenza
di caratteri con unaltra, quello che otteniamo come risultato un nuovo oggetto di tipo String
con la sequenza di caratteri cambiata. La stringa originaria rimane pertanto immutata.

Prima di analizzare limplementazione di un tipo immodificabile (Listato 10.1)


elenchiamo una serie di regole che consentono di elaborare tale strategia.

Non permettere che una classe sia derivabile in modo da non consentire che i
suoi metodi siano sovrascrivibili e dunque ridefinibili. A tal fine utilizzare la
keyword final prima della keyword class.
Definire tutte le variabili della classe come private e final. In questo caso bisogna
ricordarsi che la loro inizializzazione pu avvenire contestualmente alla loro
dichiarazione oppure con operazioni di assegnamento effettuate nellambito di
un costruttore. In ogni caso, per evitare di scrivere un metodo get per ogni
variabile privata, soprattutto se non vi alcuna necessit di information hiding,
possibile anche qualificarle come public, poich il client utilizzatore non potr
comunque modificarle.
Utilizzare sempre delle copie degli oggetti puntati dalle variabili riferimento. Se
per esempio un client esterno, tramite il costruttore di una classe, passa come
argomento una variabile riferimento, allora occorre creare una copia
delloggetto da essa puntato e memorizzare il nuovo riferimento di tale oggetto.
Cos, allo stesso modo, se da un metodo di una classe bisogna ritornare a un
client esterno una variabile riferimento, allora occorre creare una copia del
relativo oggetto e ritornare al chiamante il nuovo riferimento.
Non definire dei metodi setter oppure qualsiasi altro metodo che possa
modificare le variabili di una classe.

Listato 10.1 Classe ImmutableClass.

package com.pellegrinoprincipe;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

final class CheckingAccount // non ereditabile


{
// immodificabili
public final String name;
public final String bank;
public final String city;
private final List<Integer> accounts;

public CheckingAccount(String name, String bank, String city, List<Integer> accounts)


{
this.name = name;
this.bank = bank;
this.city = city;
this.accounts = accounts;
}

public final List<Integer> getAccounts()


{
// vista della collezione immodificabile
return Collections.unmodifiableList(accounts);
}

public CheckingAccount orderAccounts()


{
// accounts sorted e l'originale non modificato
List<Integer> copy = new ArrayList<>(accounts);
Collections.sort(copy);
return new CheckingAccount(name, bank, city, copy);
}
}

public class ImmutableClass


{
public static void main(String[] args)
{
CheckingAccount MrRossi = new CheckingAccount("Rossi", "FixaBank", "Rome",
Arrays.asList(98856, 34556, 78999));

// Errore di compilazione: cannot assign a value to final variable bank


// MrRossi.bank = "AcmeBank";

// Eccezione: java.lang.UnsupportedOperationException
// MrRossi.getAccounts().clear();

// qui stampa [98856, 34556, 78999]


System.out.println(MrRossi.getAccounts());

// qui stampa [34556, 78999, 98856]


System.out.println(MrRossi.orderAccounts().getAccounts());
}
}

Output 10.1 Dal Listato 10.1 Classe ImmutableClass.


[98856, 34556, 78999]
[34556, 78999, 98856]

Il Listato 10.1 definisce la classe CheckingAccount come final e le variabili name, bank e
city di tipo String e accounts di tipo List<Integer> anchesse tutte final e dunque non
modificabili dopo la loro inizializzazione.
In questo caso, tuttavia importante ricordare che, se una variabile un
riferimento, allora per garantire che essa sia effettivamente non mutabile non
sufficiente dichiararla solo come final perch comunque lo stato interno del suo
oggetto puntato pu essere modificato. Infatti, rendere final una variabile riferimento
consente solo di evitare che la stessa possa contenere, successivamente, riferimenti di
altri oggetti.
Per la variabile accounts, quindi, forniamo un apposito metodo getAccounts che ritorna
al client una vista non modificabile della relativa lista (metodo unmodifiableList della
classe Collections).

Per le variabili name, bank e city non necessario fornire alcun metodo che ritorni
una loro copia perch contenendo oggetti di tipo String sono gi immutabili.

Infine definiamo il metodo orderAccounts che, piuttosto che ordinare la lista interna,
ritorna un nuovo un oggetto ordinato di tipo CheckingAccount con una copia di name, bank,
city e accounts.

Per quanto concerne la copia di accounts abbiamo usato il costruttore della classe
,
ArrayList public ArrayList(Collection<? extends E> c) , che accetta come argomento una
collezione da cui ricavare gli elementi per costruire la lista relativa.
In questultimo caso, importante precisare che non abbiamo avuto alcun
problema a effettuare la copia (che di tipo shallow copy) perch gli elementi di
accounts sono di tipo primitivo (int). Nel caso in cui, invece, gli elementi fossero stati
dei riferimenti ad oggetti, allora avremmo dovuto effettuare una copia di tipo deep
copy; questo perch il costruttore dellArrayList utilizzato in precedenza avrebbe s
copiato i valori delle variabili degli elementi, ma gli stessi avrebbero riguardato gli
indirizzi degli oggetti puntati che sarebbero stati, dunque, condivisi tra la
collezione originaria e la collezione risultante (ricordiamo che, nel caso dei tipi
primitivi, i valori copiati sono i valori direttamente l contenuti).
TERMINOLOGIA
Per shallow copy (Snippet 10.12) si intende la copia superficiale di un dato (per esempio, data
una variabile riferimento, si copier solo il riferimento alloggetto puntato), mentre per deep copy
(Snippet 10.13) si intende la copia profonda o completa di un dato (per esempio, data una
variabile riferimento, si creer prima un oggetto del suo stesso tipo e poi si copieranno in
questultimo, una a una, tutte le propriet della suddetta variabile riferimento considerando che,
se una propriet essa stessa un tipo riferimento, allora si ripeter il predetto procedimento).
Snippet 10.12 Shallow copy.

class M
{
public int a = 10;
}

// shallow copy
List<M> m1 = new ArrayList<>();
m1.add(new M());
List<M> m2 = new ArrayList<>(m1);

// qui la modifica dell'elemento 0 di m2 si ripercuote sull'elemento 0 di m1


m2.get(0).a = 22;

// stampa 22 22
System.out.println(m1.get(0).a + " " + m2.get(0).a);

Snippet 10.13 Deep copy.

class M
{
public int a = 10;
}

// deep copy
List<M> m1 = new ArrayList<>();
m1.add(new M());
List<M> m2 = new ArrayList<>(m1.size());

for (M element : m1)


{
M copy = new M();
copy.a = element.a;
m2.add(copy);
}

// qui la modifica dell'elemento 0 di m2 non si ripercuote sull'elemento 0 di m1


m2.get(0).a = 22;

// stampa 10 22
System.out.println(m1.get(0).a + " " + m2.get(0).a);
Figura 10.2 Shallow copy e deep copy.

Ritornando al nostro listato, esso definisce altres la classe ImmutableClass, che


permette linterazione con un oggetto di tipo CheckingAccount (identificatore MrRossi). Se
eliminassimo i commenti da MrRossi.bank = "AcmeBank"; e MrRossi.getAccounts().clear();
avremmo: nel primo caso, un errore di compilazione perch evidentemente stiamo
cercando di assegnare un valore a una variabile final; nel secondo caso, la
generazione di uneccezione di tipo UnsupportedOperationException perch la collezione
ritornata immodificabile e non permette di compiere operazioni di manipolazione
dei suoi dati interni.
Per quanto concerne, invece, la stampa degli elementi della lista accounts, notiamo
come la prima istruzione println ne stampa gli elementi non ordinati cos come sono
ritornati dal metodo getAccounts delloggetto MrRossi, mentre la seconda istruzione
println ne stampa gli elementi ordinati cos come sono ritornati dal metodo getAccounts
delloggetto CheckingAccount, ritornato a sua volta dallinvocazione del metodo
orderAccounts delloggetto MrRossi.

First-class values e closure: un esempio


In Java, fino alla versione 7 del linguaggio, solo i riferimenti ad oggetti e i valori
primitivi avevano lo status di first-class value. Con la versione corrente di Java, ossia
la 8, anche le funzioni sono valori e, pertanto, possono essere assegnate alle variabili,
possono essere passate come argomento di un metodo oppure possono essere
ritornate come risultato da un metodo.
NOTA
Approfondiremo tra breve in che modo Java ha implementato il concetto di funzione come first-
class value. Per ora, sufficiente guardare allesempio che segue come a unintroduzione pratica
e preliminare sui concetti che descrive.
Listato 10.2 Classe FirstClassValues.

package com.pellegrinoprincipe;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

public class FirstClassValues


{
private static Function<Integer, Integer> mult(int value)
{
// qui value catturata si forma una closure
return n -> value * n; // ritorna una funzione
}

public static void main(String args[])


{
// assegnamento di una funzione a una variabile
Function<Integer, Integer> add = x -> x + 1;

// applicazione della funzione


int result = add.apply(10);
System.out.println(result);

// una lista di parole


List<String> words = new ArrayList<>(Arrays.asList("Rosso", "Giallo", "Verde",
"Blu"));

// scorriamo gli elementi della lista dove al metodo forEach passiamo


// come argomento una funzione
System.out.print("[ ");
words.forEach(w -> {System.out.print(w + " ");});
System.out.println("]");

// ritorno di una funzione dall'invocazione di mult


Function<Integer, Integer> mul10 = mult(10);
result = mul10.apply(100);
System.out.println(result);
}
}

Output 10.2 Dal Listato 10.2 Classe FirstClassValues.

11
[ Rosso Giallo Verde Blu ]
1000

Il Listato 10.2 definisce la classe FirstClassValues che nel relativo metodo main
evidenzia: come assegnare una funzione a una variabile; come passare un riferimento
a una funzione come argomento di unaltra funzione; come ritornare da una funzione
il riferimento di unaltra funzione. Per quanto concerne il primo caso, la variabile add
conterr il riferimento a una funzione che prender come argomento un valore intero
e ritorner come risultato la somma tra tale valore e il valore 1. Dopo lapplicazione
di add con il valore 10 avremo, infatti, il risultato di 11.
Il secondo caso, invece, si concretizzer mediante il passaggio del riferimento di
una funzione al metodo forEach della classe List, la quale stamper il valore di ogni
elemento della relativa lista.
Infine, nel terzo caso, la variabile mul10 conterr il riferimento della funzione
ritornata dallinvocazione del metodo statico mult, la quale prender come argomento
un valore intero e ritorner come risultato il prodotto tra questultimo e il valore
memorizzato nellargomento value quando mult stata invocata.

Ci significa che quando invocheremo mult(10) verr ritornato un riferimento a una


funzione (closure) che avr catturato la variabile libera value associata al valore 10 e
che sar, poi, impiegata quando la funzione che conterr tale riferimento sar
effettivamente invocata.
Infatti, linvocazione di mult10.apply(100) assegner il valore 100 alla variabile n (suo
parametro), che sar quindi moltiplicata per la variabile value precedentemente
catturata, e dar come risultato il valore 1000.
La programmazione funzionale con Java
Oggi, i linguaggi di programmazione maggiormente utilizzati, come C#, C++,
Python, JavaScript e cos via, danno la possibilit di utilizzare le lambda expression,
consentendo pertanto di avvantaggiarsi dei pattern propri della programmazione
funzionale. Per quanto riguarda Java, invece, solo a partire dallattuale release 8 che
tale feature stata implementata.
Fondamentalmente, il motivo trainante di questa scelta, che ha provocato un
importante cambiamento nel linguaggio, da ricercarsi nella necessit di modificare
il framework delle collezioni del JDK al fine di farlo adattare alle moderne
architetture hardware che sono praticamente tutte multiprocessore o multicore e che
consentono, pertanto, lesecuzione di pi operazioni in parallelo.
Lobiettivo quello di permettere lesecuzione parallela di operazioni sui dati delle
collezioni, e dunque in modo pi efficiente rispetto allesecuzione delle stesse in
modalit seriale o single-threaded.
In pi, lutilizzo del parallelismo deve essere facilitato e pi accessibile per lo
sviluppatore, il quale, per impiegarlo, deve avere a disposizione degli strumenti
espressivi che sicuramente la programmazione funzionale e i suoi costrutti sono in
grado di offrire.
NOTA
Indubbiamente, anche prima di Java 8 e delle lambda expression era possibile scrivere
programmi che manipolavano dati delle collezioni in parallelo, ma ci richiedeva una notevole
esperienza e conoscenza della programmazione concorrente anche se si utilizzavano framework
che la rendevano leggermente semplificata come quello denominato fork/join.

In pratica, il framework delle collezioni stato rinnovato e migliorato in modo che


possa compiere operazioni di massa sui dati di una collezione con diversi thread e in
parallelo (parallel bulk operations) e nel far ci possa ottenere le relative operazioni
come funzionalit, ovvero mediante lindicazione di cosa deve essere applicato
su ogni elemento di una collezione piuttosto di come deve essere applicato.
Chiaramente, il cosa applicare, viene fornito dalle lambda expression, che sono
esplicitate mediante una notazione chiara, concisa e conveniente.

LAMBDA EXPRESSION E JAVA: UNA BREVE NOTA STORICA


Linclusione delle lambda expression nel linguaggio Java figlia di un lungo e acceso dibattito tra
la comunit di sviluppatori che cominci nel lontano 2006, allorquando furono avanzate tre
differenti proposte implementative (BGGA, CICE e FCM), che per non portarono ad alcun risultato
concreto, poich ciascuna aveva dei vantaggi e degli svantaggi e nessuna riusc a imporsi e a
mettere tutti daccordo. Successivamente, nel dicembre del 2009, Mark Reinhold (attuale Chief
Architect del Java Platform Group in Oracle) produsse un documento informale elaborato a
partire dalla selezione di alcuni elementi proposti dai lavori BGGA, CICE e FCM da cui far
riprendere una discussione sulle lambda expression in Java che avrebbe potuto portare alla
definizione di una proposta dettagliata e a uneventuale implementazione di un prototipo
esemplificativo e pratico. Dopo di ci, Brian Goetz (attuale Java Language Architect in Oracle)
produsse dei documenti, definiti come State of the Lambda (la versione 2 nel luglio del 2010, la
versione 3 nellottobre del 2010, la versione 4 nel dicembre del 2011 e altre) che, partendo da
quello originario di Reinhold, davano aggiornamenti e indicazioni informali e incrementali
sullaggiunta delle lambda expression nel linguaggio Java. Unitamente a questi ultimi documenti
informali, nel novembre del 2010, si present la specifica JSR 335 Lambda Expressions for the
Java Programming Language, che formalizz finalmente laggiunta delle lambda expression a Java
e che rappresenta, dunque, il documento ufficiale e di riferimento per questa importante feature.

NOTA
Nel complesso, la specifica JSR 335 estende il linguaggio Java con: lambda expression e
riferimenti a metodi; enhanced type inference e target typing; metodi di default e metodi statici
nelle interfacce; nuove classi e metodi (localizzati primariamente nei nuovi package
java.util.function e java.util.stream) al fine di supportare, sulle collezioni o su altre sorgenti di
dati, operazioni aggregate, sia sequenziali sia parallele; la modifica di alcuni tipi preesistenti
(BufferedReader, String, Files, Collection, Random e cos via), al fine di consentire
unintegrazione con le nuove caratteristiche quali stream, interfacce funzionali e cos via.

Estensione delle interfacce


I progettisti di Java, allorquando hanno dovuto decidere il modo di rappresentare le
lambda expression, si sono trovati di fronte il seguente dilemma: se era, cio, il caso
di introdurre un nuovo tipo funzione nellambito del type system del linguaggio, cos
che lo stesso fosse esprimibile con qualcosa come per esempio (int, double) -> int che
potesse, quindi, significare un tipo funzione che prende in input un int e un double e
ritorna in output un int.

Alla fine, tale idea stata respinta perch, fondamentalmente, non si voluto
aggiungere complessit al type system di Java soprattutto in considerazione del fatto
che si poteva ottenere la funzionalit delle lambda expression impiegando, adattando
ed estendendo processi e tipi gi presenti nel linguaggio.
Infatti, la soluzione trovata, come analizzeremo approfonditamente tra breve,
stata quella di impiegare i tipi di interfacce di callback con un solo metodo astratto
(chiamate nel contesto delle lambda expression interfacce funzionali) e un processo
di inferenza definito target typing, grazie al quale, dato un riferimento a una lambda
expression, il compilatore cerca di capire se, nel contesto di valutazione
dellespressione corrente, possibile effettuarne una conversione in un tipo di
interfaccia di callback corrispondente (compatibile) al tipo di destinazione atteso.
DETTAGLIO
Il processo di comprensione e di traduzione di una lambda expression in un oggetto (lambda
object), istanza di una determinata interfaccia funzionale, compiuto congiuntamente dal
compilatore e dal sistema di runtime di Java. Il primo si occuper di inferire e raccogliere dal
contesto di valutazione corrente di una lambda expression tutte le informazioni necessarie per la
creazione del predetto lambda object, mentre il secondo si occuper, a run-time, delleffettiva
creazione utilizzando determinati meccanismi (invokedynamic, method handles e cos via) cos
come sono esplicitati nel JSR 292 Supporting Dynamically Typed Languages on the Java
Platform.

Infine, a completamento delloperazione di estensione delle interfacce, si sono


altres introdotte due nuove caratteristiche che riguardano la possibilit di definire
metodi non astratti e metodi statici.
Di queste due ulteriori novit, la prima si resa necessaria perch era lunico modo
di consentire lintroduzione di nuovi metodi allinterno di interfacce esistenti senza
compromettere il funzionamento di programmi preesistenti (backward compatibility),
mentre la seconda ha unutilit di convenienza e opportunit in merito alla scelta
della collocazione dei metodi soprattutto quando si ha la necessit di creare o
manipolare oggetti di quel determinato tipo (, per esempio, sicuramente pi pratico e
diretto avere un metodo statico che invocato sul nome di uninterfaccia consente di
creare il relativo tipo piuttosto che avere una classe utility separata con un apposito
metodo statico factory).
NOTA
Ricordiamo che quando una classe implementa uninterfaccia deve obbligatoriamente fornire
unimplementazione per tutti i suoi metodi astratti. Se, quindi, semplicemente, si fossero aggiunti
alle interfacce dei nuovi metodi astratti ci avrebbe compromesso il funzionamento di altri
programmi che facevano uso di quelle interfacce. Tali programmi, infatti, avrebbero smesso di
funzionare perch sarebbe stato violato quel contratto che impone a ogni classe che
implementa uninterfaccia di definirne tutti i relativi metodi astratti.

Interfacce funzionali
Uninterfaccia funzionale uninterfaccia che ha un solo metodo astratto ed il
tipo che stato scelto dai progettisti di Java per rappresentare le lambda
expression. Se analizziamo le API del JDK ci rendiamo subito conto che questo tipo
di interfaccia, conosciuta anche con il nome di interfaccia di callback, gi presente
nel linguaggio ed ampiamente utilizzata nelle varie librerie. In pi, importante
dire che a queste interfacce gi esistenti se ne sono aggiunte altre presenti nel nuovo
package java.util.function, che sono interfacce funzionali general purpose atte a
fornire un ben determinato tipo di destinazione (target type) per le lambda
expression.
TERMINOLOGIA
Le interfacce funzionali erano precedentemente conosciute con il nome di interfacce SAM (Single
Abstract Method interfaces).

Segue un esempio di alcune interfacce funzionali del JDK (Snippet 10.14).


Snippet 10.14 Alcune interfacce funzionali.

// si trova nel package java.lang


public interface Runnable
{
public abstract void run();
}

// si trova nel package java.awt.event


public interface ActionListener extends EventListener
{
public void actionPerformed(ActionEvent e);
}

// si trova nel package java.io


public interface FileFilter
{
boolean accept(File pathname);
}

// si trova nel package java.util.function


public interface Function<T, R>
{
R apply(T t);
...
}

// si trova nel package java.util.function


public interface Predicate<T>
{
boolean test(T t);
...
}

// si trova nel package java.util.function


public interface Supplier<T>
{
T get();
}

Metodi di default
Un metodo di default un metodo di uninterfaccia che ha unimplementazione di
default (Sintassi 10.2), ovvero ha un body che fornisce delle funzionalit predefinite
che sono automaticamente utilizzabili da quelle classi che implementano tale
interfaccia. Chiaramente, le classi che implementano uninterfaccia possono decidere
di avvalersi dei metodi di default ereditati oppure possono decidere di sovrascriverli
fornendo una propria implementazione.
TERMINOLOGIA
I metodi di default erano precedentemente conosciuti con il nome di virtual extension methods o
defender methods.
Sintassi 10.2 Metodo di default.

[modifiers] interface MyInterface


{
default void foo() { /* implementazione di default */ }
...
}

La Sintassi 10.2 evidenzia che un metodo di default si crea utilizzando il


modificatore default e fornendo un body con unimplementazione.

Segue un esempio di alcuni metodi di default cos come appaiono nelle relative
interfacce del JDK (Snippet 10.15).
Snippet 10.15 Alcune interfacce con dei metodi di default.

public interface Iterable<T>


{
Iterator<T> iterator();
default void forEach(Consumer<? super T> action)
{
Objects.requireNonNull(action);
for (T t : this) { action.accept(t); }
}
...
}

public interface Iterator<E>


{

boolean hasNext();
E next();
...
default void forEachRemaining(Consumer<? super E> action)
{
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}

public interface List<E> extends Collection<E>


{
int size();
...
default void replaceAll(UnaryOperator<E> operator)
{
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) { li.set(operator.apply(li.next())); }
}
}

Quando si definiscono dei metodi di default bisogna rammentare le seguenti


regole: le istruzioni ivi contenute possono utilizzare i parametri formali, gli altri
metodi definiti e/o dichiarati nella stessa interfaccia e le eventuali costanti definite
(ricordiamo che nelle interfacce non si possono definire le consuete variabili
definibili allinterno di una classe); sono implicitamente public (non possono essere
protected o private); non possono essere abstract; non possono essere static; non
possono essere final.

Metodi di interfaccia statici


Un metodo di interfaccia statico un metodo definito allinterno di uninterfaccia
con il modificatore static (Sintassi 10.3) e con un body con del codice di
implementazione.
Sintassi 10.3 Metodo di interfaccia statico.

[modifiers] interface MyInterface


{
static void bar() { /* implementazione */ }
...
}

Un metodo di interfaccia static si utilizza allo stesso modo di un membro di classe


statico, ovvero scrivendo lidentificatore dellinterfaccia, il carattere punto (.) e
lidentificatore del metodo.
Tuttavia, tra un metodo di interfaccia static e un metodo di classe static esistono
delle importanti differenze, come evidenziato nella Tabella 10.1.

Tabella 10.1 Differenze dei metodi static tra uninterfaccia e una classe.
Tipi Invocati con un riferimento Invocati con il nome Si ereditano
classe S S S
interfaccia No S No

Le stesse differenze sono mostrate negli Snippet 10.16 e 10.17.


Snippet 10.16 Invocazione di un metodo statico.

interface MyInterface
{
static void foo(){}
}

class MyClass implements MyInterface


{
public static void foo(){}
}
...
MyInterface my_int = new MyClass();
MyClass my_class = new MyClass();

// error: illegal static interface method call my_int.foo();


// the receiver expression should be replaced with
// the type qualifier 'MyInterface' 1 error
my_int.foo();
MyInterface.foo(); // ok

my_class.foo(); // ok
MyClass.foo(); // ok

Snippet 10.17 Ereditariet di un metodo statico.

interface Interface_1
{
static void foo() {}
}
interface Interface_2 extends Interface_1 {}

class Class_1
{
public static void foo() {}
}
class Class_2 extends Class_1 {}
...
Interface_1.foo(); // ok
// error: cannot find symbol Interface_2.foo();
// symbol: method foo()
// location: interface Interface_2
// 1 error
Interface_2.foo();

Class_1.foo(); // ok
Class_2.foo(); // ok

Infine, quando si definiscono dei metodi di interfaccia static bisogna ricordare le


seguenti regole: possono accedere solamente ad altri membri statici e mai dunque a
metodi astratti o a metodi di default; non possono utilizzare i parametri di tipo (se,
per esempio, uninterfaccia definita come interface I<T> {}, allora un metodo statico
non potr utilizzare la variabile di tipo T); non possono avere i modificatori protected o
private; non possono essere abstract; non possono essere final.

Ereditariet multipla e problemi di ambiguit


La possibilit di definire dei metodi di default per le interfacce ha portato anche
alla nascita di problemi relativi a come risolvere correttamente le eventuali ambiguit
e conflitti che potrebbero esserci allorquando un tipo eredita da pi tipi dei metodi
con la stessa segnatura.
In pratica, il compilatore necessita di avere delle regole grazie alle quali, in caso di
ambiguit e conflitti, in grado di decidere quali metodi di default utilizzare ovvero
quali implementazioni preferire.
Possiamo, a tal fine, avere i seguenti principali casi pratici.
a. Una classe eredita un metodo con la stessa segnatura definito in una classe che
estende e in uninterfaccia che implementa. Problema: il metodo di quale dei due tipi
la classe dovr ereditare? Regola risolutiva: il metodo della classe estesa quello
prescelto. In pratica, una definizione di un metodo di una superclasse sempre
preferita rispetto a una definizione di un metodo di default di uninterfaccia
implementata (Snippet 10.18).
Snippet 10.18 Ereditariet multipla: caso a.
interface Interface_1
{
default void foo() { System.out.println("Interface_1"); }
}

class Class_1
{
public void foo() { System.out.println("Class_1"); }
}

class Class_2 extends Class_1 implements Interface_1 {}


...

new Class_2().foo(); // Class_1


b. Una classe eredita un metodo con la stessa segnatura definito in uninterfaccia
che implementa la quale, a sua volta, estende unaltra interfaccia che, a sua volta,
estende unaltra interfaccia. Problema: il metodo di quale dei tre tipi la classe dovr
ereditare? Regola risolutiva: il metodo pi specifico, rispetto alla catena di
ereditariet delle interfacce, quello prescelto (Snippet 10.19).
Snippet 10.19 Ereditariet multipla: caso b.

interface Interface_1
{
default void foo() { System.out.println("Interface_1"); }
}

interface Interface_2 extends Interface_1


{
default void foo() { System.out.println("Interface_2"); }
}

interface Interface_3 extends Interface_2


{
default void foo() { System.out.println("Interface_3"); }
}

// qui avremmo potuto scrivere semplicementeimplements Interface_3 ma abbiamo


// implementato tutte e tre le interfacce in modo non "ordinato" per evidenziare
// che il metodo ereditato non quello della "prima" interfaccia implementata
class Class_1 implements Interface_2, Interface_3, Interface_1 {}
...

new Class_1().foo(); // Interface_3

c. Una classe eredita un metodo con la stessa segnatura definito in due interfacce
che implementa e che sono tra di esse non collegate da alcun rapporto di ereditariet.
Problema: il metodo di quale delle due interfacce la classe dovr ereditare? Regola
risolutiva: la classe deve fare loverriding esplicito del metodo e scrivere nel corpo di
definizione, utilizzando la keyword super (Sintassi 10.4), quale metodo di quale delle
due interfacce intende invocare. In pratica, in questultimo caso, il compilatore non
in grado in autonomia di risolvere il conflitto, e pertanto il programmatore deve
decidere manualmente quale metodo utilizzare (Snippet 10.20).
Sintassi 10.4 Uso di super per esplicitare il metodo di default di uninterfaccia.
MyInterface.super.defaultMethod();

Snippet 10.20 Ereditariet multipla: caso c.


interface Interface_1
{
default void foo() { System.out.println("Interface_1"); }
}

interface Interface_2
{
default void foo() { System.out.println("Interface_2"); }
}

// error: class Class_1 inherits unrelated defaults for foo()


// from types Interface_1 and Interface_2
// class Class_1 implements Interface_1, Interface_2 {}

class Class_1 implements Interface_1, Interface_2


{
public void foo() { Interface_1.super.foo(); }
}
...

new Class_1().foo(); // Interface_1

IL PROBLEMA DEL DIAMANTE


Lereditariet multipla, talune volte, pu causare un noto problema di ambiguit conosciuto in
letteratura con il nome di problema del diamante (diamond problem o deadly diamond of death).
Esso cos chiamato per la forma, appunto a diamante, mostrata da un diagramma di ereditariet
nel caso si abbia una classe X estesa da due classi Y e Z le quali sono, a loro volta, estese da una
classe W (Figura 10.3). Se poi la classe X definisce un metodo foo sovrascritto dalle classi Y e Z, che
W non sovrascrive a sua volta, quale metodo foo W deve ereditare? Quello definito in Y o quello
definito in Z? Per la risoluzione di questo problema ogni linguaggio di programmazione che
supporta lereditariet multipla adotta una propria strategia; per quanto riguarda Java, dove
rammentiamo che lereditariet multipla non mai consentita per le classi ma solo per le interfacce
(si parla in questo caso di behaviour inheritance), tale problema di ambiguit risolvibile mediante
lapproccio visto nel caso c.

Figura 10.3 Diagramma del problema del diamante.

Lambda expression
Una lambda expression (Sintassi 10.5), dal punto di vista di Java, assimilabile a
una sorta di metodo scritto senza nome ( dunque anonimo), senza un tipo di ritorno,
senza uneventuale clausola throws e con una sintassi compatta e concisa. Per quanto
riguarda il tipo di ritorno e la clausola throws, entrambe sono sempre inferite dal
compilatore.
Sintassi 10.5 Sintassi di una lambda expression.
parameter_list -> expression | block
La Sintassi 10.5 evidenzia che in una lambda expression abbiamo una lista di
parametri, il simbolo freccia -> e unespressione oppure un blocco di codice. Di
questi, la lista di parametri indicabile attraverso parameter_list esprime, per lappunto,
uno o pi parametri formali separati dal carattere virgola (,) del metodo anonimo, i
cui tipi possono essere omessi in quanto, se desumibili dal contesto valutativo
corrente, sono automaticamente inferiti dal compilatore. In pi, bisogna considerare
che non possibile mixare parametri con un tipo con parametri senza tipo, e che
possibile omettere le parentesi tonde ( ) se si scrive un solo parametro senza
indicarne il tipo.
Per quanto riguarda invece expression, essa indica una qualunque espressione
semplice scritta senza le parentesi graffe { } di delimitazione di un blocco e senza
uneventuale keyword return che, nel caso, implicitamente utilizzata; block, infine,
indica un comune blocco di codice delimitato dalle parentesi graffe al cui interno vi
possono essere una o pi statement terminate dal punto e virgola (;) ed
eventualmente la keyword return. Chiaramente, a seconda della complessit del body
della lambda expression, decideremo se utilizzare unespressione semplice oppure un
vero e proprio blocco di codice.
Lo Snippet 10.21 mostra una serie di lambda expression che evidenziano dei modi
corretti e scorretti di definirle.
Snippet 10.21 Una serie di lambda expression.

// OK parentesi tonde omettibili - un solo parametro senza tipo


ToIntFunction<Integer> func_1 = z -> z * 10;

// ERRORE - parentesi tonde non omettibili - pi di un parametro


ToIntBiFunction<Integer, Integer> func_2 = z, w -> z * w;

// OK - pi parametri indicati tra parentesi tonde


ToIntBiFunction<Integer, Integer> func_3 = (x, y) -> x - y;

// ERRORE - non possibile mixare parametri con tipo con parametri senza tipo
BiPredicate<String, String> func_4 = (String name, surname) -> name.equals(surname);

// ERRORE - non possibile omettere le parentesi tonde se vi un solo parametro


// con l'indicazione del tipo
Consumer<Integer> func_5 = Integer j -> System.out.println(j);

// ERRORE - return pu solo apparire come statement in un blocco di codice {}


Function<Double, Double> func_6 = (Double d) -> return d - 10;

// OK - return scritto nell'ambito di un blocco di codice


Function<Double, Double> func_7 = (Double d) ->
{
if (d > 10)
return d - 10;
else
return d;
};

// ERRORE - in un blocco necessario scrivere return se la funzione lo richiede


// cos come il punto e virgola
IntFunction<Integer> func_8 = j -> { j * j * j };
SUGGERIMENTO
Guardando a tutte le lambda expression scritte pu sorgere spontanea una domanda: come si fa
a comprendere quale lambda expression utilizzare in un determinato contesto valutativo? In
questa fase didattica preliminare possiamo rispondere cos: si legge la segnatura del metodo
dellinterfaccia funzionale target e si verifica se la stessa corrisponde a quella indicata nella
lambda expression che si desidera impiegare e, nel caso di compatibilit, si scrive la medesima
lambda expression.

Regole di scope rivisitate


Uno scope, ribadiamo nuovamente, una sezione di un programma al cui interno
un identificatore visibile senza alcuna particolare qualificazione. In Java le classi e i
metodi hanno un proprio scope laddove i nomi l dichiarati nascondono (variable
shadowing) gli stessi nomi eventualmente dichiarati negli scope che racchiudono tali
classi e metodi. Per quanto riguarda, invece, le lambda expression abbiamo le
seguenti regole: se sono annidate in una classe allora i nomi l dichiarati nascondono
gli stessi nomi dichiarati nella predetta classe (hanno un proprio scope); se sono
annidate in un metodo allora i nomi l indicati sono riferibili (variable binding) agli
stessi nomi dichiarati nel metodo predetto o, in assenza, nella relativa classe di
appartenenza (non hanno un proprio scope, sono parte dello scope dove appaiono). In
questultimo caso, tuttavia, e al pari dei comuni metodi, i nomi dei parametri formali
nascondono gli stessi nomi delle variabili dichiarate nella classe di appartenenza.
IMPORTANTE
Quando una lambda expression si riferisce a una variabile locale di un metodo dove stata
definita, allora tale variabile deve essere esplicitamente dichiarata come final oppure deve
essere effettivamente final, ovvero possibile omettere il qualificatore final a condizione che
tale variabile non subisca delle modifiche. Da Java 8, inoltre, anche per le classi locali annidate
(anonime e non) diventato facoltativo lutilizzo della keyword final per lutilizzo senza modifica
delle variabili locali dellenclosing scope. Tale restrizione non opera, comunque, quando il binding
verso un campo di una classe dove stata definita la lambda expression (per esempio verso
una variabile distanza o verso una variabile di classe).
Listato 10.3 Classe ScopingRules.

package com.pellegrinoprincipe;

import java.util.function.IntPredicate;

class ClassA // PROPRIO SCOPE - OUTER SCOPE


{
public static final int NUMBER = 10;
}

class ClassB // PROPRIO SCOPE - OUTER SCOPE


{
public static final int NUMBER = 100;
}

class ClassC // PROPRIO SCOPE - OUTER SCOPE


{
public static final int SIZE = 1000;
public static class ClassD // PROPRIO SCOPE - INNER SCOPE
{
// OK - nasconde SIZE di ClassC
public static final int SIZE = 2000;
}
}

class ClassE // PROPRIO SCOPE - OUTER SCOPE


{
private int width = 55;
private String prefix = "###";

public void setWidth(int width) // PROPRIO SCOPE - INNER SCOPE


{
// qui usato il parametro width che nasconde la variabile d'istanza width
int w = width;

// qui usata la variabile d'istanza width tramite l'uso di this


int w2 = this.width;

// prefix di ClassE nascosta dalla variabile locale prefix


String prefix = ";;;";
System.out.println("In setWidth prefix : " + prefix);
}
}

class ClassF // PROPRIO SCOPE - OUTER SCOPE


{
private int flag = -1;
private int value = 100;

// LAMBDA EXPRESSION ANNIDATA NELLA CLASSE


private IntPredicate i_pred = (int flag) -> // PROPRIO SCOPE - INNER SCOPE
{
// il parametro flag nasconde la variabile d'istanza flag
int _f = flag;

// la variabile locale value nasconde la variabile d'istanza value


int value = 200;
return value == _f;
};

public boolean test(int f)


{
return i_pred.test(f);
}
}

class ClassG // PROPRIO SCOPE - OUTER SCOPE


{
private int flag = 100;
private int value = 200;
private int counter = 400;

public void test(int flag) // PROPRIO SCOPE - INNER SCOPE


{
int inc = 10;

// CLASSE ANNIDATA
IntPredicate p1 = new IntPredicate() // PROPRIO SCOPE
{
public boolean test(int value)
{
// inc nasconde la variabile locale inc del metodo test
// se avessimo invece scritto qualcosa come: inc = 33
// avremmo avuto il seguente errore:
// local variables referenced from an inner class
// must be final or effectively final
int inc = 33;

// qui value nasconde la variabile d'istanza value


return value + inc == flag;
}
};
System.out.println("Test del predicato p1 = " + p1.test(flag));
// LAMBDA EXPRESSION
IntPredicate p2 = value -> // NO SCOPE PROPRIO!!!
{
// inc non pu essere dichiarata perch gi dichiarata nel metodo test
// int inc = 33;
// se avessimo inoltre scritto qualcosa come: inc = 33
// avremmo avuto il seguente errore:
// local variables referenced from a lambda expression
// must be final or effectively final

// OK - la restrizione final non opera per i campi di una classe
counter += 200;

// qui value nasconde la variabile d'istanza value
return value + counter == flag;
};
System.out.println("Test del predicato p2 = " + p2.test(flag));
}
}

public class ScopingRules


{
public static void main(String args[])
{
// OK - lo stesso nome NUMBER pu apparire in scope differenti.
// Si riferisce a entit distinte.
// Qui lo riferiamo qualificandolo con la classe di appartenenza
System.out.println("ClassA.NUMBER = " + ClassA.NUMBER + "; ClassB.NUMBER = "
+ ClassB.NUMBER);

System.out.println("ClassC.SIZE = " + ClassC.SIZE + "; ClassD.SIZE = "


+ ClassC.ClassD.SIZE);

new ClassE().setWidth(300);

System.out.println("Valore del test grazie a i_pred = " + new ClassF().test(100));



new ClassG().test(100);
}
}

Output 10.3 Dal Listato 10.3 Classe ScopingRules.


ClassA.NUMBER = 10; ClassB.NUMBER = 100
ClassC.SIZE = 1000; ClassD.SIZE = 2000
In setWidth prefix : ;;;
Valore del test grazie a i_pred = false
Test del predicato p1 = false
Test del predicato p2 = false

Le keyword this e super


Nellambito di una lambda expression lutilizzo delle keyword this e super si
riferiscono, rispettivamente, allistanza della classe che la racchiude e alla classe da
cui tale classe deriva. Quanto affermato in chiaro contrasto con quanto accade
nellambito di una classe annidata locale dove la keyword this si riferisce allistanza
della classe stessa mentre la keyword super alla sua superclasse (Listato 10.4).
Listato 10.4 Classe ThisAndSuper.

package com.pellegrinoprincipe;

import java.util.Random;
import java.util.function.DoubleConsumer;
import java.util.function.DoubleSupplier;

class Math // operazioni comuni


{
public String toString() { return "[Math]"; }
}

class Operations extends Math


{
private double result;
private double data;

public double get(int min, int max)


{
DoubleSupplier d_s = () ->
{
System.out.println("Riferimento this in d_s dal metodo get: " + this
+ "\nRiferimento super in d_s dal metodo get: "
+ super.toString());
Random r = new Random();
Double d = r.nextDouble();
result = min + (max - min) * d;
return result;
};
return d_s.getAsDouble();
}

public void set(int min, int max)


{
// esempio "forzato" ma utile per spiegare il comportamento di this e super
class Consumer
{
public String toString() { return "[Consumer]"; }
}
class MyDoubleConsumer extends Consumer implements DoubleConsumer
{
public void accept(double value)
{
System.out.println("Riferimento this in m_d_c dal metodo set: " + this
+ "\nRiferimento super in m_d_c dal metodo set: "
+ super.toString());
}

public String toString() { return "[MyDoubleConsumer]"; }


}
MyDoubleConsumer m_d_c = new MyDoubleConsumer();
m_d_c.accept(min / max);
}
public String toString() { return "[Operations]"; }
}

public class ThisAndSuper


{
public static void main(String args[])
{
Operations op = new Operations();
op.get(1, 10);
op.set(100, 2000);
}
}

Output 10.4 Dal Listato 10.4 Classe ThisAndSuper.


Riferimento this in d_s dal metodo get: [Operations]
Riferimento super in d_s dal metodo get: [Math]
Riferimento this in m_d_c dal metodo set: [MyDoubleConsumer]
Riferimento super in m_d_c dal metodo set: [Consumer]

Nellambito del metodo accept della classe MyDoubleConsumer utile ricordare che per
accedere alla classe contenitrice Operations dobbiamo utilizzare listruzione
Operations.this, mentre per accedere alla superclasse della classe contenitrice Operations
dobbiamo utilizzare listruzione Operations.super.
NOTA
Per quanto concerne le keyword break, continue, return e throw esse producono il relativo effetto
nellambito della lambda expression dove sono utilizzate e mai, dunque, nellenclosing context
della lambda stessa. Ci significa, per esempio, che: unistruzione di return scritta nellambito di
una lambda expression ritorner un valore da tale lambda e non dal metodo che la racchiude;
unistruzione di break sar permessa solo nellambito di un loop o di una statement switch scritti
in una lambda expression e non potr mai essere usata per controllare il flusso del metodo che la
racchiude (in Java i cosiddetti non-local jumps non sono supportati).

Riferimenti a metodi
I riferimenti a metodi sono qualificabili come degli handle a dei metodi esistenti
nellambito di determinate classi che possono essere utilizzati direttamente per
fornire delle funzionalit richieste e in sostituzione di lambda expression
equivalenti. In effetti, i riferimenti a metodi, come vedremo tra breve, altro non sono
che degli shorthand verso determinate lambda expression. Questi riferimenti possono
essere handle a metodi statici di una classe (Sintassi 10.6), handle a metodi distanza
di un determinato oggetto (Sintassi 10.7) e handle a metodi distanza di un oggetto
arbitrario di un tipo specifico (Sintassi 10.8).
Sintassi 10.6 Riferimento a un metodo statico.

TypeName::staticMethodName

Sintassi 10.7 Riferimento a un metodo distanza di un determinato oggetto.

instance_reference::instanceMethodName

Sintassi 10.8 Riferimento a un metodo distanza di un oggetto arbitrario di un tipo specifico.

TypeName::instanceMethodName

Nella sostanza, per utilizzare un riferimento a un metodo dovremo scrivere il nome


di una classe o di unistanza, il carattere separatore :: e il nome di tale metodo che
pu essere statico o distanza.
Listato 10.5 Classe RiferimentiAMetodi.
package com.pellegrinoprincipe;

import java.util.function.IntBinaryOperator;

class MyInteger
{
public int sum(int left, int right)
{
return left + right;
}
}

@FunctionalInterface
interface MyIntBinaryOperator
{
int applyAsInt(MyInteger m, int a, int b);
}
public class RiferimentiAMetodi
{
public static void main(String[] args)
{
// I MODALITA': old-style prima di Java 8
IntBinaryOperator bo_1 = new IntBinaryOperator()
{
public int applyAsInt(int left, int right)
{
return left + right;
}
};
System.out.println("Risultato bo_1: " + bo_1.applyAsInt(10, 10));

// II MODALITA': Java 8 - lambda expression


IntBinaryOperator bo_2 = (left, right) -> left + right;
System.out.println("Risultato bo_2: " + bo_2.applyAsInt(10, 10));

// III MODALITA': Java 8 - Riferimento a un metodo statico


IntBinaryOperator bo_3 = Integer::sum;
System.out.println("Risultato bo_3: " + bo_3.applyAsInt(10, 10));

// IV MODALITA': Java 8 - Riferimento a un metodo


// d'istanza di un oggetto specifico
MyInteger mi = new MyInteger();
IntBinaryOperator bo_4 = mi::sum;
System.out.println("Risultato bo_4: " + bo_4.applyAsInt(10, 10));

// V MODALITA': Java 8 - Riferimento a un metodo d'istanza


// di un oggetto arbitrario di un tipo specifico
MyIntBinaryOperator bo_5 = MyInteger::sum;
System.out.println("Risultato bo_5: " + bo_5.applyAsInt(new MyInteger(), 10, 10));
}
}

Output 10.5 Dal Listato 10.5 Classe RiferimentiAMetodi.

Risultato bo_1: 20
Risultato bo_2: 20
Risultato bo_3: 20
Risultato bo_4: 20
Risultato bo_5: 20

Il Listato 10.5 consente di analizzare le diverse modalit di utilizzo di una


funzionalit che ha come obiettivo quello di ritornare (output) la somma di due
numeri interi forniti come argomenti (input). A tal fine linterfaccia funzionale
impiegata IntBinaryOperator, che rappresenta unoperazione su due operandi di tipo
int atta a produrre come risultato un altro valore sempre di tipo int. Il suo metodo
funzionale dichiarato come int applyAsInt(int left, int right) e ha il seguente
function type: (int, int) -> int.
TERMINOLOGIA
Il function type, conosciuto precedentemente con il nome di function descriptor, un descrittore
di un metodo che non tiene in considerazione il suo nome e il suo body. Esso consta dei
parametri di tipo, dei tipi dei parametri formali, del tipo di ritorno e dei tipi indicati dalla clausola
throws. In definitiva pu essere pensato come il tipo di un metodo.

La prima modalit di utilizzo impiega la vecchia e prolissa sintassi che prevede


la creazione di una classe anonima a partire da uninterfaccia; in questo caso,
esplicitiamo la definizione del metodo applyAsInt nel consueto e abituale modo.
La seconda modalit di utilizzo impiega una lambda expression, scritta con la
sintassi compatta e concisa gi analizzata, e senza lindicazione dei tipi dei parametri
che sono automaticamente inferiti dal compilatore.
La terza modalit impiega un riferimento al metodo statico sum della classe Integer.
Ci perfettamente lecito perch il descrittore del metodo sum (int, int) -> int
ovvero corrispondente a quello del metodo applyAsInt (prende come argomenti due
e ritorna un int). In pratica tale riferimento uno shorthand per la lambda
int

expression: (left, right) -> Integer.sum(left, right), dove il compilatore ha copiato i


parametri formali dal function type del metodo funzionale applyAsInt e ha posto nel
body della lambda linvocazione del metodo statico sum passandogli quei parametri.

La quarta modalit impiega un riferimento al metodo distanza sum delloggetto mi


di tipo MyInteger. Anche in questo caso lassegnamento allinterfaccia funzionale bo_4
lecito e ci perch il function type di sum (int, int) -> int ossia sempre uguale a
quello di applyAsInt. Tale riferimento uno shorthand per la lambda expression: (left,
, dove il compilatore ha copiato i parametri formali dal
right) -> mi.sum(left, right)

function type del metodo applyAsInt e ha posto nel body della lambda linvocazione
del metodo distanza sum passandogli quei parametri. In questo modalit, per, e a
differenza della terza, il metodo stato invocato su un riferimento che unistanza
(lidentificatore mi catturato) e non, quindi, mediante il nome del tipo.

La quinta e ultima modalit impiega un riferimento al metodo distanza sum del tipo
specifico MyInteger. Per comprenderne la fattibilit dobbiamo fare una breve
digressione concettuale e introdurre il concetto di receiver. Per receiver intendiamo
loggetto destinazione con il quale il metodo distanza invocato. Possiamo avere un
bound receiver e allora loggetto destinazione esplicitamente utilizzato ( il caso
della quarta modalit), oppure un unbound receiver, e allora loggetto destinazione
implicitamente fornito dal compilatore come primo parametro di un metodo quando
genera la lambda expression equivalente. Nel nostro caso, dunque, il descrittore del
metodo applyAsInt dellinterfaccia funzionale IntBinaryOperator non concorderebbe con
quello fornito dallespressione MyInteger::sum perch linterfaccia funzionale di
destinazione dovrebbe avere un metodo funzionale con un descrittore come
(MyInteger, int, int) -> int, visto che il primo parametro implicitamente fornito dal

compilatore. Ecco, quindi, che abbiamo definito uninterfaccia funzionale custom


denominata MyIntBinaryOperator che ha il metodo applyAsInt con il descrittore (MyInteger,
che combacia perfettamente con quanto indicato da MyInteger::sum in
int, int) -> int
quel contesto valutativo. Il riferimento MyInteger::sum , dunque, uno shorthand per la
lambda expression: (m, a, b) -> m.sum(a, b), dove il compilatore ha copiato i parametri
formali dal function type del metodo applyAsInt (questa volta dellinterfaccia
MyIntBinaryOperator ) e ha posto nel body della lambda linvocazione del metodo
distanza sum passandogli quei parametri di cui il primo servito come receiver per sum
stesso.
Per quanto concerne gli unbound receiver appare opportuno fare un altro esempio
(Snippet 10.22) per meglio fissare questimportante concetto.
Snippet 10.22 Unbound receiver.

// lambda equivalente: (String s) -> s.toLowerCase()


Function<String, String> f_tol = String::toLowerCase;
f_tol.apply("PELLEGRINO"); // pellegrino

Nello Snippet 10.22 utilizziamo linterfaccia funzionale Function<String, String> che


rappresenta una funzione che accetta un argomento di tipo String e produce un
risultato di tipo String. Il suo metodo funzionale, in questo contesto valutativo, String
apply(String t) e ha il descrittore (String) -> (String). Al riferimento f_tol di tipo
Function<String, String> passiamo il metodo distanza toLowerCase della classe String;
nonostante abbia come descrittore naturale () -> String (leggendo la relativa API), nel
momento dellassegnamento, il suo descrittore (String) -> (String) perch il primo
parametro stato implicitamente fornito dal compilatore e rappresenta il receiver con
il quale invocare il metodo toLowerCase. Ecco perch lassegnamento stato accettato
dal compilatore senza alcun problema.

RIFERIMENTI A COSTRUTTORI
Cos come possibile utilizzare in sostituzione delle lambda expression dei riferimenti a metodi gi
presenti nei tipi altres possibile impiegare dei riferimenti ai relativi costruttori. In questo caso,
per, la sintassi da utilizzare prevede che, al posto dellidentificatore del metodo, si scriva la
keyword new. In pi, il tipo da impiegare pu essere, per lappunto, il nome di un tipo cos come
visto per i riferimenti a metodi statici oppure un tipo array. Per esempio, String::new potrebbe
essere equivalente alla lambda expression () -> new String(), mentre int[]::new equivalente
alla lambda expression (int size) -> new int[size]. comunque importante rammentare che
quando vi sono pi costruttori in overloading il compilatore sceglie quello corretto da utilizzare
inferendolo dal corrente contesto di valutazione.

Target typing
Per target type (tipo di destinazione) intendiamo il tipo atteso o previsto in un
determinato contesto di valutazione, che deve essere compatibile con il tipo inferito o
dedotto (deduced type) dal compilatore nellambito di unespressione (in pratica il
tipo dedotto deve essere uguale o convertibile nel tipo di destinazione).
In linea generale vi sono i seguenti tipi di espressioni: quelle definite standalone
(Snippet 10.23), il cui tipo determinabile in isolamento dal contenuto
dellespressione stessa, e quelle definite poly (Snippet 10.24), il cui tipo dipendente
dal contesto (poly context) dove lespressione stata definita (una poly expression
pu dunque avere tipi differenti in contesti differenti; il tipo dedotto influenzato dal
tipo di destinazione). Per quanto concerne, infine, i contesti valutativi di una poly
expression essi possono essere: quelli di assegnamento (assignment context), dove la
poly expression appare alla destra delloperatore di assegnamento e il target type
appare alla sinistra del medesimo operatore; quelli di invocazione (invocation
context), dove la poly expression appare come parametro attuale in uninvocazione di
un metodo o di un costruttore e il target type il tipo dichiarato dal corrispondente
parametro formale; quelli di conversione esplicita (casting context), dove la poly
expression appare dopo le parentesi tonde ( ) del cast e il target type appare tra le
medesime parentesi.
NOTA
Quando una poly expression scritta dopo la keyword return nellambito di un body di un
metodo possiamo dire che il suo target type quello dichiarato come tipo di ritorno nella
definizione del metodo stesso.
Snippet 10.23 Standalone expression.

// qui new HashSet<String>() un'espressione standalone


// anche in isolamento, ovvero senza l'assegnamento al riferimento
// s il compilatore pu inferire che il tipo HashSet<String>
Set<String> s = new HashSet<String>();

// qui "Pellegrino" un'espressione standalone


// il compilatore valutando il valore costante pu senza dubbio dire che di tipo String
String name = "Pellegrino";

Snippet 10.24 Poly expression.

// creazione di istanze con il diamond operator: poly expression


// new HashSet<>() appare in due contesti differenti e il tipo dedotto
// a parit di espressione differente
Set<String> s_s = new HashSet<>();
Set<Double> s_d = new HashSet<>();

// operatore ternario: poly expression


// with_capacity ? new HashSet<>(capacity) : new HashSet<>()
// appare in due contesti differenti e il tipo dedotto
// a parit di espressione differente
boolean with_capacity = true;
final int capacity = 10;
Set<String> s_s2 = with_capacity ? new HashSet<>(capacity) : new HashSet<>();
Set<Double> s_d2 = with_capacity ? new HashSet<>(capacity) : new HashSet<>();

// lambda expression: poly expression


// () -> 10 appare in due contesti differenti e il tipo dedotto
// a parit di espressione differente
IntSupplier is = () -> 10;
DoubleSupplier ds = () -> 10;
// riferimenti a metodi: poly expression
// String::valueOf appare in due contesti differenti e il tipo dedotto
// a parit di espressione differente
IntFunction ti = String::valueOf;
Function<Integer, String> func = String::valueOf;

In definitiva, per quanto riguarda le lambda expression e il riferimento a metodi o a


costruttori, il target type deve essere uninterfaccia funzionale laddove il suo metodo
funzionale deve avere un function type compatibile con quello indicato dalle predette
lambda o riferimenti. Chiaramente, essendo le lambda expression e i riferimenti a
metodi o a costruttori delle poly expression, il lambda object verso cui possono
essere convertite pu variare, a parit di stessa lambda o riferimento, a seconda del
contesto di valutazione corrente e del target type atteso.
Capitolo 11
Errori software

Un errore software, o bug, unanomalia funzionale che si pu presentare durante


lesecuzione di un programma e che, nei moderni linguaggi di programmazione,
possibile intercettare e gestire adeguatamente grazie allutilizzo di un meccanismo
definito gestione delle eccezioni.
In pratica, principalmente, grazie alla gestione delle eccezioni si pu evitare che un
programma termini bruscamente in presenza di un errore software, perch si pu
cercare di eliminarlo dopo averlo intercettato (ovviamente ci possibile per gli
errori che siamo in grado di prevedere); inoltre, si pu altres beneficiare di unaltra
importante caratteristica che strettamente collegata alla fase di progettazione dei
programmi. Infatti, tale meccanismo, come analizzeremo dettagliatamente in seguito,
permette di separare, in modo logico ed efficiente, il codice di gestione delle
eccezioni (implementato in apposite classi) dal codice di un programma
(implementato nelle relative classi che ne rappresentano lapplicazione) in modo che
lo stesso non contenga pi al suo interno anche le eventuali e innumerevoli istruzioni
di selezione che potrebbero occorrere per intercettare e gestire tali eccezioni o errori
(per esempio if(denominator == 0) return -1, if(flag == false) System.out.println("Attenzione
flag non impostato") e cos via).

BUG: UN TERMINE CHE VIENE DA LONTANO


Il termine errore software o bug, che indica la causa di un malfunzionamento di un apparato
meccanico o elettronico, stato utilizzato almeno dal XIX secolo; infatti se ne trovano tracce negli
scritti di molti inventori e scienziati dellepoca. Emblematica lannotazione di possibili errori di
operativit che Ada Lovelace, matematica inglese vissuta nel 1800, scrisse nei suoi appunti sullo
studio della macchina analitica di Charles Babbage. Secondo altre fonti storiche, invece, luso del
termine bug in unaccezione moderna si deve far risalire al 1947, quando il computer Harvard
Mark II, costruito alluniversit di Harvard, smise allimprovviso di funzionare e i tecnici scoprirono
che la causa del malfunzionamento era da imputare a un insetto (bug, appunto) che si era
incastrato tra i relais del computer.
La keyword assert
Durante la scrittura del codice, generalmente durante la fase di sviluppo, ci si pu
avvalere ai fini di debugging della keyword assert, che permette di verificare se una
data espressione vera o falsa (Sintassi 11.1). Se la valutazione dellespressione
risulter falsa, il programma verr interrotto e sar sollevata uneccezione di tipo
AssertionError.

Sintassi 11.1 Sintassi assert.

assert expression;

Sintassi 11.2 Sintassi assert condizionale.

assert expression : expression2;

La Sintassi 11.2, rispetto alla Sintassi 11.1, consente di costruire unasserzione


indicando, oltre a expression, anche unaltra espressione (expression2) che sar
utilizzata come argomento del costruttore della classe AssertionError, se la prima
espressione ritorner il valore false, per visualizzare un messaggio che indicher il
motivo del fallimento dellasserzione medesima.
Listato 11.1 Classe Assertion.
package com.pellegrinoprincipe;

public class Assertion


{
public void greaterThan(int nr, int comp) // metodo che valuta una condizione
{
// asserzione
assert nr > comp : "Attenzione " + nr + " non maggiore di " + comp;
}

public static void main(String[] args)


{
int n = 100, c = 300;
new Assertion().greaterThan(n, c);
}
}

Il Listato 11.1 definisce il metodo greaterThan che verifica semplicemente se un dato


numero (nr) maggiore di un valore sentinella (comp); se non lo viene sollevata
uneccezione di tipo AssertionError che visualizza il messaggio: "Attenzione " + nr + "
non maggiore di " + comp .

Per verificare se il nostro programma funziona correttamente dobbiamo invocare


linterprete con lopzione -ea, poich il meccanismo di verifica delle asserzioni non
abilitato in automatico.
Shell 11.1 Comando per labilitazione delle asserzioni.

java -ea com.pellegrinoprincipe.Assertion


Output 11.1 Dal Listato 11.1 Classe Assertion.

Exception in thread "main" java.lang.AssertionError: Attenzione 100 non maggiore di 300


Le eccezioni
Le eccezioni sono gestite con lutilizzo delle keyword.

try/catch : la clausola try consente di specificare un blocco di codice al cui interno


inserire le istruzioni che potrebbero generare uneccezione, mentre la clausola
catch permette di indicare i tipi di eccezione da gestire, cos come di scrivere un

blocco di codice al cui interno inserire istruzioni che si occuperanno di come


gestire leventuale eccezione occorsa.

Sintassi 11.3 Sintassi try/catch.

try { ... } // istruzioni che potrebbero generare un'eccezione


catch (ExceptionType1 | ExceptionType2 | ... j) { ... } // gestore delle eccezioni

Analizzando la Sintassi 11.3 possibile notare come i tipi di eccezione gestibili


sono indicati nella clausola catch, ciascuno separato dal simbolo del pipe (|). La
capacit di indicare pi di uneccezione nellambito dello stesso blocco catch una
caratteristica importante, perch consente di evitare di duplicare il codice di gestione
delle eccezioni in tanti blocchi catch separati, ognuno con un singolo tipo di
eccezione; tale codice di gestione, quindi, pu essere condiviso. importante
sottolineare, che quando usiamo pi di un tipo di eccezione, il parametro relativo
implicitamente final, ovvero non potremo assegnargli nessun altro valore allinterno
del blocco catch.

throw : consente di lanciare una determinata eccezione software o rilanciarne una


intercettata in un blocco catch.

Sintassi 11.4 Sintassi throw.


throw new ExceptionType(); // lancia un'eccezione del tipo indicato
throw e; // rilancia un'eccezione intercettata in un blocco catch

throws : con essa si specificano i tipi di eccezione di tipo checked che possono
occorrere in un metodo e che lo stesso demanda per la gestione al suo metodo
chiamante.

Sintassi 11.5 Sintassi throws.


public void foo() throws ExceptionType1, ExceptionType2, ..., ExceptionType3 { ... }

NOTA
Non si deve confondere throws (che compare nellintestazione di un metodo, subito dopo lelenco
degli eventuali parametri) con throw (che compare allinterno del metodo per lanciare
uneccezione). Riprenderemo pi in dettaglio questo aspetto nel proseguo del capitolo.
finally : con essa possibile specificare un blocco di codice le cui istruzioni
saranno sempre eseguite a prescindere che si sia verificata o no uneccezione.
Questa keyword opzionale, ma obbligatoria se un blocco try non ha il
relativo blocco catch.

Sintassi 11.6 Sintassi finally con catch.

try { ... }
catch (ExceptionType e) { ... }
finally { ... }

Sintassi 11.7 Sintassi finally senza catch.

try { ... }
finally { ... }

Listato 11.2 Classi ExceptionDivideByZero ed ExceptionDivideByZeroClient.

package com.pellegrinoprincipe;

class ExceptionDivideByZero extends ArithmeticException // classe eccezione


{
private static String msg = "Eccezione: divisione per 0.";
public ExceptionDivideByZero() { super(msg); }
public ExceptionDivideByZero(String msg) { super(msg); }
}

public class ExceptionDivideByZeroClient


{
public static void makeDiv(int num, int denom) // metodo per fare la divisione
{
if (denom == 0)
throw new ExceptionDivideByZero();
else
System.out.println("Divisione di " + num + " per " + denom + " = "
+ (num / denom));
}

public static void main(String[] args)


{
int num = 22;
int denom[] = {11, 0, 2, 4}; // denominatori

for (int n = 0; n < denom.length; n++) // fai la divisione


{
try
{
makeDiv(num, denom[n]);
}
catch (ExceptionDivideByZero e)
{
System.out.println(e);
}
}
}
}

Output 11.2 Dal Listato 11.2 Classi ExceptionDivideByZero ed ExceptionDivideByZeroClient.

Divisione di 22 per 11 = 2
com.pellegrinoprincipe.ExceptionDivideByZero: Eccezione: divisione per 0.
Divisione di 22 per 2 = 11
Divisione di 22 per 4 = 5

Il Listato 11.2 crea la classe ExceptionDivideByZero, che segnala un tentativo di


divisione di un numero intero per un denominatore 0. Essa estende la classe
ArithmeticException , che gi deputata a gestire tale tipo di errore. Abbiamo poi
definito la classe ExceptionDivideByZeroClient dove, nel relativo metodo main, allinterno
di un ciclo for, invochiamo il metodo makeDiv che tenta di eseguire una divisione tra il
numeratore 22 e gli elementi di un array denom. Il codice che invoca il metodo scritto
allinterno di un blocco try che, ricordiamo, deputato a contenere un insieme di
istruzioni che potrebbero lanciare o generare una determinata eccezione software.
In dettaglio, il metodo makeDiv verifica se un denominatore uguale a 0 e in tal caso
lancia con throw uneccezione di tipo ExceptionDivideByZero. Listruzione throw pu
lanciare qualsiasi oggetto eccezione che sia derivato dalla superclasse Throwable
(genitrice delle classi Exception, da cui indirettamente deriva ExceptionDivideByZero, ed
).
Error

Al momento del lancio delleccezione il programma esce subito dal metodo (se vi
fosse del codice dopo throw, questo non verrebbe mai pi eseguito) e prova a
verificare se esiste una clausola catch che sia in grado di gestire leccezione. Nel
nostro caso, la clausola catch che in grado di gestire leccezione quella che ha
come parametro un oggetto dello stesso tipo delloggetto eccezione creato da throw (o
un tipo che una sua superclasse). Infatti, il compilatore trova una clausola catch con
un parametro di tipo ExceptionDivideByZero e vi passa il controllo per la gestione
delleccezione. Qui il programma si limita a informare il client che vi stata una
divisione per 0.
Chiaramente, allinterno di un blocco catch, si pu visualizzare un messaggio
indicante il tipo di eccezione occorsa, ma si pu anche tentare di risolvere lerrore in
modo da permettere al programma di continuare lesecuzione in modo corretto. Nel
nostro caso, quindi, il blocco catch avrebbe potuto cambiare il denominatore da 0 a 1 e
permettere comunque la divisione.
NOTA
Se durante lesecuzione delle istruzioni poste in un blocco try non viene sollevata alcuna
eccezione, le istruzioni poste nel blocco catch relativo non saranno mai eseguite e il flusso di
esecuzione del programma riprender dalle istruzioni scritte dopo questultimo.

LA CLASSE EXCEPTION
Dato che il costrutto catch pu avere come parametro un tipo di una superclasse, se si deve
intercettare uneccezione checked particolare, si pu porre come parametro il tipo Exception, che
ricordiamo essere la classe padre di tutte le eccezioni di tipo checked. Tuttavia, se si vogliono
intercettare anche delle eccezioni che sono sottoclassi di Exception, si devono sempre porre le
clausole catch che le riguardano prima della clausola catch che gestisce Exception stesso, al fine di
non ottenere un errore del compilatore come il seguente: Unreachable catch block for
ExceptionType It is already handled by the catch block for Exception. In ogni caso, poich
possibile di indicare pi di uneccezione nellambito dello stesso blocco catch (multicatch),
preferibile usare questo approccio, piuttosto che indicare un oggetto di tipo Exception che verrebbe
utilizzato anche per altri tipi di eccezione da esso derivate, che magari non importante gestire
nellambito del nostro programma.

ATTENZIONE
Uneccezione pu capitare anche allinterno del gestore delleccezione stesso. In questo caso,
per gestirla baster scrivere un blocco try/catch allinterno dello stesso blocco catch.
Rilancio delle eccezioni
Un gestore delle eccezioni pu decidere che uneccezione debba essere rilanciata
per uneventuale altra gestione da parte di un blocco try/catch successivo.
Listato 11.3 Classi ExceptionDivideByZero_REV_1 ed ExceptionDivideByZeroClient_REV_1.

package com.pellegrinoprincipe;

class ExceptionDivideByZero_REV_1 extends ArithmeticException


{
private static String msg = "Eccezione: divisione per 0.";
private int ix;

public ExceptionDivideByZero_REV_1()
{
super(msg);
ix = 0;
}

// costruttore che registra anche la posizione dell'elemento causa dell'eccezione


public ExceptionDivideByZero_REV_1(int n)
{
super(msg);
ix = n;
}

public String what() // messaggio dell'eccezione


{
return msg + " alla posizione dell'array " + ix;
}

// posizione dell'elemento dell'array che ha generato l'eccezione


public int getPos() { return ix; }
}

public class ExceptionDivideByZeroClient_REV_1


{
private static final int num = 22; // numero da dividere

public static void makeDiv(int num, int denom, int ix) // metodo per fare
// la divisione
{
if (denom == 0)
throw new ExceptionDivideByZero_REV_1(ix);
else
System.out.println("Divisione di " + num + " per " + denom + " = " + num / denom);
}

public static void loopIn(int start, int num, int denom[])


// ciclo nell'array di denominatori
{
for (int n = start; n < denom.length; n++)
{
try
{
makeDiv(num, denom[n], n);
}
catch (ExceptionDivideByZero_REV_1 e)
{
System.out.print(e.what());
throw e; // rilanciamo l'eccezione
}
}
}

// provo comunque a gestire l'eccezione senza interrompere il programma


public static void tryToResolve(int denom[], int pos)
{
denom[pos] = 1;
try
{
loopIn(pos, num, denom);
}
catch (ExceptionDivideByZero_REV_1 e)
{
System.out.println(" risolvo forzando il denominatore a 1");
tryToResolve(denom, e.getPos());
}
}

public static void main(String args[])


{
int denom[] = {11, 0, 2, 0};

try
{
loopIn(0, num, denom);
}
catch (ExceptionDivideByZero_REV_1 e)
{
System.out.println(" risolvo forzando il denominatore a 1");
tryToResolve(denom, e.getPos());
}
}
}

Output 11.3 Dal Listato 11.3 Classi ExceptionDivideByZero_REV_1 ed


ExceptionDivideByZeroClient_REV_1.
Divisione di 22 per 11 = 2
Eccezione: divisione per 0. alla posizione dell'array 1 risolvo forzando il denominatore a 1
Divisione di 22 per 1 = 22
Divisione di 22 per 2 = 11
Eccezione: divisione per 0. alla posizione dell'array 3 risolvo forzando il denominatore a 1
Divisione di 22 per 1 = 22

Il Listato 11.3 mostra la classe di gestione delleccezione ExceptionDivideByZero_REV_1


con laggiunta del metodo getPos, che ritorna la posizione dellelemento dellarray che
ha causato leccezione, e del metodo what, che ritorna uninformazione dettagliata
sulleccezione. Abbiamo anche creato un costruttore in overloading che registra nella
variabile ix la posizione corrente dellelemento causa delleccezione.

Nella classe ExceptionDivideByZeroClient_REV_1 abbiamo definito il metodo loopIn;


questo invoca il metodo makeDiv che, quando incontra un denominatore uguale a 0,
solleva uneccezione di tipo ExceptionDivideByZero_REV_1 invocando il costruttore della
sua classe, al quale passa lindice dellarray che contiene lelemento con tale
denominatore.
Leccezione lanciata viene poi gestita dal blocco catch corrispondente (che si trova
nel metodo loopIn), il quale visualizza un messaggio di errore e la rilancia, con la
clausola throw, verso un successivo blocco catch che potr processarla nuovamente e in
un modo differente. Nel nostro caso il successivo blocco catch quello che si trova,
nellambito del metodo main, in corrispondenza del try dove stato invocato il metodo
loopIn. Qui, oltre a visualizzare un messaggio, si invoca il metodo tryToResolve, che
pone il valore 1 nellelemento dellarray che conteneva il valore 0 e invoca
nuovamente loopIn, che riparte da quellindice con il suo elemento, che ha ora un
valore che non genera leccezione. Ovviamente il procedimento si ripete per tutta la
scansione dellarray.
Il Listato 11.3 stato scritto in modo piuttosto elaborato al fine di evidenziare il
procedimento che il sistema segue durante lesecuzione del programma per trovare
un gestore di eccezioni appropriato.
In pratica, si verifica se il blocco catch relativo al blocco try che ha sollevato
leccezione sia in grado di gestirla e, in caso affermativo, si eseguono le istruzioni ivi
contenute, altrimenti si cercano altri blocchi catch (se esistono) facenti parte della pila
dei metodi chiamanti (stack unwinding). Se non si trova in nessun blocco un
adeguato gestore delleccezione, il programma terminer bruscamente mostrando
informazioni dettagliate sulleccezione occorsa (nome delloggetto eccezione, nome
della classe, nome del metodo, nome del file e numero di riga).
Listato 11.4 Classe ExceptionDivideByZeroClient_REV_2.

...
public class ExceptionDivideByZeroClient_REV_2
{
...
public static void loopIn(int start, int num, int denom[])
// ciclo nell'array di denominatori
{
for (int n = start; n < denom.length; n++)
{
try
{
makeDiv(num, denom[n], n);
}
catch (ExceptionDivideByZero_REV_1 e)
{
// System.out.print(e.what());
throw e; // rilanciamo l'eccezione
}
}
}

// provo comunque a gestire l'eccezione senza interrompere il programma


public static void tryToResolve(int denom[], int pos)
{
denom[pos] = 1;

try
{
loopIn(pos, num, denom);
}
catch (ExceptionDivideByZero_REV_1 e)
{
// System.out.println(" risolvo forzando il denominatore a 1");
// tryToResolve(denom, e.getPos());
}
}

public static void main(String args[])


{
int denom[] = {11, 0, 2, 0};

try
{
loopIn(0, num, denom);
}
catch (Error e)
{
// System.out.println(" risolvo forzando il denominatore a 1");
// tryToResolve(denom, e.getPos());
}
}
}

Output 11.4 Dal Listato 11.4 Classe ExceptionDivideByZeroClient_REV_2.

Divisione di 22 per 11 = 2
Exception in thread "main" com.pellegrinoprincipe.ExceptionDivideByZero_REV_1: Eccezione:
divisione per 0
at
com.pellegrinoprincipe.ExceptionDivideByZeroClient_REV_2.makeDiv(ExceptionDivideByZeroClient_REV_2.java:40
at
com.pellegrinoprincipe.ExceptionDivideByZeroClient_REV_2.loopIn(ExceptionDivideByZeroClient_REV_2.java:51)
at
com.pellegrinoprincipe.ExceptionDivideByZeroClient_REV_2.main(ExceptionDivideByZeroClient_REV_2.java:83)

Java Result: 1

LOutput 11.4 mostra chiaramente come, dopo che nel metodo makeDiv stata
lanciata leccezione ExceptionDivideByZero_REV_1, il blocco catch del metodo loopIn si
limita esclusivamente, dopo che lha intercettata, a rilanciarla verso un altro blocco
catch eventualmente presente in un suo metodo chiamante che, nel nostro caso, il

main.

Tuttavia, la classe eccezione Error, utilizzata con la clausola catch del predetto
metodo main, non corrisponde come tipo, sia esattamente sia come superclasse, al tipo
ExceptionDivideByZero_REV_1 di cui leccezione e pertanto il sistema, non trovando in quel
punto tale corrispondenza, interromper bruscamente il programma visualizzando le
informazioni sulleccezione occorsa.
altres interessante rilevare dallOutput 11.4 come il sistema mostra tutta la pila
dei metodi chiamanti srotolandola a partire dallultimo dove stata lanciata
leccezione, makeDiv, e risalendo fino al primo chiamante che sempre, ovviamente, il
main.

TYPE CHECKING MIGLIORATO DURANTE IL RILANCIO DI UNECCEZIONE


A partire dalla versione 7 di Java, il compilatore effettua unanalisi pi approfondita del tipo di
eccezione che pu essere rilanciata con la clausola throw, consentendo di specificare in modo pi
preciso le eccezioni lanciabili da un metodo mediante la clausola throws. Per esempio, nelle
versioni precedenti del compilatore, se allinterno di un metodo si lanciava in un blocco try
uneccezione e poi nel blocco catch corrispondente si definiva come parametro uneccezione di tipo
Exception, che poi veniva rilanciata, il metodo doveva necessariamente contenere la clausola
throws Exception, perch il tipo delleccezione era uguale al tipo indicato dal parametro del catch
(Exception), indipendentemente dalleffettivo tipo lanciato con il throw. Ora invece possibile
inserire direttamente nella clausola throws i tipi delle eccezioni specifiche, perch il compilatore in
grado di determinare quale tipo di eccezione pu essere lanciata dal blocco try e pertanto, anche
se il parametro del catch di tipo Exception, esso di fatto potr contenere solo un riferimento a uno
dei tipi di eccezione rilevati (Listato 11.5).

Listato 11.5 Classe RethrowingAndTypeChecking.

package com.pellegrinoprincipe;

class ExceptionA extends Exception {}


class ExceptionB extends Exception {}

public class RethrowingAndTypeChecking


{
public static void runException(String what) throws ExceptionA, ExceptionB
{
try
{
if (what.equals("A"))
throw new ExceptionA();
else if (what.equals("B"))
throw new ExceptionB();
}
catch (Exception e)
{
// rilancia e pu essere di tipo ExceptionA o di tipo ExceptionB
// cos come indicato nella clausola throws
throw e;
}
}
public static void main(String args[])
{
// esegue il metodo runException che potr lanciare un'eccezione di tipo
// ExceptionA oppure di tipo ExceptionB
try
{
runException("A");
}
catch (ExceptionA | ExceptionB e)
{ System.out.println(e.getClass().toString()); }
}
}

Output 11.5 Dal Listato 11.5 Classe RethrowingAndTypeChecking.

class com.pellegrinoprincipe.ExceptionA
Eccezioni checked e unchecked
Nella programmazione delle eccezioni importante comprendere che Java
distingue tra eccezioni che un programmatore deve sempre gestire (dette checked) ed
eccezioni che pu non gestire (dette unchecked).
ATTENZIONE
La ricerca del blocco catch in grado di gestire leccezione effettuata subito dal compilatore, in
fase di compilazione del programma, se leccezione di tipo checked, mentre viene effettuata a
run-time dalla virtual machine se leccezione di tipo unchecked.

Esempi di eccezioni unchecked sono quelle del tipo RuntimeException come, per
esempio:

ArrayOutOfBoundsException , che si verifica quando si cerca di manipolare gli elementi


di un array fuori dallindice massimo;
NullPointerException, che si verifica quando si cerca di usare un riferimento che

per non ha ancora alcun oggetto puntato;


ClassCastException, che si verifica quando si cerca di effettuare una conversione di

un tipo di oggetto verso un tipo incompatibile.

Esempi di eccezioni checked sono quelle del tipo Exception come, per esempio:

IOException , che pu verificarsi quando si usano oggetti per le manipolazioni dei


flussi di input/output;
AWTException, che pu verificarsi quando si utilizzano i componenti grafici per la

programmazione dinterfacce utente.

Come si gi detto, le eccezioni di tipo checked devono sempre essere gestite, e


per farlo possiamo utilizzare a scelta uno dei due modi seguenti:

indicare nella dichiarazione di un metodo la clausola throws con i tipi di eccezioni


che il metodo potr generare, demandandone la gestione al suo metodo
chiamante;
utilizzare il costrutto try/catch con i catch che conterranno i tipi di eccezioni che il
metodo potr generare e la cui gestione sar da essi stessi effettuata.

Listato 11.6 Classe CheckedExceptions.

package com.pellegrinoprincipe;

import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
public class CheckedExceptions
{
public static Scanner FileScanner(String file_name) throws FileNotFoundException
{
return new Scanner(new File(file_name));
}

public static void main(String[] args)


{
try(Scanner n_scanner = FileScanner("Test.html")) // try-with-resource
{
System.out.println("Se vi un'eccezione non saro' visualizzata!");
}
catch (FileNotFoundException fnf)
{
System.out.println("Eccezione. File non trovato. " +
"Forse il nome file e' errato?");
}
}
}

Output 11.6 Dal Listato 11.6 CheckedExceptions.


Eccezione. File non trovato. Forse il nome file e' errato?

Il Listato 11.6 definisce, allinterno della classe CheckedExceptions, il metodo


FileScanner , che usa un oggetto Scanner per processare un file. Quando si crea un
oggetto di questo tipo, mediante il suo costruttore che accetta come argomento un
oggetto di tipo File, si deve sempre gestire uneccezione di tipo FileNotFoundException,
poich un oggetto Scanner pu lanciarla ed essa deve essere obbligatoriamente gestita
in quanto eccezione di tipo checked. A tal fine, nel metodo FileScanner abbiamo usato
la clausola throws FileNotFoundException a indicare che il metodo consapevole che le
sue istruzioni potranno generare tale eccezione, ma tuttavia non vuole comunque
gestirla, e pertanto indicher al compilatore di verificare che il suo metodo chiamante
lo faccia. Nel nostro caso il metodo chiamante main, che attraverso una clausola
try/catch si prender limpegno di gestire leccezione.

importante notare che la clausola try scritta nel metodo main si avvale di una
sintassi particolare (try-with-resources); tale sintassi consente di inserire tra le
parentesi tonde ( ) unistruzione che crea un oggetto di un tipo di classe che
rappresenta una risorsa la quale necessita di essere chiusa dopo il suo utilizzo.
NOTA
possibile inserire anche pi istruzioni nella clausola try-with-resources, separandole con il
carattere punto e virgola (;) come in questo esempio: try (InputStream in = new
FileInputStream(input); OutputStream out = new FileOutputStream(output)) {}.

Le classi che agiscono come risorse nellambito del costrutto try-with-resources


devono aver implementato linterfaccia java.lang.AutoCloseable o java.io.Closeable.
Proprio per questo, a partire dalla versione 7 di Java, molte classi importanti sono
state riscritte con limplementazione delle interfacce citate, per esempio
java.util.Scanner, java.sql.Connection, java.io.FileReader, java.io.OutputStream e molte altre.

Grazie a questo costrutto, quindi, non vi sar pi la necessit di chiudere


manualmente una risorsa (generalmente tale operazione era effettuata allinterno di
un blocco finally), perch la stessa sar chiusa automaticamente dal sistema
indipendentemente dal fatto che vi sia stata o no uneccezione (in pratica sar
invocato il metodo close della classe di tipo risorsa indicata, e se sono state utilizzate
pi risorse, lordine di chiusura sar inverso rispetto allordine della loro
dichiarazione iniziale, partendo dallultima classe e procedendo verso la prima).
In ogni caso, se nel blocco try-with-resources si verifica uneccezione e unaltra
eccezione generata anche nellambito dellesecuzione delloperazione di chiusura
della risorsa, solo la prima sar lanciata, mentre lultima sar soppressa.
Ritornando al nostro esempio, loggetto n_scanner di tipo Scanner verr chiuso
automaticamente al termine dellesecuzione del relativo blocco try, o anche se si sar
verificata uneccezione di tipo FileNotFoundException.
Eccezioni nei costruttori e nei distruttori
Uneccezione pu essere generata senza alcun problema sia nel costruttore sia nel
distruttore di una classe, come evidenziato dal Listato 11.7.
Listato 11.7 Classe ConstructorDestructorExceptions.

package com.pellegrinoprincipe;

class Test
{
private double d[][] = new double[1000000][];

public Test() // costruttore


{
try
{
for (int a = 0; a < d.length; a++) // genero un'eccezione troppa RAM usata
d[a] = new double[8000000];
}
catch (OutOfMemoryError oom)
{
System.out.println("Eccezione dal *COSTRUTTORE* DI Test: basta, ho finito la RAM");
}
}

public void finalize() // distruttore


{
int r = 0;
try
{
r = 10 / 0; // divisione per 0
}
catch (ArithmeticException ae)
{
System.out.println("Eccezione dal *DISTRUTTORE* di Test: Divisione per 0.");
}
}
}

public class ConstructorDestructorExceptions


{
public static void main(String args[])
{
Test t = new Test();
t = null; // t non punta pi a nulla
System.gc(); // forza l'esecuzione del garbage collector
}
}

Output 11.7 Dal Listato 11.7 Classe ConstructorDestructorExceptions.

Eccezione dal *COSTRUTTORE* DI Test: basta, ho finito la RAM


Eccezione dal *DISTRUTTORE* di Test: Divisione per 0.

Il Listato 11.7 definisce la classe Test con un costruttore che compie unoperazione
che generer uneccezione di tipo OutOfMemoryError e un distruttore (metodo finalize)
che generer uneccezione di tipo ArithmeticException. La classe
ConstructorDestructorExceptions , invece, definisce un metodo main che prima crea un
oggetto di tipo Test dove il suo costruttore generer leccezione OutOfMemoryError, e poi
pone il valore null nel suo riferimento t in modo che loggetto di tipo Test non sia pi
referenziato da alcuna variabile, permettendo al garbage collector, che viene
forzatamente invitato ad attivarsi mediante listruzione System.gc(), di eliminarlo dalla
memoria; prima per viene invocato il metodo finalize delloggetto stesso, che a sua
volta generer uneccezione di tipo ArithmeticException.
Eccezioni a catena
Nella scrittura di un programma consuetudine implementare metodi che
chiamano altri metodi, che a loro volta ne chiamano altri e cos via, in una lunga
catena di invocazioni. Pu capitare che uno di questi metodi generi uneccezione che
viene catturata in un blocco catch il quale, a sua volta, la rilancia (o ne lancia unaltra
di diverso tipo). Questultima eccezione viene poi intercettata da un altro gestore, che
si comporta come il gestore precedente rilanciandola nuovamente e creando cos una
catena di eccezioni. In questo caso possiamo utilizzare dei metodi della classe
java.lang.Throwable che consentono di tenere traccia della catena di eccezioni

tracciando quelle che hanno causato le altre eccezioni.

public Throwable getCause() ritorna un oggetto di tipo Throwable the rappresenta la


precedente eccezione causa da cui ha avuto origine leccezione che si sta
processando oppure null se la causa non esistente oppure sconosciuta.
public Throwable initCause(Throwable cause) consente di porre nel parametro cause il
riferimento di un oggetto eccezione che ha dato origine alleccezione che si sta
gestendo.
public Throwable(String message, Throwable cause) crea listanza di un nuovo oggetto

eccezione, dove il parametro message conterr il messaggio delleccezione e il


parametro cause conterr loggetto eccezione da cui ha avuto origine leccezione
che si sta processando.
public Throwable(Throwable cause) crea listanza di un nuovo oggetto eccezione, dove
il parametro cause conterr loggetto eccezione da cui ha avuto origine
leccezione che si sta processando.

Listato 11.8 Classe ChainedExceptions.


package com.pellegrinoprincipe;

public class ChainedExceptions


{
public static void call1() throws Exception
{
try
{
call2();
}
catch (Exception e)
{
System.out.println("Causa originaria: " + e.getCause().getMessage());
System.out.println(e.getMessage());
throw new Exception("Eccezione lanciata da call1", e);
// throw (Exception) new Exception("Eccezione lanciata da call1").initCause(e);
}
}

public static void call2() throws Exception


{
try
{
call3();
}
catch (Exception e)
{
throw new Exception("Eccezione lanciata da call2", e);
// throw (Exception) new Exception("Eccezione lanciata da call2").initCause(e);
}
}

public static void call3() throws Exception


{
throw new Exception("Eccezione lanciata da call3");
}

public static void main(String args[])


{
try
{
call1();
}
catch (Exception e)
{
System.out.println("Causa originaria: " + e.getCause().getMessage());
System.out.println(e.getMessage());
System.out.println("ATTENZIONE eccezione nel main RILEVATA");
}
}
}

Output 11.8 Dal Listato 11.18 Classe ChainedExceptions.


Causa originaria: Eccezione lanciata da call3
Eccezione lanciata da call2
Causa originaria: Eccezione lanciata da call2
Eccezione lanciata da call1
ATTENZIONE eccezione nel main RILEVATA

Il Listato 11.8 mostra come costruire un sistema per il tracciamento delle eccezioni
a catena. Esaminando il codice sorgente a partire dal metodo main, elenchiamo i
passaggi di esecuzione del codice.

1. Viene invocato il metodo call1.


2. Il metodo call1 invoca il metodo call2.
3. Il metodo call2 invoca il metodo call3.
4. Il metodo call3 genera uneccezione di tipo Exception che, non potendo essere
gestita allinterno del metodo per mancanza di un gestore catch opportuno,
costringe il sistema a verificare se il metodo chiamante (call2) ha tale gestore.
5. Il metodo call2 ha un catch appropriato, al cui interno lancia per una nuova
eccezione indicando anche la precedente eccezione che ne causa. Qui per
tenere traccia della catena delle eccezioni abbiamo utilizzato il costruttore
delloggetto eccezione Exception, che prende come argomenti un messaggio di
eccezione e leccezione causale. Nei commenti e in grassetto riportata una
soluzione alternativa che utilizza il metodo initCause.
6. Leccezione lanciata dal metodo call2 viene rilevata dal catch del metodo call1,
che stampa un messaggio indicante leccezione causale (ottenuto grazie al
metodo getCause) e un altro messaggio indicante il messaggio delleccezione
attuale. Poi il metodo call1 lancia una nuova eccezione senza compiere alcuna
gestione particolare.
7. Il metodo main nel suo gestore catch stampa le stesse informazioni descritte
precedentemente per il metodo call1 e termina il programma con un messaggio
di eccezione rilevata.
Metodi informativi delle eccezioni
La classe Throwable, classe base per tutte le altre classi eccezioni, dispone dei
seguenti metodi utili per ottenere informazioni sulle eccezioni occorse.

public String getMessage() : ritorna una stringa descrittiva del messaggio di errore.
public void printStackTrace() : consente di visualizzare in output, sullo stream
standard error lo stack trace dettagliato dellerrore.
public void printStackTrace(PrintStream s): consente di visualizzare sullo stream di

output specificato dal parametro s lo stack trace dettagliato dellerrore.


public StackTraceElement[] getStackTrace() : ritorna le informazioni dello stack trace
come un array di oggetti di tipo StackTraceElement, dove ogni oggetto espone i
metodi getClassName, getFileName, getLineNumber e getMethodName.

TERMINOLOGIA
L o stack trace generalmente definito come una struttura informativa contenente, in
successione, tutta la pila di metodi utilizzati. Nel caso delle eccezioni conterr le informazioni
dettagliate sulla catena di metodi invocati fino allultimo dove occorsa leccezione.
Listato 11.9 Classe StackTraceExceptions.
package com.pellegrinoprincipe;

public class StackTraceExceptions


{
public static void exc() throws Exception // metodo che lancia un'eccezione
{
throw new Exception("Ops");
}

public static void main(String args[])


{
try
{
exc();
}
catch (Exception e)
{
System.out.println("Messaggio dell'eccezione: " + e.getMessage());

// stampa lo stack trace attraverso l'utilizzo di oggetti


// di tipo StackTraceElement
System.out.println("Stack trace dell'eccezione: ");
StackTraceElement st[] = e.getStackTrace();
for (StackTraceElement el : st)
System.out.println("CLASSE: " + el.getClassName() + "\n"
+ "METODO: " +
+ el.getMethodName() + "\n"
+ "FILE: "
+ el.getFileName() + "\n"
+ "NUMERO DI LINEA: "
+ el.getLineNumber());
}
}
}

Output 11.9 Dal Listato 11.9 Classe StackTraceExceptions.


Messaggio dell'eccezione: Ops
Stack trace dell'eccezione:
CLASSE: com.pellegrinoprincipe.StackTraceExceptions
METODO: exc
FILE: StackTraceExceptions.java
NUMERO DI LINEA: 7
CLASSE: com.pellegrinoprincipe.StackTraceExceptions
METODO: main
FILE: StackTraceExceptions.java
NUMERO DI LINEA: 14

ATTENZIONE
Loutput dello stack trace mostrato in ordine inverso rispetto allinvocazione dei metodi, poich
una struttura dati di tipo stack ha una modalit di accesso LIFO (Last In First Out), dove lultimo
dato inserito anche il primo a uscire. Infatti, nel nostro caso vedremo elencato prima il metodo
exc (causa delleccezione) e poi il metodo main che lha invocato.
Classi eccezione della libreria standard
Il linguaggio Java mette a disposizione un numero impressionante di classi
eccezione, che non possibile illustrare tutte in un unico diagramma. Tuttavia
importante sapere che, dal punto di vista gerarchico, tutte le classi eccezione hanno
come genitore la classe Throwable, dalla quale discendono la classe Exception e la classe
Error (Figura 11.1).

Nella Figura 11.1 vediamo che dalla classe Error discendono classi per la gestione
di errori software, come OutOfMemoryError; questi errori sono lanciati dal sistema run-
time di Java e rappresentano eventi catastrofici che non possono essere, solitamente,
gestiti dal programma.
Dalla classe Exception discendono invece classi per la gestione di errori software,
come IOException, che possono essere causati dai programmi e che devono essere
gestiti dal programmatore medesimo (in quanto eccezioni di tipo checked).
Sempre dalla classe Exception discende la classe RuntimeException, dalla quale
discendono classi per la gestione di errori software, come ArithmeticException, che sono
lanciati dalla JVM e che possono non essere gestiti dal programmatore (in quanto
eccezioni di tipo unchecked).

Figura 11.1 Diagramma parziale della gerarchia delle classi eccezione.


Ricordiamo che possono essere lanciate eccezioni solo se esse derivano almeno
dalla classe Throwable. Ci significa che non possibile scrivere classi eccezione senza
una relazione di ereditariet con la predetta classe.
Capitolo 12
Package

I package sono il meccanismo mediante il quale con Java si possono creare librerie
di tipi correlati. Il JDK formato da centinaia di package al cui interno si trovano dei
tipi che svolgono specifiche funzioni. Solo per citarne alcuni: java.lang contiene i tipi
fondamentali come Math, Object, Runtime, String e cos via; java.net contiene i tipi per la
gestione del networking come Intet4Address, Proxy, URL, Socket e cos via; java.util
contiene i tipi collezione (come ArrayList e HashMap), di tempo e data (come
), di internazionalizzazione (come Locale) e cos via; java.awt contiene i
GregorianCalendar

tipi per la realizzazione di applicazioni con interfaccia grafica come Button, Dialog,
,
Event Font e cos via.

La strutturazione dei tipi in package consente di raggiungere i seguenti scopi.

Evitare conflitti di nome tra tipi, poich il nome della classe parte del nome del
package.
Riusare tipi gi scritti da altri programmatori importandone i relativi package.
Raggruppare i tipi secondo criteri funzionali e di correlazione.
Fornire un ulteriore livello di accesso (rispetto ai livelli public, protected e private)
per i membri dei tipi.
Creazione di package
Per creare un package si utilizza la keyword package seguita dal nome del package;
questa deve sempre essere la prima istruzione allinterno del file sorgente contenente
la definizione dei tipi.
Sintassi 12.1 Keyword package.

package pref1.pref2.pref3prefN;

importante sottolineare che il nome di un package qualifica il nome dei tipi che
gli appartengono. Per esempio, se abbiamo il seguente file sorgente:
Snippet 12.1 File Employee.java.

package com.pellegrinoprincipe;
public class Employee {}

la classe Employee avr come nome completo com.pellegrinoprincipe.Employee.

Il nome scelto generalmente rispecchia una struttura di directory gerarchica,


ovvero, riprendendo lesempio dello Snippet 12.1, il package com.pellegrinoprincipe
potr essere mappato sul filesystem con la creazione di una directory denominata com
al cui interno sar creata una directory di nome pellegrinoprincipe al cui interno verr
posto il file Employee.class. Dobbiamo tuttavia precisare che la mappatura della
struttura logica in struttura fisica non viene effettuata automaticamente dal
compilatore; per richiederla occorre infatti invocare il compilatore di Java nel modo
seguente.
Shell 12.1 Compilazione del file Employee.java.

javac -d C:\MY_JAVA_CLASSES Employee.java

Il flag -d indica la directory di destinazione della compilazione, nella quale


verranno posti tutti i file .class e dove verr creata uneventuale struttura di directory,
se un file sorgente contiene la definizione di un package. Nel nostro caso il comando
crea, a partire dalla directory MY_JAVA_CLASSES, la struttura com\pellegrinoprincipe, dove
inserisce anche il file Employee.class.

Dopo aver scritto listruzione di definizione di un package, si pu scrivere la


definizione dei tipi che gli appartengono, rammentando che tra questi tipi ve n pu
essere solo uno con specificatore di accesso public che consentir di utilizzarlo da
classi appartenenti ad altri package. Gli altri tipi, invece, non potendo essere public,
saranno per default di tipo package private, ovvero saranno accessibili e utilizzabili
solo dai tipi appartenenti al loro stesso package.
SCEGLIERE I NOMI DEI PACKAGE
Il nome di un package pu essere scelto utilizzando la seguente convenzione: deve contenere solo
lettere in minuscolo; deve corrispondere al nome di dominio Internet dellautore scritto in ordine
inverso; deve avere una specificazione dettagliata, secondo le proprie convenzioni, se allinterno
del proprio dominio vi sono pi sottodomini che rappresentano pi sedi operative; deve utilizzare il
carattere di underscore (_) se il nome del proprio dominio utilizza caratteri non permessi dalla
sintassi di Java (per esempio, se il nome del dominio contiene il carattere del trattino -, oppure se
contiene una parola riservata del linguaggio, come double).

NOTA
La scelta di attribuire al package un nome secondo la convenzione tipica adottata per i nomi di
dominio Internet ha il vantaggio di assicurare lunivocit dei nomi. Questo estremamente utile in
progetti di media e grande dimensione, dove si utilizzano svariate librerie di classi, scritte da
autori diversi. Senza una simile convenzione, il rischio di name clash, ossia di conflitti di nome,
aumenta notevolmente.
Utilizzo dei package
Se un tipo, per esempio la classe A, appartiene a un package come
com.pellegrinoprincipe , allora pu essere utilizzato da altri tipi, per esempio dalla classe
B, secondo le seguenti regole.

Se la classe B appartiene allo stesso package della classe A, allora la classe A


vista dalla classe B senza particolari qualificazioni.
Se la classe B appartiene a un package differente, come com.thp, rispetto a quello
della classe A, allora la classe A per essere vista dalla classe B pu essere
qualificata con il suo nome completo che, ricordiamo, include il nome del
package di appartenenza (Snippet 12.2).

Snippet 12.2 Qualificazione completa.


com.pellegrinoprincipe.A a = new com.pellegrinoprincipe.A();

Generalmente si adotta questo approccio quando il tipo che si vuole utilizzare


richiesto in cos poche occasioni da non giustificare lutilizzo di unapposita direttiva
di importazione; in alternativa pu essere importata singolarmente (single type import
declaration), utilizzando la direttiva import che consente di evitare la completa
qualificazione del tipo importato (Snippet 12.3).
Snippet 12.3 Importazione singola.

import com.pellegrinoprincipe.A;
A a = new A();

Questo approccio elimina il noioso compito di qualificare completamente il nome


dei tipi ed generalmente utile quando un package non contiene un numero elevato
di tipi. La classe pu anche essere importata insieme agli altri tipi appartenenti al
medesimo package (type import on demand declaration), sempre con la direttiva
import, che consente in questo caso di non dover qualificare nessun tipo importato

oltre a quello di nostro interesse Ssnippet 12.4.


Snippet 12.4 Importazione multipla.
import com.pellegrinoprincipe.*;
A a = new A();

In questo approccio la direttiva import utilizzata, scrivendo il simbolo asterisco *


dopo il nome del package, per importarne tutti i tipi, che per non verranno
automaticamente caricati in memoria. Infatti, la JVM, caricher in memoria i tipi solo
allorquando saranno utilizzati.
La direttiva import pu essere altres utilizzata:

per importare i membri statici di un tipo (static import) consentendo a un


programma di utilizzarli direttamente senza qualificarli con il nome della classe
di appartenenza. Cos come per i package, anche per i membri statici esiste la
possibilit di effettuare unimportazione singola e unimportazione multipla;
per limportazione singola (single static import), dove limportazione statica
effettuata utilizzando la keyword import static che ha consentito di utilizzare il
metodo statico abs senza qualificarlo con il nome della classe Math:

Snippet 12.5 Importazione singola membri statici.

import static java.lang.Math.abs;


int a = abs(-5);

per limportazione multipla (static import on demand), dove limportazione di


tutti i membri statici della classe Math si effettua utilizzando sempre la keyword

import static, ma anche il simbolo di asterisco (*) dopo il nome della classe
medesima. Quando si utilizza la direttiva di importazione statica, occorre
ricordare di far precedere il nome del tipo dal nome del suo package di
appartenenza:

Snippet 12.6 Importazione multipla membri statici.

import static java.lang.Math.*;


int a = abs(-5);
int b = sqrt(16);
int c = max(5, 10);

per importare tipi pubblici annidati allinterno di un altro tipo. Se abbiamo un


file sorgente Computer.java cos definito:

Snippet 12.7 File Computer.java.


package com.pellegrinoprincipe;

public class Computer


{
public enum Hardware { MOUSE, KEYBOARD; }
}

Possiamo, da un programma che utilizza la classe Computer, importare solamente il


tipo Hardware, utilizzando la direttiva import nel seguente modo:
Snippet 12.8 Classe ComputerClient.

import com.pellegrinoprincipe.Computer.Hardware;

public class ComputerClient


{
public static void main(String[] args)
{
Hardware _h_ = Hardware.KEYBOARD;
}
}
Visibilit e disponibilit dei package
Dopo aver creato i package in una locazione del filesystem del sistema operativo in
uso, vediamo la procedura da seguire per renderli visibili (disponibili) sia al
compilatore sia alla virtual machine.
TERMINOLOGIA
Spesso si utilizza il termine deployment per indicare le operazioni con cui si rende disponibile per
lutilizzo un sistema software. Nel caso dei package, per deployment intendiamo la procedura
mediante la quale i package vengono creati in una locazione del filesystem del sistema operativo
e vengono resi disponibili (visibili) al compilatore e alla virtual machine.

Tale procedura si attua scegliendo una di due opzioni: utilizzando il flag -classpath
(o -cp) del compilatore e il flag -cp della JVM, oppure utilizzando una variabile di
ambiente denominata CLASSPATH.
Listato 12.1 Classe Computer.

package com.pellegrinoprincipe.hardware;

public class Computer


{
private String os;

public enum Hardware { MOUSE, KEYBOARD; }

public void setOS(String os) { this.os = os; }


public String getOS() { return os; }
}

Shell 12.2 Compilazione della classe Computer (dalla dir C:\MY_JAVA_SOURCES).

javac -d C:\MY_JAVA_PACKAGES Computer.java

Il comando della Shell 12.2 crea nella directory MY_JAVA_PACKAGES la struttura


com\pellegrinoprincipe\hardware dove saranno posti i file Computer$Hardware.class e
Computer.class .
Listato 12.2 Classe ComputerClient.

package com.thp;

import com.pellegrinoprincipe.hardware.*;

public class ComputerClient


{
public static void main(String[] args)
{
Computer c = new Computer(); // istanza di un oggetto Computer
c.setOS("GNU/LINUX");

System.out.println("OS = " + c.getOS());


}
}

Shell 12.3 Compilazione della classe ComputerClient (dalla dir C:\MY_JAVA_SOURCES).

javac -classpath C:\MY_JAVA_PACKAGES -d C:\MY_JAVA_CLASSES ComputerClient.java


Il comando della Shell 12.3 compila la classe ComputerClient.java indicando tramite il
flag -classpath la directory dove cercare i package che potrebbero contenere i tipi di
cui si avvale la classe ComputerClient. La classe ComputerClient utilizza infatti la classe
Computer, che si trova nel package com.pellegrinoprincipe.hardware, cercato a partire dalla
directory C:\MY_JAVA_PACKAGES.

Quando si utilizza il flag -classpath:

il compilatore elimina dal proprio percorso di default di ricerca dei package la


directory corrente dove si trovano il file sorgente o i file .class di interesse.
Pertanto, se a partire dalla directory corrente ci sono dei package utilizzati,
occorre indicare anche tale directory;
possibile specificare percorsi multipli di ricerca separandoli con il punto e
virgola (;) per i sistemi Windows e con i due punti (:) per i sistemi GNU/Linux.

Ora vediamo un esempio di compilazione che comprende sia il percorso corrente,


sia il separatore utilizzato per indicare package posti in percorsi differenti.
A tal fine creiamo una nuova classe Software che avr un suo package e la cui
struttura sar posta a partire dalla directory corrente (ovvero a partire dalla directory
MY_JAVA_SOURCES dove, ricordiamo, si trovano tutti i nostri sorgenti).

Listato 12.3 Classe Software.

package com.pellegrinoprincipe.software;

public class Software


{
private Graphic graphic;

public enum Graphic { PHOTOSHOP, PAINT_NET; }

public void setGraphic(Graphic g) { graphic = g; }


public Graphic getGraphic() { return graphic; }
}

Shell 12.4 Compilazione della classe Software (dalla dir C:\MY_JAVA_SOURCES).

javac -d . Software.java

Il comando della Shell 12.4 compila il file Software.java e crea, a partire dalla
directory corrente indicata dal carattere punto (.), la struttura
com\pellegrinoprincipe\software , allinterno della quale saranno posti i file
Software$Graphic.class e Software.class.
Listato 12.4 Classe ComputerClient_REV_1.
package com.thp;
import com.pellegrinoprincipe.hardware.*;
import com.pellegrinoprincipe.software.*;
public class ComputerClient_REV_1
{
public static void main(String[] args)
{
...
Software s = new Software(); // oggetto di tipo Software
Software.Graphic g = Software.Graphic.PHOTOSHOP;
s.setGraphic(g);
System.out.println("SOFTWARE = " + s.getGraphic());
}
}

Ora lanciamo il comando riportato di seguito.


Shell 12.5 Compilazione della classe ComputerClient_REV_1 (dalla dir C:\MY_JAVA_SOURCES).

javac -classpath .;C:\MY_JAVA_PACKAGES -d C:\MY_JAVA_CLASSES ComputerClient_REV_1.java

Il comando della Shell 12.5 mostra come importare pi package: quelli trovati nel
percorso corrente e quelli trovati nella directory MY_JAVA_PACKAGES.

Dopo aver compilato le classi delle nostre applicazioni, vediamo ora come si
eseguono i programmi del Listato 12.2 (classe ComputerClient) e del Listato 12.4 (classe
ComputerClient_REV_1 ), tenendo presente che anche la JVM, come il compilatore, deve
trovare i package di cui entrambi i programmi necessitano.
Shell 12.6 Esecuzione della classe ComputerClient (dalla dir C:\MY_JAVA_CLASSES).
java -cp .;C:\MY_JAVA_PACKAGES com.thp.ComputerClient

Output 12.1 Dal Listato 12.2 Classe ComputerClient.


OS = GNU/LINUX

Shell 12.7 Esecuzione della classe ComputerClient_REV_1 (dalla dir C:\MY_JAVA_CLASSES).

java -cp .;C:\MY_JAVA_PACKAGES;C:\MY_JAVA_SOURCES com.thp.ComputerClient_REV_1

Output 12.2 Dal Listato 12.4 Classe ComputerClient_REV_1.


OS = GNU/LINUX
SOFTWARE = PHOTOSHOP

ATTENZIONE
Dobbiamo precisare che il compilatore e linterprete si comportano in modo differente riguardo
allimpostazione della directory corrente. Infatti, quando utilizziamo il flag -cp linterprete vuole
che si specifichi sempre come percorso di ricerca la directory corrente, anche se la classe che
stiamo eseguendo si trova in tale directory, mentre per il compilatore tale indicazione non
obbligatoria.

Vediamo ora come si imposta la variabile di ambiente CLASSPATH per lindicazione


dei percorsi dei package, ricordando che essa ci permette di non specificare alcun
flag in fase di compilazione e di esecuzione del codice.
Per impostare la variabile dambiente limitatamente alla sessione di shell corrente,
scriveremo i comandi che seguono.
Shell 12.8 Impostazione della variabile CLASSPATH in ambiente Windows.
set CLASSPATH=.;C:\MY_JAVA_PACKAGES;C:\MY_JAVA_SOURCES

Shell 12.9 Impostazione della variabile CLASSPATH in ambiente GNU/Linux.

CLASSPATH=.:/opt/MY_JAVA_PACKAGES:/opt/MY_JAVA_SOURCES
export CLASSPATH

Per impostare la variabile dambiente in modo che non sia limitata alla sessione di
shell corrente ma disponibile a ogni accesso al sistema, si procede in due modi
diversi a seconda del sistema operativo.

Per Windows, accedere alla voce Impostazioni di sistema avanzate e dalla


finestra Propriet di sistema fare clic sul pulsante Variabili dambiente, che
visualizzer la finestra omonima. A questo punto, se non esiste gi, creare come
variabile di sistema, o come variabile dellutente connesso, la variabile CLASSPATH
con lo stesso valore indicato nella Shell 12.8.
Per GNU/Linux, editare il file .bash_profile dellutente attualmente connesso,
oppure il file /etc/profile, per rendere i package disponibili a tutti gli utenti, e
scrivere gli stessi comandi indicati nella Shell 12.9.

Dopo aver impostato la variabile di ambiente CLASSPATH potremo compilare ed


eseguire le classi client come segue.
Shell 12.10 Compilazione (dalla dir C:\MY_JAVA_SOURCES) ed esecuzione
(dalla dir C:\MY_JAVA_CLASSES) della classe ComputerClient.

javac -d C:\MY_JAVA_CLASSES ComputerClient.java


java com.thp.ComputerClient
Archiviazione dei package
Quando si sviluppano molti package, oppure quando si hanno package che
contengono molti tipi, pu essere opportuno archiviarli in un unico file che sar poi
utilizzato per la distribuzione in un ambiente di produzione.
Il formato di file utilizzato da Java per larchiviazione si chiama JAR (Java
Archive file format) e il tool (comando) che consente di effettuare tale operazione
denominato jar.

Un file JAR si crea utilizzando la seguente sintassi.


Sintassi 12.2 Utilizzo del comando jar.

jar options jar_name input_files

Il parametro options rappresenta una delle seguenti opzioni che possiamo passare al
comando: c per creare un archivio; t per visualizzare la struttura di un archivio; x per
estrarre il contenuto di un archivio; u per aggiornare il contenuto di un archivio; f per
indicare il nome del file dellarchivio; v per indicare che si vuole indirizzare loutput
sullo stdout delle operazioni eseguite dal comando; 0 per indicare di non comprimere
larchivio (altrimenti sar compresso per default in formato ZIP); M per non produrre
il file manifesto di default; m per indicare un file che contiene le informazioni di
manifesto; -C per cambiare la directory di ricerca dei file da aggiungere allarchivio
durante lesecuzione del comando jar.

Riportiamo nel seguito alcuni esempi di utilizzo pratico del comando jar,
considerando:

i package fin qui creati, ovvero com.pellegrinoprincipe.hardware (che si trover nella


directory MY_JAVA_PACKAGES) e com.pellegrinoprincipe.software (che sposteremo dalla
directory MY_JAVA_SOURCES alla directory MY_JAVA_PACKAGES);
che la directory corrente, prima del lancio del comando, deve essere
MY_JAVA_PACKAGES:

Shell 12.11 Creazione di un archivio (dalla dir C:\MY_JAVA_PACKAGES).

jar cfv C:\MY_JAVA_JARS\HardwareAndSoftwareAPI.jar *

Il comando della Shell 12.11 crea: un archivio di nome HardwareAndSoftwareAPI.jar


nella directory MY_JAVA_JARS (lestensione .jar non obbligatoria) aggiungendo tutti i
file e le directory (carattere *) che si trovano nella directory MY_JAVA_PACKAGES; una
directory denominata META-INF, al cui interno verr posto un file denominato
MANIFEST.MF . Questo manifest file un file al cui interno si possono scrivere, secondo
unapposita sintassi, delle informazioni sulle funzionalit che il file .jar in grado di
fornire e, pi in generale, su altri aspetti come per esempio il nome dei file che
contiene.
Il file manifesto di default si presenta come il seguente snippet di codice.
Snippet 12.9 File manifesto di default.

Manifest-Version: 1.0
Created-By: 1.8.0-ea (Oracle Corporation)

Notiamo che le informazioni si scrivono utilizzando, per ognuna di esse, degli


header (chiamati anche attributi) indicati come delle coppie di chiave/valore separate
dal segno di due punti. Nel nostro caso le informazioni indicano che il manifesto
conforme alla versione 1.0 delle specifiche sui manifesti e che stato creato con la
versione 1.8.0 del JDK.
Per aggiungere specifiche informazioni a un file manifesto occorre effettuare le
seguenti operazioni:

creare un file di testo con le informazioni desiderate secondo la sintassi esposta


e facendolo terminare, obbligatoriamente, con un carattere di new line o di
carriage return;
creare il file di archivio utilizzando la sintassi che segue dove oltre ai flag c e f,
gi esaminati, si utilizzano il flag m e il nome del file manifesto da cui reperire le
informazioni.
Sintassi 12.3 Creazione di un JAR con laggiunta di un manifest file personalizzato.

jar cfm jar_name manifest_file input_files

Vediamo ora degli esempi che aggiungono informazioni personalizzate,


considerando che il nostro file manifesto si chiama Manifest.txt e che sar posto nella
directory C:\MY_JAVA_SOURCES.
Snippet 12.10 Indicazione della classe che avvia lapplicazione e dei dati di versioning.
Main-Class: com.thp.ComputerClient_REV_1
Specification-Title: HardwareAndSoftware API
Specification-Version: 1.0
Specification-Vendor: Pellegrino Principe
Implementation-Title: com.pellegrinoprincipe
Implementation-Version: build 500
Implementation-Vendor: Pellegrino Principe

Lo Snippet 12.10 mostra come indicare la classe di avvio dellapplicazione


utilizzando lheader Main-Class. Infatti, nel nostro caso indichiamo come classe che
conterr il metodo main la classe ComputerClient_REV_1, che porremo (il relativo file
ComputerClient_REV_1.class ), unitamente alla sua struttura di directory (com\thp), nella
directory MY_JAVA_PACKAGES, al fine di farla inserire allinterno dellarchivio
HardwareAndSoftwareAPI.jar .
Shell 12.12 Creazione dellarchivio con un file manifesto personalizzato
(dalla dir C:\MY_JAVA_PACKAGES).

jar cfmv C:\MY_JAVA_JARS\HardwareAndSoftwareAPI.jar C:\MY_JAVA_SOURCES\Manifest.txt *

Il comando della Shell 12.12 creer larchivio HardwareAndSoftwareAPI.jar, che


conterr tutti i file posti sotto la directory MY_JAVA_PACKAGES, e il file MANIFEST.MF, che
conterr al suo interno, oltre agli header Manifest-Version e Created-By, anche gli header
specificati nel file Manifest.txt.

La creazione del file JAR effettuata nel modo descritto consente, di fatto, di creare
un archivio auto-eseguibile, poich al suo interno vi anche una classe che contiene
un metodo main. Potremo infatti eseguire la nostra applicazione invocando linterprete
java nel modo seguente.
Shell 12.13 Esecuzione di un JAR (dalla dir C:\MY_JAVA_JARS).
java -jar HardwareAndSoftwareAPI.jar

Mostriamo di seguito come si visualizza, estrae e aggiorna un archivio JAR.


Shell 12.14 Visualizzazione di un archivio (dalla dir C:\MY_JAVA_JARS).

jar tfv HardwareAndSoftwareAPI.jar

Output 12.3 Dalla Shell 12.14.

0 Thu Nov 14 19:38:46 CET 2013 META-INF/


353 Thu Nov 14 19:38:46 CET 2013 META-INF/MANIFEST.MF
0 Thu Nov 14 19:35:28 CET 2013 com/
0 Thu Nov 14 19:03:48 CET 2013 com/pellegrinoprincipe/
0 Thu Nov 14 18:46:06 CET 2013 com/pellegrinoprincipe/hardware/
1101 Thu Nov 14 18:46:06 CET 2013 com/pellegrinoprincipe/hardware/Computer$Hardware.class
517 Thu Nov 14 18:46:06 CET 2013 com/pellegrinoprincipe/hardware/Computer.class
0 Thu Nov 14 18:51:00 CET 2013 com/pellegrinoprincipe/software/
1099 Thu Nov 14 18:51:00 CET 2013 com/pellegrinoprincipe/software/Software$Graphic.class
626 Thu Nov 14 18:51:00 CET 2013 com/pellegrinoprincipe/software/Software.class
0 Thu Nov 14 19:35:32 CET 2013 com/thp/
1250 Thu Nov 14 18:53:20 CET 2013 com/thp/ComputerClient_REV_1.class

Shell 12.15 Estrazione da un archivio di un singolo file (dalla dir C:\MY_JAVA_JARS).

jar xf HardwareAndSoftwareAPI.jar com/pellegrinoprincipe/hardware/Computer.class

NOTA
possibile estrarre tutti i file dallarchivio non indicando nessun singolo file.

Per laggiornamento dellarchivio HardwareAndSoftwareAPI.jar supponiamo di aver


creato una classe Printer posta nel package com.pellegrinoprincipe.hardware e di voler
aggiungere, nel medesimo archivio, il suo .class (ricordiamo che tale classe definita
nel file Printer.java e deve essere compilata, dalla dir C:\MY_JAVA_SOURCES, con il comando
javac -d C:\MY_JAVA_PACKAGES Printer.java).
Shell 12.16 Aggiornamento di un archivio (dalla dir C:\MY_JAVA_PACKAGES).

jar ufv c:\MY_JAVA_JARS\HardwareAndSoftwareAPI.jar com\pellegrinoprincipe\hardware\Printer.class

Il comando della Shell 12.16 aggiunge allarchivio HardwareAndSoftwareAPI.jar il file


Printer.class.

Vediamo infine come larchivio HardwareAndSoftwareAPI.jar sia referenziato nella fase


di compilazione e nella fase di esecuzione, quando il programma client
(ComputerClient_REV_1) non posto al suo interno (eliminarlo a tal fine dallarchivio
citato unitamente alla sua directory contenitrice).
Shell 12.17 Uso di un JAR in fase di compilazione (dalla dir C:\MY_JAVA_SOURCES).
javac -cp C:\MY_JAVA_JARS\HardwareAndSoftwareAPI.jar -d C:\MY_JAVA_CLASSES
ComputerClient_REV_1.java

Il comando della Shell 12.17 mostra come il flag -cp indichi nel classpath il nome
dellarchivio HardwareAndSoftwareAPI.jar e non solamente la directory che lo contiene
(C:\MY_JAVA_JARS). Lindicazione del nome dellarchivio essenziale, infatti il
compilatore inizier a cercare le librerie richieste scorrendo la struttura di directory
ivi indicata.
Shell 12.18 Uso di un JAR in fase di esecuzione (dalla dir C:\MY_JAVA_CLASSES).

java -cp .;C:\MY_JAVA_JARS\HardwareAndSoftwareAPI.jar com.thp.ComputerClient_REV_1

Il comando della Shell 12.18 mostra come anche nel caso dellinterprete si debba
indicare nel classpath il nome dellarchivio. Inoltre, per la corretta esecuzione si deve
indicare anche la directory corrente al fine di far trovare la classe principale del
programma, ovvero ComputerClient_REV_1.
Meccanismo delle estensioni
Oltre ai metodi di deployment dei package appena esposti, ne esiste anche un altro
che definito extension mechanism, o meccanismo delle estensioni (poich di fatto
estende il core delle API del JDK stesso), ed stato introdotto a partire dalla versione
1.2 di Java.
Grazie a esso possibile creare package di tipi che possono essere referenziati
senza utilizzare le procedure di impostazione del classpath.
Per utilizzare questa procedura occorre per prima cosa archiviare in un file .jar i
package che si desiderano rendere disponibili al sistema di run-time, e poi copiare il
medesimo archivio in una directory particolare denominata ext il cui path si potr
specificare tramite la propriet di sistema di Java java.ext.dirs; tale variabile, di
default, punter a un percorso come <java-home>\lib\ext, dove <java-home> si riferir alla
directory nella quale il run-time di Java sar stato installato (per esempio C:\Program
Files (x86)\Java\jdk1.8.0\jre o C:\Program Files (x86)\Java\jre8).
NOTA
Se abbiamo pi run-time di Java installati nel sistema possiamo pubblicare i package nelle
seguenti directory al fine di renderli disponibili a tutti contemporaneamente: per sistemi Windows,
%SystemRoot%\Sun\Java\lib\ext (dove %SystemRoot% pu essere, per esempio, C:\Windows); per
sistemi GNU/Linux, /usr/java/packages/lib/ext.

Vediamo ora un esempio, considerando il nostro archivio HardwareAndSoftwareAPI.jar


in cui, ricordiamo, possiamo tranquillamente eliminare il file ComputerClient_REV_1.class
che non ci servir pi in quanto larchivio medesimo non dovr pi essere auto-
eseguibile.
Copiamo larchivio HardwareAndSoftwareAPI.jar in una delle directory ext indicate
precedentemente e poi eseguiamo il file ComputerClient_REV_1 con il comando riportato
di seguito.
Shell 12.19 Esecuzione del file ComputerClient_REV_1 (dalla dir C:\MY_JAVA_CLASSES).
java com.thp.ComputerClient_REV_1
Specificatore di accesso di tipo package
Se un metodo o un dato membro di una classe non hanno uno specificatore di
accesso, per default riceveranno uno specificatore di accesso di tipo package e
saranno accessibili direttamente da altre classi appartenenti al medesimo package.
Listato 12.5 Classe Printer.

package com.pellegrinoprincipe.hardware;

public class Printer


{
String name;
public Printer(){ /* to do */ }
}

Il Listato 12.5 definisce una classe denominata Printer e una variabile di istanza
denominata name che, non avendo nessun specificatore di accesso (public, private o
protected ), avr di default quello di tipo package.
Listato 12.6 Classe Computer_REV_1.
package com.pellegrinoprincipe.hardware;

public class Computer_REV_1


{
...
Printer p = new Printer(); // oggetto di tipo Printer
public enum Hardware { MOUSE, KEYBOARD; }
...
public void setPrinterName(String name) { p.name = name; }
public String getPrinterName() { return p.name; }
}

Il Listato 12.6 mostra che la classe Computer_REV_1 tramite i metodi setPrinterName e


getPrinterName accede direttamente alla variabile name della classe Printer e, ripetiamo,
ci reso possibile poich entrambe appartengono allo stesso package:
com.pellegrinoprincipe.hardware.

Listato 12.7 Classe ComputerClient_REV_2.

...
public class ComputerClient_REV_2
{
public static void main(String[] args)
{
Computer_REV_1 c = new Computer_REV_1();
...
// impostiamo e otteniamo il nome della stampante in uso
// tramite i metodi get e set della classe Computer_REV_1
c.setPrinterName("EPSON");
System.out.println(c.getPrinterName());

// oggetto di tipo Printer


Printer p = new Printer();

// attenzione: accesso diretto non consentito perch Printer


// appartiene a un package differente
p.name = "HP"; // ERRORE
}
}
Il Listato 12.7 mostra come la classe ComputerClient_REV_2 tenti di accedere
direttamente alla variabile name della classe Printer ma non vi riesca, poich tale classe
non appartiene allo stesso package della classe Printer, ma al package com.thp.

Se invochiamo il compilatore, otterremo il seguente errore.


Errore 12.1 Dal Listato 12.7 Classe ComputerClient_REV_2.

...ComputerClient_REV_2.java:29: error: name is not public in Printer; cannot be accessed from


outside package p.name = "HP"; // ERRORE
1 error

NOTA
Ricordiamo che prima di compilare il Listato 12.7 (javac -cp c:\MY_JAVA_PACKAGES -d
c:\MY_JAVA_CLASSES ComputerClient_REV_2.java) necessario compilare il file Printer.java (javac
-d c:\MY_JAVA_PACKAGES Printer.java) e il file Computer_REV_1.java (javac -cp
c:\MY_JAVA_PACKAGES -d c:\MY_JAVA_PACKAGES Computer_REV_1.java).

Riportiamo di seguito una tabella riepilogativa che indica le modalit di accesso a


un membro denominato name di una classe denominata MyClass appartenente a un
package denominato myPackage.

Tabella 12.1 Specificatori di accesso.


Classi private no specificatore protected public
myPackage.MyClass.ex s s s s
myPackage.ExtendMyClass.ex no s s s
myPackage.OtherClass.ex no s s s
otherPackage.ExtendMyClassOtherPackage.ex no no s s
otherPackage.OtherClassOtherPackage.ex no no no s

La tabella si legge considerando che ogni valore di colonna lo specificatore che


potr essere attribuito al membro name e che ogni valore delle righe rappresentato dal
membro ex appartenente alla classe e al package indicato.

In pratica possiamo interpretare la tabella come segue:

la prima riga dice che il membro ex della classe myPackage.MyClass pu accedere al


membro name qualsiasi specificatore esso abbia;
la seconda riga dice che il membro ex della classe myPackage.ExtendMyClass (dove
ExtendMyClass una sottoclasse di MyClass) pu accedere a name solo se non private;
la terza riga dice che il membro ex della classe myPackage.OtherClass pu accedere a
name solo se esso non private;
la quarta riga dice che il membro ex della classe
otherPackage.ExtendMyClassOtherPackage (dove ExtendMyClassOtherPackage una
sottoclasse di MyClass) pu accedere a name solo se protected o public;
la quinta riga dice che il membro ex della classe otherPackage.OtherClassOtherPackage
pu accedere a name solo se public.

Possiamo sintetizzare dicendo che, nellambito di uno stesso package, una classe
pu accedere a tutti i membri di altre classi appartenenti al suo stesso package tranne
che a quelli di tipo private, e che una classe appartenente a un package differente da
quello di unaltra classe pu accedere ai membri di questultima solo se sono di tipo
public o di tipo protected, a condizione che laltra classe sia la sua superclasse.

Vediamo un esempio pratico considerando che, per le ovvie ragioni di non poter
usare lo stesso nome di variabili nello stesso scope, il membro name e il membro ex
avranno il nome cambiato.
Listato 12.8 Classe MyClass.

package myPackage;

public class MyClass


{
private int name_private;
int name_packaged;
protected int name_protected;
public int name_public;

// accesso
public int ex_name_private = name_private; // OK visibile
public int ex_name_packaged = name_packaged; // OK visibile
public int ex_name_protected = name_protected; // OK visibile
public int ex_name_public = name_public; // OK visibile
}

Listato 12.9 Classe ExtendMyClass.

package myPackage;

public class ExtendMyClass extends MyClass


{
// accesso
public int ex_name_private = name_private; // NON visibile
public int ex_name_packaged = name_packaged; // OK visibile
public int ex_name_protected = name_protected; // OK visibile
public int ex_name_public = name_public; // OK visibile
}

Listato 12.10 Classe OtherClass.

package myPackage;

public class OtherClass


{
MyClass my_class = new MyClass();

// accesso
public int ex_name_private = my_class.name_private; // NON visibile
public int ex_name_packaged = my_class.name_packaged; // OK visibile
public int ex_name_protected = my_class.name_protected; // OK visibile
public int ex_name_public = my_class.name_public; // OK visibile
}

Listato 12.11 Classe ExtendMyClassOtherPackage.


package otherPackage;

import myPackage.MyClass;

public class ExtendMyClassOtherPackage extends MyClass


{
// accesso
public int ex_name_private = name_private; // NON visibile
public int ex_name_packaged = name_packaged; // NON visibile
public int ex_name_protected = name_protected; // OK visibile
public int ex_name_public = name_public; // OK visibile
}

Listato 12.12 Classe OtherClassOtherPackage.

package otherPackage;

import myPackage.MyClass;

public class OtherClassOtherPackage


{
MyClass my_class = new MyClass();

// accesso
public int ex_name_private = my_class.name_private; // NON visibile
public int ex_name_packaged = my_class.name_packaged; // NON visibile
public int ex_name_protected = my_class.name_protected; // NON visibile
public int ex_name_public = my_class.name_public; // OK visibile
}

ATTENZIONE
Gli specificatori di accesso sin qui esaminati sono applicabili solo ai membri di una classe, mentre
per la classe stessa esiste solo lo specificatore di accesso public che rende accessibile il suo
codice da qualsiasi altra classe posta al di fuori del suo package di appartenenza. Se, invece, alla
classe non si assegna alcuno specificatore di accesso, allora il suo codice sar inaccessibile da
altre classi non appartenenti al suo package.
Capitolo 13
Annotazioni

Le annotazioni sono dei metadati, scritti secondo una particolare sintassi, che si
possono applicare durante la fase di dichiarazione (type declarations) e/o di utilizzo
(type uses) degli elementi di un programma (tipi, metodi, variabili di istanza, variabili
locali e cos via).
TERMINOLOGIA
Il termine metadato deriva dal greco meta (oltre) e dal latino data (informazioni); esso
rappresentato da una o pi informazioni che hanno lo scopo di descrivere in modo significativo o
aggiuntivo altri dati a cui il metadato stesso si riferisce. Lesempio classico il metadato
associato al file di una foto che contiene informazioni quali la data e lora dello scatto, il tempo di
esposizione e cos via.

Lo scopo delle annotazioni semplicemente quello di associare o fornire delle


informazioni agli elementi che decorano. Non impattano, dunque, sulla semantica del
programma nellambito del quale sono utilizzate.
Di fatto, questi metadati possono essere impiegati da appositi tool di processing o
dal compilatore stesso per estrarne delle informazioni utilizzabili per il rilevamento
di errori, per leliminazione dei warning, per generare del codice modificato, per
creare file XML, per generare file di configurazione ad hoc per il deployment di una
classe in un application server e cos via.
Le annotazioni, permettono, quindi, di scrivere in un programma delle
informazioni, dei commenti o qualsiasi tipo di documentazione si ritenga necessaria
in un modo standardizzato, uniforme e impiegabile in modo automatizzato dagli
strumenti di processing alluopo creati e impiegati.
APPROFONDIMENTO
Le annotazioni sono documentate nelle due specifiche JSR 175 A Metadata Facility for the Java
Programming Language e JSR 308 Annotations on Java Types. La JSR 175, pubblicata nel
2004, ha di fatto introdotto il sistema delle annotazioni utilizzabili con il linguaggio Java (release
5). La JSR 308, pubblicata con lattuale release 8 di Java ne ha invece esteso linsieme delle
locazioni ed entit annotabili, rendendole di uso pi generalizzato (per esempio, ora possibile
annotare anche lutilizzo di un tipo oppure la dichiarazione dei parametri di tipo).
Sintassi di base
Dal punto di vista di Java, unannotazione rappresenta una sorta di modificatore da
applicare a un determinato elemento del linguaggio il cui nome si riferisce al nome di
un tipo di annotazione dichiarata allo scopo. Ogni annotazione pu altres indicare
zero o pi attributi, espressi nella forma identifier = element_value, laddove element_value
associato al corrispondente identifier che rappresenta il nome di un elemento
dichiarato nel tipo di annotazione relativa.
Unannotazione pu essere categorizzata in una delle tre tipologie descritte di
seguito; di queste, le prime due, sono praticamente degli shorthand (abbreviazioni),
mentre lultima di utilizzo pi generale.

Di tipo indicatore (marker annotation), caratterizzata dallassenza di attributi. Si


utilizza scrivendone il nome preceduto dal simbolo @ (Sintassi 13.1).

Sintassi 13.1 Sintassi marker annotation.


@AnnotationName // shorthand di @AnnotationName()

A singolo elemento (single-element annotation), caratterizza dalla presenza di


un solo attributo. Si utilizza scrivendone il nome preceduto dal carattere @
seguito dallindicazione dellattributo posto tra parentesi tonde (). Inoltre, se
lidentificatore dellelemento dichiarato allinterno del tipo di annotazione
denominato come value, quando si utilizza tale tipologia di annotazione, se ne
pu omettere la scrittura e si pu indicare solo il valore (Sintassi 13.2).

Sintassi 13.2 Sintassi single-element annotation.


@AnnotationName(identifier = element_value)
@AnnotationName(element_value) // shorthand di @AnnotationName(value = element_value)

Normale (normal annotation), caratterizzata dalla presenza di una molteplicit


di attributi. Si utilizza scrivendone il nome preceduto dal carattere @ seguito
dallindicazione degli attributi, separati dal simbolo virgola (,), posti tra le
parentesi tonde () (Sintassi 13.3). bene sottolineare che se si utilizza questo
tipo di annotazione laddove si definisce, tra gli altri, anche un elemento con il
nome value, lo stesso non pu essere omesso quando si forniscono i relativi
valori come abbiamo visto per il caso dellannotazione a singolo elemento.

Sintassi 13.3 Sintassi normal annotation.


@AnnotationName(identifier = element_value, identifier2 = element_value2, ..., identifierN =
element_valueN)
Annotazioni standard per il compilatore
Esaminiamo nel seguito le annotazioni, i cui tipi sono definiti nel package java.lang,
che il linguaggio Java mette a disposizione e che sono utilizzate dal compilatore,
fondamentalmente, per intercettare degli errori e per generare o sopprimere degli
avvisi.

Annotazione @Override
Listato 13.1 Class AnnOverride.

package com.pellegrinoprincipe;

class Base
{
public void M1() {}
}

class Derived extends Base


{
@Override
public void M1() {} // qui nessun errore perch il metodo M1 presente
// nella sua classe base
}

class Derived2 extends Base


{
@Override
public void M() {} // qui errore perch il metodo M non presente
// nella sua classe base
}

public class AnnOverride


{
public static void main(String[] args) {}
}

Il Listato 13.1 esamina lannotazione @Override che, posta su di un metodo (nel


nostro caso il metodo M1 della classe Derived), indica al compilatore che esso deve
sovrascrivere un metodo della classe base estesa dalla sua classe derivata. Ci
significa che, per esempio, se poniamo tale annotazione su di un metodo (come il
metodo M della classe Derived2) di una classe derivata da una classe base che non fa
loverriding di un metodo di questultima, il compilatore ci segnaler lErrore 13.1,
che indica, per lappunto, che il metodo M deve trovarsi anche nella classe Base e che
tale metodo deve essere sovrascritto.
Errore 13.1 @Override non rispettata.
...AnnOverride.java:17: error: method does not override or implement a method from a supertype
@Override
1 error

Annotazione @Deprecated
Listato 13.2 Classe AnnDeprecated.

package com.pellegrinoprincipe;

class MyInteger
{
@Deprecated
public static int doSum(int a, int b) { return a + b; } // metodo deprecato
// sconsigliato l'utilizzo
public static int doSum(int[] ii)
{
int s = 0;
for (int i : ii)
s += i;
return s;
}
}
public class AnnDeprecated
{
public static void main(String[] args)
{
MyInteger.doSum(44, 55);
}
}

Il Listato 13.2 esamina lannotazione @Deprecated, che posta su di un metodo indica


che lo stesso deprecato e pertanto il suo utilizzo non consigliato. Nella classe
AnnDeprecated, linvocazione del metodo doSum, con il passaggio di due parametri di tipo

intero, verr rilevata dal compilatore che generer un avviso (e non un errore) in fase
di compilazione, poich nella definizione di tale metodo abbiamo utilizzato, per
lappunto, lannotazione @Deprecated.
Warning 13.1 @Deprecated non rispettata.
Note: AnnDeprecated.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

Per ottenere un dettaglio del metodo deprecato si pu compilare il file sorgente


utilizzando il flag di compilazione -Xlint:deprecation.
Shell 13.1 Compilazione con il flag -Xlint:deprecation.

javac -Xlint:deprecation AnnDeprecated.java

Output 13.1 Dalla Shell 13.1.

...AnnDeprecated.java:20: warning: [deprecation] doSum(int,int) in MyInteger has been deprecated


MyInteger.doSum(44, 55);
1 warning

Annotazione @SuppressWarnings
Listato 13.3 Classe AnnSuppressWarnings.
package com.pellegrinoprincipe;

import java.util.ArrayList;

public class AnnSuppressWarnings


{
@SuppressWarnings("unchecked")
public static void addIntoAList()
{
ArrayList l = new ArrayList();
l.add("...");
}

public static void main(String[] args)


{
addIntoAList();
}
}

Il Listato 13.3 esamina lannotazione @SuppressWarnings, che posta su di un metodo


indica che, se il metodo genera un warning, questo non deve essere visualizzato dal
compilatore. Nel nostro caso il metodo addIntoAList crea un oggetto di tipo ArrayList e
poi ne utilizza il metodo add in modo non sicuro, poich utilizza la sintassi tipica delle
liste non generiche; pertanto, senza lannotazione @SuppressWarnings("unchecked") il
compilatore genererebbe un avviso di utilizzo non sicuro dei generici.
NOTA
Il valore dellidentificatore dellannotazione esaminata stato scritto senza lindicazione del suo
nome e ci perch, lo stesso, stato denominato come value, e dunque, come gi
precedentemente detto, pu essere omesso.

Lannotazione @SupressWarnings consente di specificare anche pi valori (Listato


13.4) che possono essere scritti utilizzando la sintassi inline di inizializzazione di un
array, ovvero scrivendo gli stessi tra parentesi graffe.
La Sintassi 13.4 mostra come utilizzare unannotazione che fa uso di un array
initializer.
Sintassi 13.4 Array initializer per i valori di un elemento di unannotazione.
// qui l'identificatore denominato value e dunque implicito
@AnnotationName({element_value, element_value2, ..., element_valueN});

// qui il nome dell'identificatore dato da identifier


@AnnotationName(identifier = {element_value, element_value2, ..., element_valueN});

NOTA
Quando si fornisce un solo valore a un elemento di unannotazione che accetta come valori un
array, possibile omettere le indicazioni delle parentesi graffe.

I valori che unannotazione di tipo @SuppressWarnings pu accettare dipendono


dallimplementazione del produttore del compilatore. Tuttavia, nelle specifiche del
linguaggio sono indicati solamente i valori unchecked e deprecation, pertanto un
compilatore dovrebbe come minimo processare questi ultimi.
Per sapere quali valori riconosce il compilatore ufficiale di Java possiamo invocare
il comando javac con il flag X e leggerli dalloutput di Xlint.
Shell 13.2 Valori di @SuppressWarnings.

javac -X
Output 13.2 Dalla Shell 13.2.

-Xlint Enable recommended warnings -Xlint:{all,auxiliaryclass,cast,classfile,deprecation,dep-


ann,divzero,empty,fallthrough,finally,options,overloads,overrides,path,processing,rawtypes,serial,static,t
auxiliaryclass,-cast,-classfile,-deprecation,-dep-ann,-divzero,-empty,-fallthrough,-finally,-
options,-overloads,-overrides,-path,-processing,-rawtypes,-serial,-static,-try,-unchecked,-
varargs,none} Enable or disable specific warnings
-Xdoclint Enable recommended checks for problems in javadoc comments
-Xdoclint:(all|none|[-]<group>)[/<access>]
...

Listato 13.4 Classe AnnSuppressWarningsMultiple.

package com.pellegrinoprincipe;

public class AnnSuppressWarningsMultiple


{
@SuppressWarnings({"fallthrough", "divzero"})
public static void manyWarnings()
{
int num = 0;
switch (num)
{
case 3: System.out.println("...3");
case 5: System.out.println("...5");
case 0: System.out.println(5 / 0 + " divisione per 0");
}
}

public static void main(String[] args)


{
manyWarnings();
}
}

Il Listato 13.4 mostra come si scrive unannotazione che accetta valori multipli.
Infatti, sul metodo manyWarnings insiste lannotazione @SuppressWarnings({"fallthrough",
"divzero"}) , che indica al compilatore di non avvisare se durante la compilazione
incontra delle istruzioni case senza il relativo break (fallthrough) e se incontra una
potenziale divisione per 0 (divzero).

Pertanto, compilando il programma con il flag -Xlint, che scritto senza argomenti
indica al compilatore di generare tutti i tipi di warning, oppure con il flag -
Xlint:fallthrough,divzero , che indica al compilatore di generare solo i tipi di warning
indicati, il compilatore non ci avviser del possibile fallthrough nelle istruzioni case e
della possibile divisione per 0.

Annotazione @SafeVarargs
Prima di spiegare come si comporta lannotazione @SafeVarargs dobbiamo analizzare
i seguenti importanti concetti.

Lheap pollution (inquinamento dellheap) indica una situazione per cui


possibile che una variabile di un tipo parametrizzato contiene un riferimento a
un oggetto che non di quel tipo parametrizzato. Lheap pollution pu infatti
occorrere se si compiono operazioni con un tipo raw che genera a compile-time
un unchecked warning (Snippet 13.1) oppure se si assegna il riferimento di un
array di un tipo non-reifiable (per esempio List<String>[]) a una variabile che un
array di un suo supertipo, sia raw (per esempio Collection[]) sia non generico (per
esempio Object[]) (Listato 13.5 metodo listManipulation).

Snippet 13.1 Heap pollution con un tipo raw.

List raw_list = new ArrayList<Double>();

// in questo caso non possibile verificare, sia a compile-time sia a run-time, se il


// riferimento raw_list contiene effettivamente una lista di tipo List<String>
// c' un heap pollution perch il riferimento string_list si riferisce a un
// valore che, di fatto, di tipo ArrayList<Double> e non di tipo List<String>
List<String> string_list = raw_list; // Unchecked warning

I non-reifiable types indicano quei tipi che, a run-time, per effetto dellerasure
hanno perso le informazioni sul loro tipo corretto (si pensi a oggetti di tipo
ArrayList<Number> o ArrayList<String> che dopo la compilazione sono diventati

semplicemente di tipo ArrayList).


I reifiable types indicano quei tipi che, a run-time, conservano le informazioni
sul loro tipo effettivo (rientrano in questa categoria i tipi primitivi, i tipi non
generici, i tipi raw, gli array i cui elementi sono essi stessi reifiable e i tipi
parametrizzati laddove gli argomenti di tipo sono unbounded wildcard, come per
esempio Class<?>, Map<?, ?> e cos via).

Ci detto, dobbiamo ricordare che un metodo pu contenere, tra gli altri, anche un
parametro a lunghezza variabile (vararg) il quale, a sua volta, pu essere definito
come tipo non generico (per esempio int elements), pu essere di tipo parametrizzato
(per esempio List<String>... lists) oppure pu avere un parametro di tipo (per esempio
T array).

Se quindi il compilatore incontra un vararg non generico, lo trasforma in un array


di oggetti del tipo specificato (per esempio, int diventa int[]); se rileva un vararg di
tipo parametrizzato lo converte in un array del corrispondente tipo raw eliminando
linformazione dellargomento di tipo (per esempio, List<String>... diventa List[]); se
rileva un vararg con un parametro di tipo lo traduce in un array di oggetti di
parametri di tipo e poi lo converte in un array di oggetti del tipo indicato dallupper
bound relativo (per esempio, T tradotto in T[] e poi convertito in Object[]).

Nel caso, dunque, di utilizzo di vararg generici, essendo effettuate le


trasformazioni citate, il compilatore, si premunisce di avvisarci che un loro impiego
potrebbe causare un eventuale inquinamento dellheap.
Infatti, nelleventualit di definizione di un metodo con un vararg generico e poi di
una sua invocazione, il compilatore Java potr generare: un messaggio di unchecked
warning del tipo warning: unchecked generic array creation for varargs parameter of type; un
messaggio di unchecked warning come warning: [unchecked] Possible heap pollution from
parameterized vararg type che, per distinguerlo dal primo, viene denominato varargs
warning.
Entrambi i warning, dunque, avvisano in modo abbastanza pressante che ci
potrebbe essere la possibilit di compiere operazioni non sicure nel metodo che fa
uso del parametro a lunghezza variabile generico, con la conseguenza di incorrere in
gravi errori come, per esempio quelli che generano uneccezione di tipo
ClassCastException.

Se, tuttavia, siamo certi che il metodo non gestir impropriamente il parametro a
lunghezza variabile generico (cio che non compir operazioni unsafe), potremo
porre su di esso lannotazione @SafeVarargs per sopprimere sia il varargs warning
generato a causa della definizione del metodo sia lunchecked warning generato a
causa dellinvocazione del metodo.
Listato 13.5 Classe HeapPollution.

package com.pellegrinoprincipe;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class HeapPollution


{
// in questo metodo non c' alcun problema di utilizzo improprio di T array
// possiamo marcare il metodo come "sicuro" e non far generare warning
@SafeVarargs
public static <T> void showParamInfo(T array)
{
for (T element : array)
System.out.println(element.getClass().getName() + ": " + element);
}

// in questo metodo si compiono operazioni con List<String>... lists improprie


// e quindi non bisogna marcarlo come "sicuro"
public static void listManipulation(List<String>... lists)
{
// assegnamento perfettamente lecito, ma attenzione: questa istruzione
// pu ingenerare poi un heap pollution!
Object[] an_array = lists;

// creiamo due liste, una per contenere Double
// e l'altra per contenere Long
List<Double> l_D = Arrays.asList(12.33, 44.66, 55.66);
List<Long> l_L = Arrays.asList(100L, 1000L, 10000L);

an_array[0] = l_D;
an_array[1] = l_L;

// facciamo delle manipolazioni con lists ma otteniamo, invece,


// una ClassCastException!
String f_0_element = lists[0].get(0);
String f_1_element = lists[0].get(1);

String s_0_element = lists[1].get(0);
String s_1_element = lists[1].get(1);

/* esegue qualche operazione */
}

public static void main(String[] args)


{
// per il metodo showParamInfo
ArrayList<Integer> l_I = new ArrayList<>();
l_I.add(10);
l_I.add(20);
ArrayList<Double> l_D = new ArrayList<>();
l_D.add(55.77);
l_D.add(22.19);
showParamInfo(l_I, l_D, "XYZ", 500);

// per il metodo listManipulation


listManipulation(Arrays.asList("One, Two"), Arrays.asList("Red, Blue"));
}
}

Il Listato 13.5 definisce il metodo showParamInfo, che marchiamo con lannotazione


@SafeVarargs perch il parametro di tipo a lunghezza variabile array viene utilizzato
esclusivamente per compiere una comune operazione di lettura dei suoi elementi. Il
metodo listManipulation, invece, utilizza in modo non sicuro il tipo parametrizzato a
lunghezza variabile lists e, dunque, non lo marchiamo con la predetta annotazione.

Precisamente, la prima statement del metodo listManipulation, assegna il riferimento


lists, che dopo lerasure diventato di tipo List[], al riferimento an_array di tipo
Object[] . Questa istruzione, tuttavia, pur non generando alcun problema di
compilazione, perch il tipo lists un sottotipo del tipo an_array, pu causare un
inquinamento dellheap se per il tramite del riferimento an_array compiamo operazioni
di scrittura di tipi non corrispondenti ai tipi attesi dal riferimento lists (ricordiamo
che an_array e lists puntano alla stessa area di memoria).

Nel nostro caso, purtroppo, con le successive istruzioni, causiamo uno heap
pollution perch an_array[0] e an_array[1], e dunque lists[0] e lists[1], contengono,
rispettivamente, un riferimento a una lista di tipo List<Double> e un riferimento a una
lista di tipo List<Long>. Non contengono pi, quindi, le due liste attese di tipo
List<String> .

Da ci ne consegue che, quando successivamente cerchiamo di ottenere gli item


delle liste per il tramite del riferimento lists (per esempio lists[0].get(0)), avremo
uneccezione di tipo ClassCastException perch tali item non saranno del tipo atteso (per
esempio, lists[0].get(0) ritorner un oggetto di tipo Double e non un oggetto di tipo
String ).
Vediamo, dunque, i messaggi di warning (Warning 13.2) che si ottengono
compilando il Listato 13.5 con il consueto comando javac -Xlint:unchecked
HeapPollution.java e il risultato dellesecuzione del suo .class (Output 13.3).
Warning 13.2 HeapPollution.

...HeapPollution.java:20: warning: [unchecked] Possible heap pollution from parameterized vararg


type List<String> public static void listManipulation(List<String>... lists)
...HeapPollution.java:56: warning: [unchecked] unchecked generic array creation for varargs
parameter of type List<String>[] listManipulation(Arrays.asList("One, Two"), Arrays.asList("Red,
Blue"));
2 warnings

Output 13.3 Dal Listato 13.5 Classe HeapPollution

java.util.ArrayList: [10, 20]


java.util.ArrayList: [55.77, 22.19]
java.lang.String: XYZ
java.lang.Integer: 500
Exception in thread "main" java.lang.ClassCastException: java.lang.Double cannot be cast to
java.lang.String
at com.pellegrinoprincipe.HeapPollution.listManipulation(HeapPollution.java:35)
at com.pellegrinoprincipe.HeapPollution.main(HeapPollution.java:56)
Java Result: 1

NOTA
Le versioni 5 e 6 del compilatore di Java generano un messaggio di unchecked warning per
lutilizzo di un parametro a lunghezza variabile di tipo parametrizzato solo quando si invoca un
metodo che lo pu causare, mentre la versione 7 migliora il raggio di azione, generandolo anche
nel momento della dichiarazione. Tuttavia, se vogliamo essere avvisati solo nel momento
dellinvocazione di un metodo, possiamo usare lannotazione @SuppressWarnings({"unchecked",
"varargs"}).

Annotazione @FunctionalInterface
Listato 13.6 Interfaccia AFunctionalInterface.
package com.pellegrinoprincipe;

@FunctionalInterface
public interface AFunctionalInterface
{
public void process();
}

Il Listato 13.6 esamina lannotazione @FunctionalInterface, che posta su di un tipo


indica che il compilatore deve generare dei messaggi di errore se il tipo annotato non
uninterfaccia oppure se non soddisfa i requisiti sintattici che definiscono
uninterfaccia come funzionale. Se, per esempio, linterfaccia AFunctionalInterface
avesse avuto un altro metodo, allora il compilatore avrebbe generato il seguente
errore.
Errore 13.2 @ FunctionalInterface non rispettata.

...AFunctionalInterface.java:3: error: Unexpected @FunctionalInterface annotation


@FunctionalInterface AFunctionalInterface is not a functional interface multiple non-overriding
abstract methods found in interface AFunctionalInterface
1 error

Listato 13.7 Classe AnnFunctionalInterface

package com.pellegrinoprincipe;

public class AnnFunctionalInterface


{
public static void makeProcessing(AFunctionalInterface fi)
{
fi.process();
}

public static void main(String[] args)


{
makeProcessing(() -> System.out.println("I'm processing"));
}
}

Output 13.4 Dal Listato 13.7 Classe AnnFunctionalInterface.


I'm processing
Type annotation
Come gi brevemente anticipato, a partire dalla versione 8 di Java, le annotazioni
possono essere applicate anche quando si utilizza un tipo e con la dichiarazione dei
parametri di tipo.
Di seguito ne mostriamo alcuni esempi considerando che vale la regola generale
che una type annotation pu essere sempre scritta se appare prima del nome del tipo
che decora:

per la creazione di unistanza: MyClass c = new @Interned MyClass();


con la clausola throws: int getBooks() throws @SQLsecurity BookException { ... };
con la clausola implements: class MyList<E> implements @Readonly List<@Readonly E> { ...
};
per la dichiarazione di un parametro di tipo: class MyString<@Immutable E> { ... };
con gli argomenti di tipo di una classe parametrizzata: Map<@NonNull Integer,

@NonEmpty List<Employee>> foo_company ;
con un cast: String a_string = (@NonNull String) an_object.

NOTA
Le annotazioni utilizzate negli esempi non sono predefinite del linguaggio Java ma le abbiamo
impiegate solamente a scopo illustrativo. Appare altres opportuno precisare che una type
annotation, per essere tale, deve avere essa stessa come meta-annotazione lannotazione
@Target con la specificazione del valore ElementType.TYPE_USE o ElementType.TYPE_PARAMETER.

Le type annotation, nella sostanza, rilevano tutta la loro utilit ed efficacia quando
sono impiegate unitamente ai cosiddetti type checking framework, che rappresentano
dei moduli software sviluppati come plug-in per il compilatore Java che consentono
di estenderlo migliorandone il type system di default, che diviene cos pi robusto
e pi potente.
Un type checker, nella sostanza, permette di controllare, a compile-time, la
presenza di un particolare bug o problema potenziale e avvisa adeguatamente lo
sviluppatore permettendogli di compiere tutte quelle operazioni necessarie a
correggerlo prima che il programma sia eseguito.
Inoltre, un aspetto di rilievo che consente di scrivere realmente software meno
soggetto a errori, legato alla possibilit di utilizzare pi type checker ciascuno
deputato a rilevare un problema del suo dominio applicativo.
Per esempio, gli stessi autori del JSR 308 (Michael Ernst e Alex Buckley della
University of Washington) hanno sviluppato il Checker Framework, che dispone di
molti checker ciascuno specializzato nel rilevare o prevenire una ben determinata
tipologia di errore.
Sono infatti utilizzabili, solo per citarne alcuni: un Nullness Checker, il quale non
genera avvisi se il programma sar eseguito senza la generazione di alcuna
NullPointerException; un Interning Checker, il quale non genera errori se i test di

uguaglianza effettuati sui riferimenti con loperatore == sono senza ambiguit (in
pratica se non vi stato alcun utilizzo di == laddove era invece richiesto luso di
); un Regex Checker, il quale previene lutilizzo di espressioni regolari scritte
equals()

secondo una sintassi non corretta; un Lock Checker, il quale previene determinate
tipologie di errori legati alla programmazione concorrente e al meccanismo dei lock.
Annotazioni personalizzate
Unannotazione personalizzata un nuovo tipo di annotazione (annotation type)
creato dallo sviluppatore, atto a fornire un insieme informazioni custom. Si dichiara
utilizzando la sintassi seguente:
Sintassi 13.5 Annotazione custom.

public @interface AnnotationName


{
type element1();
type element2();
...
}

dove si utilizza la keyword interface con un prefisso indicato dal simbolo @ (at,
come in annotation type) e poi si dichiarano gli elementi come metodi senza
parametri formali e con un tipo di ritorno. Occorre tenere presente che le annotazioni
personalizzate:

non possono avere superclassi esplicite;


i metodi definiti possono avere solamente i seguenti tipi di ritorno: boolean, char,
, , , , , , ,
byte short int long float double String Class , un tipo enum e un tipo annotazione.
altres possibile ritornare un tipo array il cui tipo degli elementi di uno dei tipi
indicati;
non possono avere una clausola throws;
se si dichiarano del tipo a singolo elemento, convenzione definirle con un solo
metodo denominato value che pu anche essere scritto indicando come valore di
ritorno un array del tipo desiderato (per esempio String[] value());
non possono avere riferimenti a se stesse (ovvero non possono contenere un
metodo che ha come tipo di ritorno se stesse) oppure riferimenti circolari
(ovvero, data unannotazione denominata A, essa non pu contenere un metodo
che ha come tipo di ritorno unannotazione denominata B che ha a sua volta un
metodo che ha come tipo di ritorno lannotazione A).

Listato 13.8 Annotazione WorkToDo.

package com.pellegrinoprincipe;

public @interface WorkToDo


{
String msg();
String start_date();
String developer();
int uid() default 0;
}
Il Listato 13.8 mostra la creazione di unannotazione custom denominata WorkToDo
con vari elementi. Tra questi, lelemento denominato uid ha la clausola default, che
permette di assegnargli il valore 0 se tale identificatore non viene indicato quando si
utilizza lannotazione.
Decompilato 13.1 File WorkToDo.class.

import java.lang.annotation.Annotation;

public interface WorkToDo extends Annotation


{
public abstract String msg();
public abstract String start_date();
public abstract String developer();
public abstract int uid();
}

Il Decompilato 13.1 mostra chiaramente che unannotazione di tipo custom altro


non che uninterfaccia che estende linterfaccia Annotation e che ha tutti metodi
pubblici e astratti.
Listato 13.9 Classe AnnCustom.
package com.pellegrinoprincipe;

public class AnnCustom


{
@WorkToDo (developer = "Pellegrino Principe",
msg = "Inizio calcolo somme",
start_date = "05/01/2014")
public static void calculator(){ System.out.println("DONE!"); }

public static void main(String[] args)


{
calculator();
}
}

Output 13.5 Dal Listato 13.9 Classe AnnCustom.

DONE!

Il Listato 13.9 evidenzia come lannotazione custom venga usata allo stesso modo
delle annotazioni predefinite del linguaggio esaminate precedentemente.
Annotare le annotazioni
Quando si dichiarano delle annotazioni personalizzate, si pu avere la necessit di
marcare le stesse annotazioni con altre che consentono di specificare particolari tipi
di informazioni. Nel package java.lang.annotation sono definite le seguenti meta-
annotations.

@Target, con cui si specifica su quale elemento del programma lannotazione


insiste. Si possono utilizzare i seguenti valori derivanti dallenumerazione
java.lang.annotation.ElmentType: ANNOTATION_TYPE per un tipo annotazione; CONSTRUCTOR

per i costruttori; FIELD per le variabili di istanza e le costanti di un enum;


LOCAL_VARIABLE per le variabili locali o la clausola catch; METHOD per i metodi (ma non
per i costruttori); PACKAGE per i package; PARAMETER per i parametri formali di un
metodo; TYPE per Class, interface (inclusa quella per la definizione di un tipo
annotazione) o enum; TYPE_PARAMETER per un parametro di tipo, TYPE_USE, per luso di
un tipo.
@Retention , con cui si specifica quando disponibile lannotazione e dove essa
memorizzata. Si possono utilizzare i seguenti valori derivanti dallenumerazione
java.lang.annotation.RetentionPolicy: SOURCE, con cui si indica che lannotazione deve

essere presente solo nel codice sorgente e sar scartata dal compilatore; CLASS,
con cui si indica che lannotazione deve essere compilata allinterno del file
.class e sar scartata dalla virtual machine; RUNTIME, con cui si indica che

lannotazione deve essere compilata allinterno del file .class e non sar scartata
dalla virtual machine che la render disponibile a run-time;
@Documented, con cui si indica che lannotazione deve essere documentata da tool

di generazione e di documentazione del codice, come javadoc o simili, al fine di


renderla parte dellAPI di documentazione dellelemento su cui agisce. Per
utilizzare questa annotazione dobbiamo specificare, per lelemento su cui
insiste, anche il metadato @Retention(RetentionPolicy.RUNTIME), poich un tool come
javadoc ottiene le informazioni per produrre documentazione da un file .class
caricato dalla virtual machine.
@Inherited, con cui si indica che lannotazione deve essere ereditata da classi che

ereditano dalla classe dove stata applicata lannotazione stessa. Ci significa


che, se applichiamo unannotazione custom che ha come metadato @Inherited su
di una classe base, allora una classe derivata da questa erediter
automaticamente la stessa annotazione.
@Repeatable, con cui si indica che lannotazione pu essere ripetuta pi volte
sullelemento che decora.
@Native, con cui si indica che lelemento marcato definisce un valore costante che

pu essere referenziato da codice nativo.

Listato 13.10 Annotazione WorkToDo_REV_1.


package com.pellegrinoprincipe;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// metadati
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface WorkToDo_REV_1 { ... }

Il Listato 13.10 crea lannotazione custom WorkToDo_REV_1 applicandole il metadato


@Target(ElementType.METHOD) per indicare che annoter dei metodi e il metadato
@Retention(RetentionPolicy.CLASS) per indicare che sar memorizzata nel file .class della
classe contenente il metodo annotato.
Annotazioni ripetibili
Un altro interessante miglioramento al sistema delle annotazioni introdotto a
partire dalla versione 8 di Java riguarda la possibilit di decorare pi volte con lo
stesso tipo di annotazione un elemento di un programma.
Ricordiamo che prima dellavvento delle repeating annotations, si usava aggirare
tale limitazione mediante un escamotage che consisteva nel dichiarare
unannotazione, diciamo A, che conteneva gli elementi desiderati e poi unaltra
annotazione, diciamo B, che conteneva un elemento che ritornava un array del tipo di
annotazione da ripetere, ossia A. Successivamente si decorava lelemento di interesse
utilizzando lannotazione B e specificando, in modo ripetuto, come valori le
annotazioni A ciascuna con lindicazione dei propri valori (Snippet 13.2).
Snippet 13.2 Annotazioni ripetibili prima di Java 8.

// dichiarazione delle annotazioni


public @interface Author { String value(); } // Annotazione A

public @interface Authors { Author[] value(); } // Annotazione B che ritorna A[]

// utilizzo a ripetizione dell'annotazione A per il "tramite" di B


@Authors({@Author("Pellegrino Principe"), @Author("Silvio Rossi")})
public void foo() {}

Con Java 8, invece, per marcare unannotazione come ripetibile, sufficiente


utilizzare la meta-annotazione @Repeatable alla quale passare come valore il .class del
tipo di annotazione che funger da contenitore di tale annotazione ripetuta. Questa
annotazione container deve essere dichiarata con un elemento denominato value che
deve ritornare un array dello stesso tipo dellannotazione da ripetere. Dopo di ci,
lannotazione ripetibile pu essere apposta, a ripetizione, sullelemento del
programma da decorare (Snippet 13.3).
Snippet 13.3 Annotazioni ripetibili con Java 8.
// dichiarazione delle annotazioni
public @interface Authors { Author[] value(); }

@Repeatable(Authors.class)
public @interface Author { String value(); }

// utilizzo a ripetizione dell'annotazione Author


@Author("Pellegrino Principe")
@Author("Silvio Rossi")
public void foo() {}

APPROFONDIMENTO
interessante rilevare come, se proviamo a vedere il decompilato di una classe che fa uso di
unannotazione ripetibile, la stessa venga sostituita dal compilatore con lannotazione container
che contiene come valori le annotazioni ripetute. In effetti, il compilatore ha adottato lo stesso
escamotage illustrato precedentemente dallo Snippet 13.2 e che viene tecnicamente definito
container pattern. Ritornando allo Snippet 13.3, le due annotazioni ripetute sono state sostituite
dal compilatore con la seguente annotazione: @Authors(value={@Author(value="Pellegrino
Principe"), @Author(value="Silvio Rossi")}).
Processare le annotazioni custom
Le annotazioni di tipo custom possono essere lette e interpretate (processate) a
livello di file di codice sorgente, di file .class (nel byte-code) oppure a run-time nella
virtual machine con la reflection.

Processing a livello di codice sorgente


Il processing a livello di file di codice sorgente pu essere effettuato utilizzando le
API definite nella JSR 269 e denominate Pluggable Annotation Processing. Vediamo
un esempio che processa lannotazione WorkToDo_REV_1 a partire dal codice sorgente
dove stata impiegata.
Listato 13.11 Class AnnCustom_REV_1.
package com.pellegrinoprincipe;

public class AnnCustom_REV_1


{
@WorkToDo_REV_1(developer = "Pellegrino Principe",
msg = "Inizio calcolo somme",
start_date = "05/01/2014")
public static void calculator(){ System.out.println("DONE!"); }
...
}

Listato 13.12 Classe AnnCustomProcessor.


package com.pellegrinoprincipe;
...
@SupportedAnnotationTypes( { "com.pellegrinoprincipe.WorkToDo_REV_1"} )
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AnnCustomProcessor extends AbstractProcessor
{
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
for (Element elems : roundEnv.getElementsAnnotatedWith(WorkToDo_REV_1.class))
{
// metodo dell'annotazione
System.out.println("METODO ANNOTATO: " + elems.toString());
WorkToDo_REV_1 wtd = elems.getAnnotation(WorkToDo_REV_1.class);

// output dati dell'annotazione


System.out.println("Sviluppatore: " + wtd.developer());
System.out.println("Messaggio: " + wtd.msg());
System.out.println("Data inizio: " + wtd.start_date());
System.out.println("ID: " + wtd.uid());
}
return true;
}
}

Il Listato 13.11 applica al metodo calculator lannotazione @WorkToDo_REV_1 (Listato


13.10), mentre il Listato 13.12 scrive un processore che sar in grado di leggere la
nostra annotazione custom.
In dettaglio, per scrivere un processore di annotazioni dovremo effettuare i
seguenti passi.

1. Importare i tipi AbstractProcessor, RoundEnvironment, SupportedAnnotationTypes e


SupportedSourceVersion dal package javax.annotation.processing.
2. Importare i tipi SourceVersion, Element e TypeElement dal package java.lang.model.
3. Annotare la classe processore con le seguenti annotazioni:
@SupportedAnnotationTypes, che indica quali tipi di annotazioni processeremo (nel

nostro caso indichiamo di processare solamente il tipo WorkToDo_REV_1; per


processare tutte le annotazioni avremmo potuto scrivere lannotazione
utilizzando il carattere wildcard come in @SupportedAnnotationTypes({"*"});
@SupportedSourceVersion , che indica la versione del linguaggio Java del file
sorgente).
4. Estendere la classe AbstractProcessor e sovrascriverne il metodo process con i
seguenti parametri: Set<? extends TypeElement> annotations, che conterr un insieme
di tutte le annotazioni che processeremo; RoundEnvironment roundEnv, che
rappresenter lambiente di processing.
5. Implementare, secondo le nostre esigenze, il metodo process. Nel nostro caso
otteniamo dallambiente di processing corrente (metodo getElementsAnnotatedWith)
tutti gli elementi annotati con il tipo WorkToDo_REV_1 e poi su ogni oggetto elemento
eventualmente trovato invochiamo il metodo getAnnotation, che ritorna unistanza
di unannotazione (che per noi, ripetiamo, rappresentata dallinterfaccia
WorkToDo_REV_1) della quale utilizziamo i relativi metodi per ottenere i valori di

interesse. Dobbiamo precisare che per semplicit abbiamo scritto staticamente il


nome dellannotazione da processare, ma ovviamente avremmo potuto ottenere
lo stesso risultato scorrendo linsieme delle annotazioni (annotations) e
determinando dinamicamente, con opportuni controlli, i valori che ci
interessavano.
6. Specificare per il metodo process un valore di ritorno true se abbiamo la
ownership delle annotazioni che processiamo e false nel caso contrario.

DETTAGLIO
Avere la ownership di unannotazione durante la fase di processing significa che lannotazione in
questione non potr essere utilizzata in successione da altri processori. Ci previene, pertanto, la
possibilit di invocare processori multipli durante la fase di compilazione quando utilizziamo il flag
-processor Processor1, Processor2, ..., ProcessorN.
Vediamo ora come impiegare il processore descritto, ricordando di compilare,
prima del suo utilizzo e dalla dir C:\MY_JAVA_SOURCES le classi WorkToDo_REV_1 (javac -d
C:\MY_JAVA_CLASSES WorkToDo_REV_1.java ) e AnnCustomProcessor (javac -cp C:\MY_JAVA_CLASSES -d
C:\MY_JAVA_CLASSES AnnCustomProcessor.java ).
Shell 13.3 Utilizzo del processore (dalla dir C:\MY_JAVA_CLASSES).

javac -d C:\MY_JAVA_CLASSES -processor com.pellegrinoprincipe.AnnCustomProcessor


C:\MY_JAVA_SOURCES\AnnCustom_REV_1.java

Il comando della Shell 13.3 mostra che per utilizzare un processore di annotazioni
dobbiamo avvalerci del compilatore javac e del flag -processor seguito dal nome della
classe processore, che processer il file sorgente indicato di seguito.
Nel nostro caso lavvio del comando utilizzer la classe processore
AnnCustomProcessor invocandone il metodo process, a cui saranno passate le annotazioni

trovate nel file AnnCustom_REV_1.java e le relative informazioni di processing.


Output 13.6 Dalla Shell 13.3.
METODO ANNOTATO: calculator()
Sviluppatore: Pellegrino Principe
Messaggio: Inizio calcolo somme
Data inizio: 05/01/2014
ID: 0

Processing a run-time
Vediamo nellesempio seguente come si possono processare le annotazioni a run-
time utilizzando lAPI Java Reflection.
Listato 13.13 Annotazione WorkToDo_REV_2.
...
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WorkToDo_REV_2
{
...
}

Il Listato 13.13 mostra che, come primo passo, dobbiamo cambiare la policy di
retention impostando il valore dellannotazione @Retention a RetentionPolicy.RUNTIME in
modo che la stessa sia resa disponibile dalla JVM durante lesecuzione del nostro
programma.
Listato 13.14 Classe AnnCustom_REV_2.

package com.pellegrinoprincipe;

public class AnnCustom_REV_2


{
@WorkToDo_REV_2(developer = "Pellegrino Principe",
msg = "Inizio calcolo somme",
start_date = "05/01/2014")
public static void calculator(){ System.out.println("DONE!"); }
...
}

Listato 13.15 Annotazione AnnCustomProcessorRunTime.

package com.pellegrinoprincipe;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

public class AnnCustomProcessorRunTime


{
public static void main(String args[]) throws NoSuchMethodException
{
// classe dove presente l'annotazione
Class<AnnCustom_REV_2> class_obj = AnnCustom_REV_2.class;
Method[] ms = class_obj.getMethods(); // metodi della classe

for (Method m : ms)


{
Annotation[] method_annotations = m.getAnnotations(); // ottengo l'annotazione
// del metodo

if (method_annotations.length > 0)
{
// metodo dell'annotazione
System.out.println("METODO ANNOTATO: " + m.getName());
for (Annotation ann : method_annotations)
{
if (ann instanceof WorkToDo_REV_2) // stampo i valori dell'annotazione
{
WorkToDo_REV_2 wtd = (WorkToDo_REV_2) ann;
System.out.println("Sviluppatore: " + wtd.developer());
System.out.println("Messaggio: " + wtd.msg());
System.out.println("Data inizio: " + wtd.start_date());
System.out.println("ID: " + wtd.uid());
}
}
}
}
}
}

Il Listato 13.14 applica al metodo calculator lannotazione revisionata, mentre il


Listato 13.15 usa le API della reflection per ottenere a run-time, data la classe
AnnCustom_REV_2, i suoi metodi, quindi per ogni metodo verifica se ha delle annotazioni:

se tale verifica ha esito positivo, allora ne ottiene delle informazioni e le mostra in


output.
Vediamo ora come utilizzare il processore descritto, ricordando di compilare prima
del suo utilizzo e dalla dir C:\MY_JAVA_SOURCES le classi WorkToDo_REV_2 (javac -d
C:\MY_JAVA_CLASSES WorkToDo_REV_2.java ), AnnCustom_REV_2 (javac -cp C:\MY_JAVA_CLASSES -d
C:\MY_JAVA_CLASSES AnnCustom_REV_2.java ) e AnnCustomProcessorRunTime.java (javac -cp
C:\MY_JAVA_CLASSES -d C:\MY_JAVA_CLASSES AnnCustomProcessorRunTime.java ).
Shell 13.4 Utilizzo del processore (dalla dir C:\MY_JAVA_CLASSES).

java com.pellegrinoprincipe.AnnCustomProcessorRunTime

Output 13.7 Dalla Shell 13.4.


METODO ANNOTATO: calculator
Sviluppatore: Pellegrino Principe
Messaggio: Inizio calcolo somme
Data inizio: 05/01/2014
ID: 0

Il comando della Shell 13.4 mostra che lutilizzo di un processore di annotazioni


avviene eseguendo a run-time (con il comando java) il programma che lo contiene, e
ci differisce ovviamente dallutilizzo di un processore come quello mostrato in
precedenza, che processa delle annotazioni a livello di codice sorgente (con il
comando javac).

In conclusione possiamo affermare che:

un processore che agisce a livello di codice sorgente viene, il pi delle volte,


implementato per generare nuovo codice sorgente a partire dalle informazioni
contenute nelle annotazioni del file sorgente processato; generalmente viene
indicato anche come generatore o tool generico;
un processore che agisce a livello di byte-code viene sovente implementato per
generare un file .class nuovo a partire dal file .class contenente le annotazioni da
processare; viene indicato anche come modificatore di byte-code o come tool
specifico;
un processore che agisce a run-time viene implementato per eseguire delle
procedure sul codice che ha la necessit di essere manipolato quando in
esecuzione, come pu essere, per esempio, un tool di testing; viene indicato
anche come tool di introspezione.
Capitolo 14
Documentazione del codice sorgente

Java mette a disposizione un tool, javadoc, che consente di generare file HTML
contenenti informazioni di documentazione sul codice sorgente delle applicazioni.
Per documentare il codice sorgente dobbiamo utilizzare dei blocchi di commento
(documentation comments), delimitati dai caratteri /** e */, al cui interno inseriamo le
informazioni desiderate, che possono essere costituite da semplice testo, da tag
HTML e da tag speciali riconosciuti solamente da javadoc. Questi blocchi di
commento sono inseriti nel codice sorgente subito prima della dichiarazione di una
classe, di uninterfaccia, di dati membro e di metodi.
Documentare una classe
Riportiamo nel seguito due esempi che mostrano come si scrivono informazioni di
documentazione.
Listato 14.1 Classe Man_REV_2.

package com.pellegrinoprincipe;

/**
* <p><b>Classe</b> per la gestione di un generico uomo</p>
*
* @author Pellegrino ~thp~ Principe
* @see com.pellegrinoprincipe.Time_REV_7
* @version 1.0
*/
public class Man_REV_2
{
/**
* Indica il cognome di un uomo
*/
private String cognome;
/**
* Indica il nome di un uomo
*/
private String nome;
/**
* Indica un oggetto <code>Time_REV_7</code>
*/
private Time_REV_7 time_to_work;

/**
* Crea un oggetto di tipo Man_REV_2
*
* @param c indica il cognome
* @param n indica il nome
* @param o indica le ore di inizio lavoro
* @throws Exception se l'orario superiore alle 9
*/
public Man_REV_2(String c, String n, int o) throws Exception
{
cognome = c;
nome = n;

if (o > 9)
throw new Exception("Attenzione il lavoro inizia prima delle 9!");
else
time_to_work = new Time_REV_7(o);
}

/**
* Ritorna una rappresentazione leggibile di un oggetto Man_REV_2
*
* @return una <code>String</code> che rappresenta un Man_REV_2
*/
public String toString()
{
return cognome + " " + nome + " va a lavorare alle ore: " + time_to_work.getOra();
}
}

Nel Listato 14.1, partendo dal commento posto sul costrutto di classe, vediamo
subito che abbiamo usato sia comuni tag HTML, per esempio il tag <p> e il tag <b>, sia
tag riconosciuti esplicitamente dal tool javadoc, quelli preceduti dal carattere @.
In dettaglio abbiamo usato i seguenti tag: @author, che indica la sezione Author e
descrive lautore della classe; @see, che indica la sezione See Also e descrive altre
classi correlate a questa classe, creando anche un collegamento verso di esse; @version,
che indica la sezione Version e descrive il numero di versione del programma.
Per i commenti posti sul costruttore e sul metodo toString abbiamo usato i tag:
@param, che indica la sezione Parameters e descrive i parametri del metodo; @throws,
che indica la sezione Throws e descrive le eccezioni che un metodo pu lanciare;
@return, che indica la sezione Returns e descrive il tipo di ritorno di un metodo.

Listato 14.2 Classe Time_REV_7.

package com.pellegrinoprincipe;

/**
* <p><b>Classe</b> per la gestione di un Time_REV_7</p>
* @author Pellegrino ~thp~ Principe
*
* @version 1.0
*/
public class Time_REV_7
{
...
/**
* Imposta un orario
*
* @param o indica l'ora
* @param m indica i minuti
* @param s indica i secondi
*
* @see com.pellegrinoprincipe.Time_REV_7#setOra
* @see Time_REV_7#setMinuti
* @see #setSecondi
*/
public void setTime(int o, int m, int s)
{
setOra(o);
setMinuti(m);
setSecondi(s);
}
...
}

Il Listato 14.2 usa gli stessi tag del Listato 14.1, ma mostra come il tag @see possa
essere utilizzato per referenziare non solo altre classi, ma anche metodi e dati
membro sia della propria classe, sia di altre classi. In particolare il metodo setTime, nel
suo blocco di documentazione, utilizza il tag @see per creare dei collegamenti verso:

il metodo setOra, utilizzando una sintassi completa di qualificazione della sua


classe di appartenenza;
il metodo setMinuti, utilizzando una sintassi che indica solamente il nome della
sua classe di appartenenza;
il metodo setSecondi, utilizzando una sintassi senza qualificazione della classe di
appartenenza. In questultimo caso il tool javadoc cercher il metodo a partire
dalla classe corrente e poi nelle eventuali superclassi, package e altre classi
importate.

ATTENZIONE
Il tag @see utilizza il simbolo # invece del simbolo punto (.) per referenziare i metodi o i dati
membro appartenenti a un tipo.
Altri tag
Elenchiamo di seguito altri tag utilizzabili per documentare il codice sorgente.


@deprecated deprecated-text consente di indicare che un elemento (metodo, classe e
cos via) deprecato e il suo utilizzo scoraggiato poich, per esempio, il
medesimo stato sostituito con uno pi recente. Notiamo che tale tag pu essere
abbandonato utilizzando lannotazione @Deprecated.
,
@serial field-description | include | exclude @serialField field-name field-type field-

description e @serialData data-description sono applicabili su elementi di una classe


per documentare stati di serializzazione.
@since since-text consente di indicare che un elemento (per esempio un metodo)

gi presente da una determinata versione.

Snippet 14.1 Tag @since.


/**
* doStuff
*
* @since 1.0
*/
public void doStuff() {}

@exception class-name description applicabile su un metodo per indicare quale


eccezione pu generare. sinonimo del tag @throws.
{@code text} permette di scrivere testo formattato con lo stesso font del tag HTML
<code> , ma senza interpretare il testo stesso come codice HTML e consentendo di
scrivere anche i caratteri < e >.

Snippet 14.2 Tag {@code}.


/**
* doStuff {@code <doStuff> method}
*
* @since 1.0
*/
public void doStuff() {}

{@link package.class#member label} permette di inserire un hyperlink. simile al tag


@see, con la differenza che il collegamento viene posto direttamente inline dove il
medesimo tag stato definito, evitando cos la creazione di una sezione See
Also.

Snippet 14.3 Tag {@link}.


/**
* doStuff {@code metodo <doStuff>}
*
* @since 1.0 Nuova versione {@link #doStuff(int a) }
*/
public void doStuff() {}

{@linkplain package.class#member label} simile al tag {@link}, ma il testo


rappresentato senza lapplicazione di un font di tipo code.
{@literal text} consente di scrivere del testo di documentazione senza
interpretarlo come codice HTML e consentendo di scrivere anche i caratteri < e
>.
{@value package.class#field} consente di far visualizzare il valore di un dato
membro costante.

Snippet 14.4 Tag {@value}.

/**
* __do Valore costante: {@value}
*/
private final int __do = 10;

{@docRoot} rappresenta il path relativo della directory corrente dove stata creata
la documentazione. In pratica contiene un riferimento alla directory dove si
trova il file index.html della documentazione.

Snippet 14.5 Tag {@docRoot}.

/**
* See the Copyright image for the following image
* {@docRoot}/img/copyright.png
*/
private String trash_image = "trash.png";

{@inheritDoc} consente di ereditare (copiare) la documentazione gi scritta per un


metodo quando lo stesso sovrascritto.

Snippet 14.6 Tag {@inheritDoc}.

/**
* {@inheritDoc}
*/
@Override
public void bar() {}

Il Listato 14.3 mostra unimplementazione di alcuni dei tag descritti in precedenza.


Listato 14.3 Classe OtherTags.

package com.pellegrinoprincipe;

import java.io.IOException;

/**
* <p><b>Classe</b> OtherDocumentation</p>
*
* @author Pellegrino ~thp~ Principe
* @version 1.0
*/
class OtherDocumentation
{
/**
* un metodo qualsiasi
*/
public void bar() {}
}

/**
* <p><b>Classe</b> OtherTags</p>
*
* @author Pellegrino ~thp~ Principe
* @version 1.1
*/
public class OtherTags extends OtherDocumentation
{
/**
* __do Valore costante: {@value}
*/
private final int __do = 10;

/**
* Copyright image for the following image {@docRoot}/img/copyright.png
*/
private String trash_image = "trash.png";

/**
* @deprecated
* metodo foo
* @param g indica un intero
*/
public void foo(int g) {}

/**
* doStuff {@code metodo <doStuff>}
* @since 1.0 Nuova versione {@link #doStuff(int a) }
*/
public void doStuff(){}

/**
* doStuff {@code <doStuff> new version method}
* @param a indica un intero
* @since 1.1
*/
public void doStuff(int a) {}

/**
* metodo per la creazione di un file
* @throws IOException se il file non stato creato correttamente
*/
public void fileCreation() throws IOException {}

/**
* {@inheritDoc}
*/
@Override
public void bar(){}
}
Generare la documentazione
Dopo aver scritto i commenti di documentazione, per generare i file .html che li
descrivono si utilizza il tool javadoc con la seguente sintassi:
Sintassi 14.1 Sintassi di javadoc.

javadoc [options] [packagenames] [sourcefiles] [@files]

dove options indica le opzioni del tool; packagesnames indica se vi sono package da
documentare; sourcefiles indica se vi sono file di codice sorgente da documentare;
@files indica file che contengono le opzioni di javadoc, i nomi dei package e i nomi dei
file sorgente da far processare al tool.
Nel caso delle classi dei Listati 14.1 e 14.2 si genera la relativa documentazione
utilizzando i seguenti comandi.
Shell 14.1 Esecuzione di javadoc (dalla dir C:\MY_JAVA_SOURCES).

javadoc -d C:\MY_JAVA_DOCUMENTATION\MyApp -link http://download.java.net/jdk8/docs/api/ -author


-private Time_REV_7.java Man_REV_2.java

Per il Listato 14.3 i comandi saranno i seguenti.


Shell 14.2 Esecuzione di javadoc (dalla dir C:\MY_JAVA_SOURCES).

javadoc -d C:\MY_JAVA_DOCUMENTATION\MyAppOtherTags -link http://download.java.net/jdk8/docs/api/


-author -private OtherTags.java

I comandi impartiti nelle Shell 14.1 e 14.2 mostrano lutilizzo dei seguenti flag: -d
indica il percorso dove verr creata la documentazione HTML; -link indica di
collegare anche la documentazione ufficiale di Oracle del linguaggio Java; -author
indica di processare il tag @author; -private indica di documentare anche i membri
private .

Dopo lesecuzione dei comandi, il tool creer le directory MyApp e MyAppOtherTags


(allinterno della directory C:\MY_JAVA_DOCUMENTATION) con allinterno una serie di file e
directory che rappresenteranno la struttura fisica sul filesystem della documentazione
generata. In particolare noteremo la presenza del file index.html, aprendo il quale
potremo visionare le pagine di informazione della documentazione creata (Figure
14.1 e 14.2).
Figura 14.1 Pagina iniziale della nostra documentazione per MyApp.

Figura 14.2 Pagina iniziale della nostra documentazione per MyAppOtherTags.


Capitolo 15
Caratteri e stringhe

I caratteri e le stringhe sono senza dubbio i mattoni fondamentali per iniziare a


scrivere programmi per la manipolazione di testi. Un carattere rappresentato da un
simbolo racchiuso tra apici singoli (') che ha associato un valore numerico intero.
Questo valore numerico definito in una tabella di caratteri, comunemente
conosciuta come tabella ASCII, che oggi un sottoinsieme del set di caratteri
codificato secondo le specifiche dello standard Unicode. Cos, per esempio, la
rappresentazione del carattere A si ottiene scrivendo 'A' e avr il valore 65 come
numero intero decimale equivalente. In Java un carattere utilizzabile sia come tipo
primitivo, attraverso il tipo char, sia come oggetto attraverso la classe Character.

Una stringa, invece, rappresentata da una sequenza di caratteri racchiusi tra doppi
apici ("). In Java una stringa utilizzabile come oggetto, principalmente, attraverso la
classe String.
ATTENZIONE
Un oggetto di tipo String immutabile. Ci significa che qualsiasi operazione tenteremo di fare
sul suo contenuto generer una nuova stringa con il risultato della predetta operazione, senza
pertanto modificare il contenuto della stringa originale.
La classe Character
La classe Character incapsula in un oggetto un valore che rappresenta un carattere.
Essa fa parte del package java.lang, i cui tipi sono automaticamente importati. Un
oggetto di tipo Character pu essere creato sia attraverso il costruttore della classe
Character , che accetta come argomento il carattere da incapsulare, sia assegnando
direttamente il valore del carattere a un riferimento di tipo Character.
Snippet 15.1 Creazione di un oggetto di tipo Character.

Character c = new Character('A'); // istanza di Character


Character a_c = 'Z'; // autoboxing
char c_c = a_c; // autounboxing

Alcuni metodi del tipo Character


public int compareTo(Character anotherCharacter) confronta in modo lessicografico il
valore numerico del carattere delloggetto con il valore numerico del carattere
anotherCharacter. Ritorna il valore 0 se i valori dei caratteri sono uguali, un valore

minore di 0 se il valore del carattere minore del valore di anotherCharacter e un


valore maggiore di 0 se il valore del carattere maggiore del valore di
anotherCharacter . Lo Snippet 15.2 mostra che il metodo ha ritornato il valore 1 a
indicare che il carattere A minore del carattere B. Infatti il carattere A ha il valore
ASCII 65, che si trova a una posizione in meno dal valore ACII 66 del carattere B.
Se avessimo comparato il carattere A con il carattere E, che ha il valore ACII 69, il
risultato sarebbe stato 4.

Snippet 15.2 compareTo.


Character c = new Character('A'); // valore ASCII 65
Character d = new Character('B'); // valore ASCII 66
int n = c.compareTo(d); // 1

public static int digit(char ch, int radix) ritorna il carattere ch convertito in un
intero secondo la base numerica espressa da radix. Se non stato possibile
eseguire la conversione, viene ritornato il valore 1.

Snippet 15.3 digit.


Character c = new Character('A');
Character d = new Character('4');
int n1 = Character.digit(c, 16); // 10. A convertibile perch in esadecimale
// equivale a 10
int n2 = Character.digit(c, 10); // 1. A non convertibile. Valori permessi
// da 0 a 9 (base 10)
int n3 = Character.digit(d, 2); // 1. 4 non convertibile. Valori permessi
// 0 e 1 (base 2)
public static char forDigit(int digit, int radix) ritorna come carattere il valore
numerico di digit nella base numerica espressa da radix.

Snippet 15.4 forDigit.

char a1 = Character.forDigit(6, 10); // 6


char a2 = Character.forDigit(13, 16); // d

public boolean equals(Object obj) ritorna true se il carattere uguale al carattere


contenuto in obj.

Snippet 15.5 equals.


Character c = new Character('A');
Character d = new Character('A');
boolean b = c.equals(d); // true

ATTENZIONE
Leguaglianza dei contenuti degli oggetti Character deve essere verificata con il metodo equals e
non con loperatore di uguaglianza ==, perch questultimo, nel caso degli oggetti, confronter
luguaglianza tra riferimenti. Anche se due oggetti carattere hanno lo stesso carattere, non
saranno tuttavia uguali perch le loro variabili conterranno un differente valore del riferimento
allindirizzo di memoria del rispettivo oggetto puntato.

public static boolean isDigit(char ch) verifica se il carattere ch un numero.

Snippet 15.6 isDigit.

Character d = new Character('A');


boolean n = Character.isDigit(d); // false

public static boolean isLetter(char ch) verifica se il carattere ch una lettera.

Snippet 15.7 isLetter.

Character d = new Character('4');


boolean b = Character.isLetter(d); // false

public static boolean isJavaIdentifierStart(char ch) verifica se il carattere ch pu


essere usato come primo carattere di un identificatore Java.

Snippet 15.8 isJavaIdentifierStart.

boolean b1 = Character.isJavaIdentifierStart(''); // true


boolean b2 = Character.isJavaIdentifierStart('['); // false

public static boolean isJavaIdentifierPart(char ch) verifica se il carattere ch pu


essere usato allinterno di un identificatore Java.

Snippet 15.9 isJavaIdentifierPart.

boolean b1 = Character.isJavaIdentifierPart('5'); // true


boolean b2 = Character.isJavaIdentifierPart(''); // false
converte il carattere ch in minuscolo.
public static char toLowerCase(char ch)

Snippet 15.10 toLowerCase.


Character c = new Character('A');
char c2 = Character.toLowerCase(c); // a

converte il carattere ch in maiuscolo.


public static char toUpperCase(char ch)

Snippet 15.11 toUpperCase.

Character c = new Character('a');


char c2 = Character.toUpperCase(c); // A
La classe String
Una sequenza di caratteri pu essere incapsulata in un oggetto istanza della classe
String, che appartiene al package java.lang ed importata automaticamente. Possiamo

creare un oggetto di tipo stringa sia invocando un costruttore della classe String, sia
utilizzando direttamente un letterale stringa. In questultimo caso, infatti, Java creer
automaticamente un oggetto String che conterr come valore il letterale indicato e ne
passer il riferimento alla variabile relativa.
ATTENZIONE
Se creiamo pi stringhe a cui passiamo lo stesso letterale stringa, ogni stringa punter sempre
allo stesso oggetto in memoria. Infatti, per ovvie ragioni di efficienza, pi letterali che hanno la
stessa sequenza di caratteri saranno rappresentati sempre dallo stesso oggetto.

Alcuni metodi del tipo String


public String() crea una stringa vuota, ovvero senza caratteri e con lunghezza pari
a 0.

Snippet 15.12 String.


String s1 = new String(); // stringa vuota
String s2 = ""; // stringa vuota

public String(String original) crea una stringa che una copia della stringa original.

Snippet 15.13 String.

String s1 = new String("Sono una stringa!");


String s2 = new String(s1); // Sono una stringa!

public String(char[] value) crea una stringa con i caratteri contenuti nellarray
value .

Snippet 15.14 String.


char c[] = {'a', 'b', 'c'};
String s1 = new String(c); // abc

public String(char[] value, int offset, int count) crea una stringa composta da un
sottoinsieme di caratteri dellarray value. Tale sottoinsieme ricavato fornendo
un indice di partenza inclusivo, offset, e il numero di caratteri da prelevare, count.

Snippet 15.15 String.


char c[] = {'a', 'b', 'c', 'd', 'e'};
String s1 = new String(c, 1, 3); // bcd
public int length() ritorna il numero di caratteri contenuti nella stringa.

Snippet 15.16 length.


String s1 = new String("Sono una stringa!");
int l = s1.length(); // 17

public char charAt(int index) ritorna il carattere della stringa che si trova alla
posizione specificata da index. Ricordiamo che il primo carattere avr la
posizione 0.

Snippet 15.17 charAt.


String s1 = new String("Sono una stringa!");
char c = s1.charAt(s1.length() - 1); // !

public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) copia una
sequenza di caratteri della stringa, indicati dallindice di partenza srcBegin e
dallindice finale srcEnd, a partire dallindice dstBegin dellarray dst. importante
precisare che il valore dellindice finale deve essere sempre a una posizione in
pi rispetto al valore dellindice del carattere scelto. Infatti, nel nostro caso,
abbiamo scritto il valore dindice 15 per identificare il carattere g che invece ha
un valore dindice pari a 14.

Snippet 15.18 getChars.


String s1 = new String("Sono una stringa!");
char c[] = new char[20];
s1.getChars(9, 15, c, 5); // c = string

public boolean equals(Object anObject) verifica che la stringa abbia la stessa


sequenza di caratteri delloggetto anObject.

Snippet 15.19 equals.


String s1 = new String("Sono una stringa!");
String s2 = new String("Sono una stringa!");
boolean b2 = s1.equals(s2); // true

public int compareTo(String anotherString) confronta la stringa con anotherString


comparando lessicograficamente ciascun carattere. Se le stringhe sono uguali il
risultato sar il valore 0, se la stringa minore di anotherString il risultato sar un
valore minore di 0, mentre se la stringa maggiore di anotherString il risultato
sar maggiore di 0.

Snippet 15.20 compareTo.


String s1 = new String("Labi");
String s2 = new String("Gabit");
int r1 = s1.compareTo(s2); // 5 perch ASCII L = 76 ASCII G = 71 L > G
public boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset,

int len) verifica se una sequenza di caratteri della stringa uguale a quella della
stringa other. La sequenza di caratteri indicata fornendo gli indici di partenza,
toffset e ooffset, rispettivamente della stringa e di other e, con len, il numero di
caratteri da verificare. Tramite il parametro ignoreCase possiamo decidere se la
comparazione ignorer la distinzione maiuscolo/minuscolo.

Snippet 15.21 regionMatches.


String s1 = "Pallone";
String s2 = "Pollore";
boolean b = s1.regionMatches(true, 2, s2, 2, 3); // true perch llo uguali

public boolean startsWith(String prefix) verifica se la stringa inizia con la sequenza


di caratteri della stringa prefix.

Snippet 15.22 startsWith.


String s1 = "Pallone";
boolean b = s1.startsWith("Pa"); // true

public boolean endsWith(String suffix) verifica se la stringa termina con la sequenza


di caratteri della stringa suffix.

Snippet 15.23 endsWith.


String s1 = "Pallone";
boolean b = s1.endsWith("ore"); // false

public int indexOf(int ch) consente di cercare allinterno della stringa un carattere
indicato dal parametro ch. Il metodo ritorner il valore -1 se il carattere non
stato trovato, altrimenti ritorner un valore che rappresenter la posizione,
allinterno della stringa, della prima occorrenza del carattere trovato.

Snippet 15.24 indexOf.


String s1 = new String("sono una stringa!");
int r1 = s1.indexOf('s'); // 0 prima occorrenza trovata di s

public int lastIndexOf(int ch) consente di cercare allinterno della stringa un


carattere indicato dal parametro ch. Il metodo ritorner il valore -1 se il carattere
non stato trovato, altrimenti ritorner un valore che rappresenter la posizione,
allinterno della stringa, dellultima occorrenza del carattere trovato. Il metodo
lastIndexOf consente, in pratica, di effettuare la ricerca del carattere partendo dalla

fine della stringa e procedendo verso linizio.

Snippet 15.25 lastIndexOf.


String s1 = new String("sono una stringa!");
int r1 = s1.lastIndexOf('s'); // 9 prima occorrenza trovata di s a partire dalla fine

public String substring(int beginIndex, int endIndex) ritorna una sottostringa che
parte dallindice beginIndex incluso e termina allindice endIndex escluso.

Snippet 15.26 substring.


String s1 = new String("sono una stringa!");
String sub = s1.substring(s1.indexOf("stringa"), s1.lastIndexOf("!") + 1); // stringa!

public String concat(String str) concatena la stringa con unaltra stringa str. La
concatenazione consente, di fatto, di aggiungere alla fine di una stringa tutti i
caratteri di unaltra stringa. Dallo Snippet 15.27 possiamo notare come in Java
sia possibile concatenare delle stringhe utilizzando loperatore +. In effetti il
compilatore, dopo aver compilato il codice, sostituir lespressione x + y con
lespressione (new StringBuilder()).append(x).append(y).toString(), con cui creer un
oggetto di tipo StringBuilder a cui aggiunger il valore della stringa x e della
stringa y prima di riconvertirlo in un oggetto di tipo String.

Snippet 15.27 concat.


String x = new String("Giga");
String y = "bit";
String z = x.concat(y); // Gigabit
String w = x + y; // Gigabit

DETTAGLIO
Loperatore di concatenazione + pu essere utilizzato anche in espressioni con un operando di
tipo String e un operando di tipo primitivo o di un tipo di un qualsiasi altro oggetto. Nel caso dei
tipi primitivi il valore che rappresentano sar convertito in stringa, mentre nel caso di oggetti di tipi
differenti, su di essi verr automaticamente invocato il metodo toString, che ritorner una
rappresentazione in forma di stringa degli oggetti medesimi.

public String replace(CharSequence target, CharSequence replacement) sostituisce ogni


occorrenza trovata della sequenza di caratteri indicati da target, nella stringa, con
la sequenza di caratteri indicata da replacement. Al termine della sostituzione il
metodo ritorner un oggetto String con i cambiamenti apportati. La stringa
originale non subisce alcuna modifica.
Snippet 15.28 replace.

String s1 = new String("sono una stringa!");


String n = s1.replace('s', 'S'); // Sono una Stringa!

public String trim() ritorna una copia della stringa da cui sono stati eliminati i
caratteri iniziali e finali di spaziatura.
Snippet 15.29 trim.

String s1 = new String(" Sono una stringa!\n\n\t");


String n = s1.trim(); // Sono una stringa!

public char[] toCharArray() ritorna un array di char contenente tutti i caratteri della
stringa.

Snippet 15.30 toCharArray.

String s1 = new String(" Sono una stringa!\n\n\t");


char c[] = s1.toCharArray(); // Sono una stringa!\n\n\t

public static String valueOf(Object obj) ritorna una rappresentazione stringa


delloggetto obj. Possiamo utilizzare anche dei metodi in overloading che
consentono di ritornare largomento indicato convertito in stringa.

Snippet 15.31 valueOf.

Object o = new java.util.Date();


int i = 10;
long l = 10l;
float f = 10.4f;
double d = 11.22;
char c = 'X';
boolean b = true;
char a_c[] = { 'a', 'b', 'c', 'd', 'e' };

String s_1 = String.valueOf(o); // la data odierna Fri Dec 06 19:05:09 CET 2013
String s_2 = String.valueOf(i); // 10
String s_3 = String.valueOf(l); // 10
String s_4 = String.valueOf(f); // 10.4
String s_5 = String.valueOf(d); // 11.22
String s_6 = String.valueOf(c); // X
String s_7 = String.valueOf(b); // true
String s_8 = String.valueOf(a_c); // abcde
String s_9 = String.valueOf(a_c, 1, 3); // bcd

public boolean contains(CharSequence s) verifica se i caratteri indicati da s sono


contenuti nella stringa.
Snippet 15.32 contains.

String s1 = new String("Sono una stringa!\n\n\t");


boolean b = s1.contains("una"); // true

NOTA
Esistono altri metodi della classe String, come matches, replaceAll, split e cos via, che si
possono utilizzare passando come argomento una stringa contenente dei caratteri espressi in
una sintassi basata sulle espressioni regolari. Tali metodi saranno pertanto studiati nel Capitolo
16, che tratta questo argomento.
La classe StringBuilder
La classe StringBuilder, appartenente al package java.lang, consente di creare degli
oggetti che conterranno stringhe modificabili, ovvero stringhe il cui contenuto potr
subire dei cambiamenti durante lesecuzione del programma. Ogni oggetto di tipo
StringBuilder ha una capacit, che rappresenta il numero massimo di caratteri che la

stringa pu contenere, e una lunghezza, che rappresenta il numero di caratteri


attualmente presenti nella stringa.
Un oggetto di tipo StringBuilder creato senza una stringa iniziale avr una capacit
di partenza di 16 caratteri, mentre lo stesso oggetto creato con una stringa iniziale avr
una capacit che sar data da 16 caratteri pi la lunghezza della medesima stringa
passata al costruttore.
APPROFONDIMENTO
Una stringa modificabile pu essere creata anche con un oggetto di tipo StringBuffer il cui uso
per indirizzato a programmi che utilizzano pi thread. Infatti, un oggetto di tipo StringBuffer
definito come thread-safe, poich vi sono operazioni di sincronizzazione tra i vari thread che
potrebbero accedere alla stringa delloggetto StringBuffer.

Listato 15.1 StringBuilderDemo.

package com.pellegrinoprincipe;

public class StringBuilderDemo


{
public static String getStringInfo(StringBuilder str)
{
return "Attualmente la stringa '" + str.toString() + "' ha capacita': "
+ str.capacity() + " e " + "lunghezza: " + str.length();
}

public static void main(String[] args)


{
StringBuilder mod_string = new StringBuilder(); // vuota
System.out.println(getStringInfo(mod_string));

mod_string.append("Sed ut perspiciatis");
// aggiungiamo una sequenza di caratteri con append
System.out.println(getStringInfo(mod_string));

// aggiungiamo un'altra sequenza di caratteri con insert


mod_string.insert(7, "accusamus et iusto odio dignissimos ducimus");
System.out.println(getStringInfo(mod_string));

mod_string.delete(0, 4); // togliamo dei caratteri


System.out.println(getStringInfo(mod_string));
}
}

Il Listato 15.1 mette in evidenza che tutte le operazioni compiute sulloggetto


StringBuilder riguardano sempre la stringa in esso contenuta, che cambia

dinamicamente. In particolare vediamo che abbiamo utilizzato i seguenti metodi:


append, che ha aggiunto alla fine della stringa i caratteri passati come argomento.
Questo metodo utilizzabile, in overloading, passando come argomento altri tipi
di dato come int, double, char, boolean e cos via;
insert, che ha inserito a partire dalla settima posizione la stringa passata come
argomento. Anche questo metodo utilizzabile in overloading, consentendo di
inserire il tipo di dato passato come argomento direttamente a partire dalla
posizione specificata;
delete, che ha cancellato la sequenza di caratteri dalla posizione 0 e fino alla

posizione 4 esclusa.

Output 15.1 Dal Listato 15.1 Classe StringBuilderDemo.


Attualmente la stringa '' ha capacita': 16 e lunghezza: 0
Attualmente la stringa 'Sed ut perspiciatis' ha capacita': 34 e lunghezza: 19
Attualmente la stringa 'Sed ut accusamus et iusto odio dignissimos ducimusperspiciatis' ha
capacita': 70 e lunghezza: 62
Attualmente la stringa 'ut accusamus et iusto odio dignissimos ducimusperspiciatis' ha
capacita': 70 e lunghezza: 58

LOutput 15.1 evidenzia come la capacit della stringa sia stata adattata
automaticamente in modo che sia sempre superiore (o al massimo uguale) alla
lunghezza della stringa. In particolare, il valore della capacit attuale si ottiene con il
metodo capacity.

Alcuni metodi del tipo StringBuilder


public void ensureCapacity(int minimumCapacity) permette di aumentare la capacit
minima di caratteri che loggetto pu contenere. Se il parametro minimumCapacity
superiore allattuale valore, allora verr impostato come attuale valore di
capacit il valore maggiore tra il valore della capacit attuale (dato dal doppio
del suo valore + 2) e minimumCapacity stesso. Ovviamente, se largomento
inferiore al valore di capacit attuale, questultimo non verr modificato.
Snippet 15.33 ensureCapacity.

StringBuilder sb_1 = new StringBuilder("Sono una stringa di tipo builder!");


int l = sb_1.capacity(); // 49
sb_1.ensureCapacity(55); // pi grande di capacity
l = sb_1.capacity(); // 100, ovvero (49 * 2 + 2) che maggiore di 55

public void setLength(int newLength) imposta la lunghezza della stringa delloggetto


al valore indicato da newLength. Se la nuova lunghezza inferiore allattuale, i
caratteri eccedenti saranno eliminati, mentre se superiore saranno aggiunti, per
i caratteri in eccedenza, dei null character (caratteri con valore '\u0000').
Snippet 15.34 setLength.

StringBuilder sb_1 = new StringBuilder("Sono una stringa di tipo builder!");


sb_1.setLength(10); // stringa troncata a Sono una s

public void setCharAt(int index, char ch) modifica il carattere della stringa
delloggetto, che si trova alla posizione index, con il carattere indicato da ch.

Snippet 15.35 setCharAt.

StringBuilder sb_1 = new StringBuilder("Sono una stringa di tipo builder!");


int l = sb_1.lastIndexOf("s");
sb_1.setCharAt(l, 'S'); // Sono una Stringa di tipo builder!

public StringBuilder deleteCharAt(int index), cancella il carattere della stringa


delloggetto alla posizione indicata da index.

Snippet 15.36 deleteCharAt.

StringBuilder sb_1 = new StringBuilder("Sono una stringa di tipo builder!");


int offset = sb_1.indexOf("u");
sb_1.deleteCharAt(offset); // Sono na stringa di tipo builder!

public StringBuilder replace(int start, int end, String str) sostituisce la sequenza di
caratteri della stringa delloggetto che si trovano tra le posizioni start ed end (non
inclusa) con la stringa str.

Snippet 15.37 replace.

StringBuilder sb_1 = new StringBuilder("Sono una stringa di tipo builder!");


// Sono StringBuilder!
sb_1.replace(sb_1.indexOf("una"), sb_1.lastIndexOf("!"), "StringBuilder");
La classe StringTokenizer
Un oggetto di tipo StringTokenizer, appartenente al package java.util, consente di
avere una stringa formata da token, che ne rappresentano le singole unit di
composizione. Ogni token separato da un delimitatore, che pu essere costituito da
una determinata sequenza di caratteri.
Listato 15.2 Classe StringTokenizerDemo.

package com.pellegrinoprincipe;

import java.util.StringTokenizer;

public class StringTokenizerDemo


{
public static void main(String[] args)
{
// token separati dai delimitatori di default
StringTokenizer st1 = new StringTokenizer("Sono una stringa");
// token separati da delimitatori custom
StringTokenizer st2 = new StringTokenizer("Altro##che##stringa", "##");

while (st1.hasMoreTokens()) // scorriamo i token


{
String token = st1.nextToken();
System.out.print(token + " ");
}
System.out.println();
while (st2.hasMoreTokens())
{
String token = st2.nextToken();
System.out.print(token + " ");
}
}
}

Output 15.2 Dal Listato 15.2 Classe StringTokenizerDemo.

Sono una stringa


Altro che stringa

Il Listato 15.2 mette in evidenza come loggetto di tipo StringTokenizer st1 crei una
stringa composta da token separati dai delimitatori di default che sono rappresentati
dai caratteri di spaziatura \t, \n, \r, \f e \u0020. Loggetto st2 crea, invece, unaltra
stringa composta da token il cui delimitatore composto dai caratteri indicati come
secondo argomento del costruttore. Successivamente utilizziamo un ciclo while per
stampare il valore di tutti i token presenti nelloggetto. In particolare, il ciclo while
verr eseguito finch loggetto di tipo StringTokenizer avr ancora dei token da
processare (hasMore Tokens). Il token da processare poi ottenuto invocando il metodo
nextToken.

Alcuni metodi del tipo StringTokenizer


crea un oggetto
public StringTokenizer(String str, String delim, boolean returnDelims)

di tipo StringTokenizer contenente la stringa str, separata dal delimitatore delim, e


indica, con il flag booleano returnDelims, se il delimitatore deve essere ritornato
anchesso come token.
public int countTokens() ritorna il numero di token di cui composta la stringa

delloggetto.
ritorna il successivo token della stringa e
public String nextToken(String delim)

consente anche di impostare tramite delim un nuovo separatore che verr


utilizzato per le successive invocazioni del metodo.
Capitolo 16
Espressioni regolari

Unespressione regolare (regular expression, regex o regexp) unespressione


formata da una stringa di testo i cui caratteri sono simboli (caratteri dellalfabeto e
caratteri speciali) che formano una sorta di pattern di ricerca utilizzato per effettuare
confronti con una sequenza di caratteri di una stringa al fine di verificare delle
corrispondenze. Se il pattern rappresentato dai simboli descritti con lespressione
regolare trova corrispondenza con i caratteri che descrive, allinterno della stringa di
ricerca, allora si pu dire che lespressione regolare ha avuto esito positivo ed stata
soddisfatta.
Concetti propedeutici
Prima di elencare le classi e i metodi che Java mette a disposizione per la
manipolazione delle espressioni regolari, e prima di elencare i rimanenti metodi della
classe String che accettano come argomento unespressione regolare, fondamentale
conoscere i simboli che potremo utilizzare per la loro costruzione.
Nel seguito riportiamo una serie di tabelle, ognuna delle quali formata dalle
seguenti colonne: Costrutto indica un simbolo di unespressione regolare da
utilizzare ed eventualmente dei caratteri di esempio; Corrispondenza rappresenta
ci che il simbolo in grado di cercare; Esempio rappresenta un esempio di come
si scrive, e si prova, lespressione regolare, e fa uso di una metasintassi che prevede a
sinistra del simbolo la stringa da verificare e alla sua destra la stringa contenente
lespressione regolare. Seguir un commento con un valore true o false a indicare se
lespressione regolare stata soddisfatta.

Tabella 16.1 Corrispondenza precisa con il carattere indicato.


Costrutto Corrispondenza Esempio
x Il carattere x "x" "x" // true

\\ Il carattere backslash "\\" "\\\\" // true

\0n Il carattere come valore ottale "\5" "\\05" // true

\xhh Il carattere come valore esadecimale (2 cifre) "9" "\\x39" // true

\uhhhh Il carattere come valore esadecimale (4 cifre) "j" "\u006A" // true

\t Il carattere TAB "\t" "\t" // true

\n Il carattere NEW LINE "\n" "\n" // true

\r Il carattere CARRIAGE RETURN "\r" "\r" // true

\f Il carattere FORM-FEED "\f" "\f" //true

\a Il carattere BELL "\u0007" "\\a" // true

\e Il carattere ESCAPE "\u001B" "\\e" // true

Spieghiamo alcuni esempi della Tabella 16.1.

Per il costrutto \\ abbiamo scritto la stringa da verificare con un doppio


backslash (\\) perch, ricordiamo, in Java un carattere di escape preceduto dal
simbolo \, quindi per far interpretare letteralmente lo stesso carattere lo si deve
far precedere ancora da \. La stringa dellespressione regolare ha, invece, ben
quattro backslash (\\\\) e ci perch anche per un motore di espressioni regolari
il carattere backslash un carattere speciale. Pertanto avremo i due backslash
per il parser Java e i due backslash per il motore regex.
Per i costrutti \xhh e \uhhhh il pattern di ricerca espresso scrivendo in
esadecimale, a 2 o a 4 cifre, il valore del carattere rispetto alla tabella Unicode.

La Tabella 16.2 evidenzia che per utilizzare una classe carattere non predefinita si
scrive tra le parentesi quadre [ ] un insieme di valori che determinano un significato
di ricerca per lespressione regolare.
Tabella 16.2 Corrispondenza con una classe di caratteri non predefinita.
Costrutto Corrispondenza Esempio
[abc] a, b oppure c "b" "[abc]" // true

[^abc] Ogni carattere eccetto a, b oppure c "x" "[^abc]" // true

[a-zA-Z] Tutti i caratteri tra a e z e A e Z (range) "5" "[a-zA-Z]" // false

[a-d[m-p]] Tutti i caratteri tra a e d e tra m e p (unione) "m" "[a-d[m-p]]" // true

[a-z&&[def]] d, e oppure f (intersezione con a-z) "e" "[a-z&&[def]]" // true

[a-z&&[^bc]] Tutti i caratteri tra a e z tranne b e c (sottraz.) "b" "[a-z&&[^bc]]" // false

[a-z&&[^m-p]] I caratteri tra a e z tranne quelli tra m e p "b" "[a-z&&[^m-p]]" // true

ATTENZIONE
I costrutti della Tabella 16.2 cercano la corrispondenza di un solo carattere alla volta che risponde
ai requisiti imposti dai range di ricerca specificati.

La Tabella 16.3 illustra i costrutti che servono per creare espressioni regolari che
cercano il carattere indicato rispetto a una classe di caratteri predefinita.

Tabella 16.3 Corrispondenza con una classe di caratteri predefinita.


Costrutto Corrispondenza Esempio
. Ogni carattere che non sia un terminatore di linea "*" "." // true

\d Un carattere numerico compreso tra 0 e 9 "4" "\\d" // true

\D Qualunque carattere non numerico "k" "\\D" // true

\s Un carattere tra \t \n \f \r \x20 \x0B "\f" "\\s" // true

\S Non un carattere di spazio "-" "\\S" // true

\w Un carattere alfanumerico inclusivo del carattere _ "*" "\\w" // false

\W Un carattere non alfanumerico e non _ "*" "\\W" // true

La Tabella 16.4 illustra come cercare un carattere in base a delle corrispondenze di


limite su linee di testo e su singole parole. In particolare vediamo che sia il costrutto
che utilizza il simbolo ^, sia quello che utilizza il simbolo \A cercano un carattere
allinizio di una sequenza di input. Tuttavia, mentre il costrutto \A trover il carattere 5
solo una volta, ovvero solo allinizio della linea di testo e indipendentemente dal fatto
che vi siano o meno caratteri di terminazione di linea, il costrutto ^ trover il carattere
due volte, poich considerer anche il carattere di terminazione di linea. La stessa
5
considerazione vale anche per il costrutto $ e il costrutto \Z, ovviamente riferita a
caratteri che si troveranno alla fine di una sequenza di input.
Tabella 16.4 Corrispondenze di limite.
Costrutto Corrispondenza Esempio
^ Un carattere allinizio di una stringa "5ab\n5yu" "^\\d" // true

$ Un carattere alla fine di una stringa "ab5\nyu5" "\\d$" // true

\b Un carattere al limite di una parola "is a child" "\\bc" // true

\B Un carattere non al limite di una parola "is a child" "\\Bc" // false

\A Un carattere allinizio di una linea di input "5ab\n5yu" "\\A\\d" // true

\Z Un carattere alla fine di una linea di input "ab5\nyu5" "\\d\\Z" // true

\z Un carattere alla fine di una linea di input "ab5\nyu5\n" "\\d\\z" // false

\G Un carattere allinizio della stringa del primo match "d4nd4" "\\G\\w\\d" // true

Il costrutto \z, invece, differisce dal costrutto \Z perch trova una corrispondenza
solo se il carattere non seguito da un carattere di terminazione di linea. Il costrutto
\G, infine, soddisfatto solamente se i caratteri trovati fanno parte di una

corrispondenza che posta allinizio della stringa. Ci significa che lesempio


trover una corrispondenza solo per i primi caratteri d4, mentre per gli altri, posti
dopo il carattere n, non vi sar corrispondenza. Infine, i costrutti \b e \B cercano,
rispettivamente, un carattere che si trovi al limite o non al limite di una parola, dove
il limite di una parola dato dal carattere di spazio o di newline.

MODALIT MULTILINE
Per far s che il costrutto ^ e il costrutto $ riconoscano i caratteri iniziali e finali a ogni terminazione
di linea, e non solo considerando lintera sequenza di input, occorre attivare la modalit MULTILINE,
cosa che si pu fare sia scrivendo la modalit come argomento del metodo compile per esempio
Pattern.compile("^\\d",Pattern.MULTILINE).matcher("5ab\n5yu").find() sia definendola con
lespressione (?m) allinterno della stringa espressione regolare, per esempio "(?m)\\d$".

APPROFONDIMENTO
I caratteri di terminazione di linea riconosciuti sono i seguenti: \n (newline), \r\n (carriage-return
seguito da un newline), \r (carriage-return), \u0085 (next-line), \u2028 (line-separator) e \u2029
(paragraph-separator).

La Tabella 16.5 elenca i costrutti relativi ai quantificatori che consentono di


cercare caratteri che sono presenti (si ripetono) un determinato numero di volte.
Questi costrutti sono definiti dai caratteri ?, *, + e { } e devono essere posti sempre
dopo i caratteri da quantificare. Per default i quantificatori si comportano in modo
greedy, ovvero cercano di trovare con ingordigia quante pi corrispondenze dei
caratteri indicati, ritornando la pi lunga porzione della stringa soddisfatta. Possiamo
tuttavia cambiare questo comportamento facendo diventare i quantificatori di tipo
lazy (aggiungendo dopo il quantificatore greedy il carattere ?) o possessive
(aggiungendo dopo il quantificatore greedy il carattere +). I quantificatori lazy
cercano con pigrizia il minor numero di corrispondenze dei caratteri indicati,
ritornando solo la prima porzione della stringa soddisfatta, mentre con i quantificatori
possessive i caratteri cercati devono soddisfare lespressione regolare come stringa
nella sua interezza.

Tabella 16.5 Corrispondenze con quantificatori greedy.


Costrutto Corrispondenza Esempio
x? Il carattere x zero o una volta "x" "x?" // true

x* Il carattere x zero o pi volte "xxxxx" "x*" // true

x+ Il carattere x una o pi volte "v" "x+" // false

x{n} Il carattere x esattamente n volte "xxx" "x{3}" // true

x{n,} Il carattere x almeno n volte "xxxxx" "x{3,}" // true

x{n,m} Il carattere x tra n e m volte "xxxxx" "x{3,6}" // true

Nella Tabella 16.6 lutilizzo delle parentesi tonde ( ) consente di raggruppare in


ununica entit la sequenza di caratteri ivi inclusa e trattarla come tale nelle
operazioni di comparazione. Inoltre le parentesi permettono di memorizzare
leventuale corrispondenza trovata per un successivo utilizzo, sia allinterno
dellespressione regolare, sia al di fuori di essa, con la sintassi propria del linguaggio
di programmazione utilizzato. Ogni gruppo di corrispondenze trovate numerato a
partire da 1 e lordine di conteggio delle parentesi tonde procede da sinistra a destra.
Tali gruppi di corrispondenze memorizzate possono poi essere utilizzati
nellespressione regolare, referenziandoli con il simbolo backslash (\) e un numero
indicante il gruppo interessato.
Cos, per esempio, lespressione regolare (\d\w)\1 indicher che la corrispondenza
sar soddisfatta solo se avremo inserito un singolo numero, un singolo carattere e se
lo stesso numero e carattere sar ripetuto. Infatti, nel nostro esempio le parentesi
tonde creeranno un gruppo di cattura numerato con 1 che sar referenziato con \1.

Tabella 16.6 Corrispondenze con gli operatori logici.


Costrutto Corrispondenza Esempio
xy Il carattere x seguito dal carattere y "xy" "xy" // true

x | y Il carattere x oppure il carattere y "z" "x|y" // false

(x) Il carattere x come gruppo di cattura "x" "(x)" // true

TERMINOLOGIA
Si utilizza il termine backreference per indicare la memorizzazione, da parte del motore delle
espressioni regolari, di una parte della stringa trovata come gruppo di cattura e il suo successivo
riutilizzo.

Nella Tabella 16.7 vediamo lelenco di costrutti speciali che consentono di


utilizzare un nome per i gruppi di cattura, un gruppo senza catturarlo e delle ricerche
definite di lookaround.
Tabella 16.7 Corrispondenze con costrutti speciali.
Costrutto Corrispondenza Esempio
(?<name>x) Il carattere x come gruppo di cattura con un nome "x" "(?<captX>x)" // true

(?:x) Il carattere x come gruppo ma senza catturarlo "x" "(?:x)" // true

x(?=y) Il carattere x se seguito dal carattere y "xy" "x(?=y)" // true

x(?!y) Il carattere x se non seguito dal carattere y "xy" "x(?!y)" // false

(?<=y)x Il carattere x se preceduto dal carattere y "yx" "(?<=y)x" // true

(?<!y)x Il carattere x se non preceduto dal carattere y "yx" "(?<!y)x" // false

Il primo caso utile quando vogliamo dare un nome ai gruppi di cattura e


referenziarli con esso. Questa possibilit risulta sicuramente pi pratica rispetto al
dover referenziare i gruppi di cattura con numeri attribuiti automaticamente dal
motore, il cui gruppo di appartenenza non chiaramente individuabile se non dopo
aver rivisto lespressione regolare e aver ricomputato tutte le parentesi tonde
costituenti i gruppi.
Potremo cos riscrivere lespressione regolare del precedente esempio da (\d\w)\1 a
(?<numword>\d\w)\k<numword> , dove il backreference viene effettuato utilizzando il simbolo
a cui segue il nome dato al gruppo di cattura tra le parentesi angolari < >.
\k

Le ricerche di lookaround sono invece definite positive lookahead come nel


costrutto x(?=y), negative lookahead come nel costrutto x(?!y), positive lookbehind
come nel costrutto (?<=y)x e negative lookbehind come nel costrutto (?<!y)x.
NOTA
In conclusione ci preme ricordare il seguente punto chiave: ogni costrutto corrisponde a un solo
carattere da cercare; pertanto, per effettuare comparazioni su pi caratteri occorre usare i
quantificatori ?, *, + e { } e ricordare che essi si riferiscono sempre ai simboli che li precedono.
Espressioni regolari con la classe String
La classe String mette a disposizione i seguenti metodi che accettano come
argomento una stringa contenente dei caratteri che rappresentano unespressione
regolare.

public boolean matches(String regex) verifica se c una corrispondenza tra i caratteri


della stringa e lespressione regolare del parametro regex. Lo Snippet 16.1
verifica se la stringa ip contiene la rappresentazione di un indirizzo di rete.
Ovviamente la regex non completa, perch non verifica il range di valori
accettabili; infatti un valore come 999.999.999.999 sarebbe valido per la regex, ma
non lo sarebbe come indirizzo di rete i cui valori accettabili, per ogni ottetto,
sono compresi tra 0 e 255.

Snippet 16.1 matches.


String ip = "192.168.1.25";
String regex = "(\\d{1,3}\\.){3}\\d{1,3}";
boolean b = ip.matches(regex); // true

public String replaceAll(String regex, String replacement) sostituisce i caratteri della


stringa che corrispondono allespressione regolare indicata dal parametro regex
con i caratteri indicati dalla stringa del parametro replacement. Il metodo ritorna
un oggetto String con gli eventuali caratteri sostituiti.

Snippet 16.2 replaceAll.


String ip = "10.150.36.25";
String regex = "\\.";
String rep = ip.replaceAll(regex, "#"); // 10#150#36#25

public String[] split(String regex) divide la stringa in tante sottostringhe a seconda


dei caratteri di separazione trovati che corrispondono allespressione regolare
del parametro regex. Le sottostringhe trovate saranno ritornate, nellordine, in un
array di oggetti String. Se nessuna sottostringa stata trovata, larray ritornato
conterr solo un elemento che corrisponder allintera stringa.

Snippet 16.3 split.

String ip = "10.150.36.25";
String regex = "\\.";
String rep[] = ip.split("\\."); // rep[0] = 10 rep[1] = 150 rep[2] = 36 rep[3] = 25
Le classi Pattern e Matcher
Il linguaggio Java, oltre a consentire di utilizzare i costrutti delle espressioni
regolari con alcuni metodi della classe String, permette anche di gestire le regex
tramite le classi Pattern e Matcher appartenenti al package java.util.regex.

La classe Pattern consente di creare un oggetto che contiene una rappresentazione


di unespressione regolare, mentre la classe Matcher consente di eseguire le operazioni
di comparazione tra lespressione regolare e una sequenza di caratteri.
Listato 16.1 Classe PatternMatcherDemo.

...
public class PatternMatcherDemo
{
// validiamo la stringa con la regex
public static String[] validate(String to_compare[], String regex)
{
String found[] = new String[to_compare.length]; // array di stringhe convalidate
Pattern regex_pattern = Pattern.compile(regex); // creo un oggetto Pattern
for (int i = 0; i < to_compare.length; i++)
{
String comp = to_compare[i];
Matcher matcher = regex_pattern.matcher(comp); // creo un oggetto Matcher

if (matcher.find()) // trova la successiva occorrenza
found[i] = matcher.group(); // ritorna la sequenza trovata
else
found[i] = null;
}
return found;
}

public static void main(String[] args)


{
String tel_number[] = {"06/442255","fdr3344\n","081-556677","085/6677889900"};
String names[] = {"maurizio","Pellegrino","Paolo Pietro","Mass6imo"};

// espressioni regolari
String regex_for_tel = "0\\d/\\d{6}";
String regex_for_names = "[A-Z][a-z]+(\\s[A-Z][a-z]+)?";

String tel_val[] = validate(tel_number, regex_for_tel); // valida i telefoni
System.out.print("TELEFONI VALIDI:[ ");
for (String tel : tel_val)
System.out.print(tel + " ");

System.out.print("]\nNOMI VALIDI:[ ");
for (int i = 0; i < names.length; i++)
{
String name = names[i];

boolean succ = Pattern.matches(regex_for_names, name); // corrisponde?


if(succ)
System.out.print(name);
else
System.out.print("null");

System.out.print(" ");
}
System.out.println("]");
}
}

Output 16.1 Dal Listato 16.1 Classe PatternMatcherDemo.


TELEFONI VALIDI:[ 06/442255 null null null ]
NOMI VALIDI:[ null Pellegrino Paolo Pietro null ]

Nel Listato 16.1 creiamo quanto segue:

larray di stringhe tel_number, contenente dei numeri di telefono che dovranno


essere convalidati con la stringa regex_for_tel, contenente lespressione regolare
0\d/\d{6} . Tale espressione regolare convalida un numero di telefono solo se esso
inizia con il numero 0 seguito da un numero, dal carattere / e da altri 6 numeri;
larray di stringhe names, contenente dei nomi di persona che dovranno essere
convalidati con la stringa regex_for_names, contenente lespressione regolare [A-Z]
[a-z]+(\s[A-Z][a-z]+)? . Tale espressione regolare convalida un nome se esso inizia
con una lettera compresa tra A e Z e seguita da una o pi lettere comprese tra a e z
oppure se, opzionalmente, sar seguito da un carattere di spazio, una lettera
compresa tra A e Z e una o pi lettere comprese tra a e z;
un ciclo che scorre tutto larray names e per ogni nome controlla se vi una
corrispondenza utilizzando il metodo statico matches della classe Pattern. Questo
metodo ritorna un valore true solo se lintera sequenza di input verificata;
il metodo statico validate, che utilizza sia il metodo statico compile della classe
Pattern , per creare un oggetto di tipo Pattern che conterr lespressione regolare
passata come argomento, sia il metodo matcher delloggetto Pattern, per creare un
oggetto di tipo Matcher a cui passeremo la stringa da analizzare. Loggetto Matcher
cos creato servir per effettuare, tramite il suo metodo find, la verifica di
comparazione tra lespressione regolare e la stringa da analizzare. Il metodo find
ritorner un valore true per la successiva ricorrenza trovata che sar restituita dal
metodo group delloggetto Matcher.

DIFFERENZA TRA IL METODO MATCHES E IL METODO FIND


La differenza fondamentale tra il metodo matches e il metodo find data dal fatto che questultimo
cerca nella stringa quante pi ricorrenze soddisfano lespressione regolare, mentre il primo verifica
se tutta la stringa soddisfa lespressione regolare. Ci significa che nella nostra espressione
regolare di ricerca dei nomi, utilizzata nel Listato 16.1, una stringa come "Paolo Pietro stato con
Marta" sar soddisfatta due volte con il metodo find, perch alla prima invocazione il metodo
trover Paolo Pietro e poi a unaltra invocazione trover Marta, ma non sar soddisfatta con il
metodo matches, perch tutta la stringa non conforme al pattern di ricerca, dato che contiene
anche i caratteri stato non codificati nellespressione regolare.

Alcuni metodi del tipo Pattern


public static Pattern compile(String regex, int flags) crea un oggetto di tipo Pattern
contenente lespressione regolare fornita dal parametro regex con dei flag di
compilazione specificati dal parametro flags. I flag attribuibili sono dati dalle
seguenti costanti: Pattern.UNIX_LINES, con cui decidiamo che lunico carattere di
terminazione di una linea il new line \n; Pattern.CASE_INSENSITIVE, con cui
decidiamo di effettuare la comparazione senza distinguere se un carattere ASCII
sia scritto in maiuscolo oppure in minuscolo; Pattern.COMMENTS, con cui possiamo
scrivere caratteri di spazio e commenti che iniziano con il carattere # senza che
vengano interpretati dal motore delle regex; Pattern.MULTILINE, con cui attiviamo il
modo multiriga; Pattern.LITERAL, con cui consentiamo linterpretazione letterale
dei metacaratteri e delle sequenze di escape; Pattern.DOTALL, con cui consentiamo
di includere nei caratteri rappresentati dal costrutto . anche il carattere di
terminazione di riga; Pattern.UNICODE_CASE (da utilizzare insieme al flag
CASE_INSENSITIVE ), con cui consentiamo la ricerca di corrispondenza senza
distinguere se un carattere Unicode sia scritto in maiuscolo oppure in minuscolo;
Pattern.CANON_EQ, con cui consentiamo di far riconoscere come equivalenti i

caratteri che possono avere una forma composita. Per esempio, la lettera e con
un accento acuto pu essere rappresentata sia come singolo carattere ( \u00E9)
sia con due caratteri (e e\u0301); pertanto, per far riconoscere entrambe le
rappresentazioni come equivalenti dovremo attivare tale flag.
public String pattern() ritorna la stringa contenente lespressione regolare oggetto

della compilazione.
public int flags() ritorna i flag specificati.

Listato 16.2 Classe PatternFlag.

...
public class PatternFlag
{
public static void main(String[] args)
{
// CASE_INSENSITIVE
String c_ins = "abcdefg";
String regex_c_ins = "ABCDEFG";
boolean res = Pattern.compile(regex_c_ins,
Pattern.CASE_INSENSITIVE).matcher(c_ins).find();
System.out.println("FLAG CASE_INSENSITIVE match? " + res);

// COMMENTS
String comm = "<!-- css rules -->";
String regex_comm = "<!--.*?--> # Pattern di ricerca per commenti HTML";
res = Pattern.compile(regex_comm, Pattern.COMMENTS).matcher(comm).find();
System.out.println("FLAG COMMENT match? " + res);

// DOTALL
String dotall = "Test di Touring\n";
String regex_dotall = ".";
Matcher m = Pattern.compile(regex_dotall, Pattern.DOTALL).matcher(dotall);
res = m.find(15); // trova il new line
System.out.println("FLAG DOTALL match? " + res);

// UNICODE_CASE
String uni_case = ""; // lettere in cirillico MAIUSC.
String regex_uni_case = ""; // lettere in cirillico MINUSC.
res = Pattern.compile(regex_uni_case, Pattern.CASE_INSENSITIVE |
Pattern.UNICODE_CASE).matcher(uni_case).find();
System.out.println("FLAG UNICODE_CASE match? " + res);

// LITERAL
String literal = "\\d\\w";
String regex_literal = "\\d\\w";
res = Pattern.compile(regex_literal, Pattern.LITERAL).matcher(literal).find();
System.out.println("FLAG LITERAL match? " + res);
}
}

Output 16.2 Dal Listato 16.2 Classe PatternFlag.


FLAG CASE_INSENSITIVE match? true
FLAG COMMENT match? true
FLAG DOTALL match? true
FLAG UNICODE_CASE match? true
FLAG LITERAL match? true

Il Listato 16.2 mostra come utilizzare alcuni dei flag citati. In particolare:

per il flag DOTALL notiamo come il metodo find trovi alla posizione 15 della stringa
dotall il carattere new line. Se avessimo omesso tale flag e avessimo invocato il
metodo allo stesso modo, non avremmo avuto nessuna corrispondenza;
per il flag UNICODE_CASE notiamo come i flag possano essere utilizzati insieme
grazie alloperatore OR inclusivo a livello di bit |, che crea di fatto una
maschera di bit. Nel caso in esame abbiamo unito il flag UNICODE_CASE e il flag
CASE_INSENSITIVE al fine di consentire che i caratteri scritti in alfabeto cirillico
potessero essere confrontati, indipendentemente dalla loro rappresentazione
maiuscolo/minuscolo, come da codifica Unicode;
per il flag LITERAL notiamo come i caratteri scritti nella stringa regex_literal
vengano confrontati letteralmente con i caratteri scritti nella stringa literal.
Infatti, i caratteri \d, . e \w non avranno alcun significato per il motore delle
regex.

UTILIZZO ALTERNATIVO DEI FLAG DI COMPILAZIONE


I flag esaminati possono essere inseriti direttamente nella stringa contenente lespressione
regolare, scrivendo tra parentesi tonde il carattere punto interrogativo e uno dei seguenti caratteri: d
per UNIX_LINE, i per CASE_INSENSITIVE, x per COMMENTS, m per MULTILINE, s per DOTALL e u per
UNICODE_CASE. Non sono presenti i corrispondenti flag per LITERAL e CANON_EQ. Tutti i flag citati
possono essere combinati scrivendoli insieme, sempre dopo il carattere ?. Riprendendo lesempio
del Listato 16.2, per il flag UNICODE_CASE avremmo potuto scrivere lespressione regolare nel
seguente modo: String regex_uni_case = "(?iu)", dove avremmo posto allinizio della stringa
i flag da utilizzare.
Alcuni metodi del tipo Matcher
public Matcher reset() consente di resettare loggetto Matcher portandolo a uno stato
uguale a quello che aveva allatto della sua creazione e permettendo di
riutilizzarlo.
public Matcher reset(CharSequence input) consente di reinizializzare loggetto Matcher

passandogli una nuova stringa da comparare, rappresentata dal parametro input.


public int start() ritorna lindice numerico del primo carattere dellattuale
corrispondenza trovata.

Snippet 16.4 start.

String str = "Il mio nome e' Pellegrino e non Rino";


String regex = "(?i)rino"; // trova rino case-insensitive
Matcher m = Pattern.compile(regex).matcher(str);
m.find();
int s = m.start(); // 21

Lo Snippet 16.4 mostra come il metodo start abbia ritornato il valore 21, che
rappresenta la posizione allinterno della stringa di ricerca della lettera r, primo
carattere della stringa rino. Se avessimo invocato nuovamente il metodo m.find() e poi
il metodo m.start(), questultimo avrebbe ritornato il valore 32, relativo alla posizione
del carattere R della stringa Rino.

public int end() ritorna lindice numerico dellultimo carattere dellattuale


corrispondenza trovata.

Snippet 16.5 end.


String str = "Il mio nome e' Pellegrino e non Rino";
String regex = "(?i)rino"; // trova rino case-insensitive
Matcher m = Pattern.compile(regex).matcher(str);
m.find();
int s = m.end(); // 25

Lo Snippet 16.5 mostra che il metodo end ha ritornato il valore 25 che si riferisce
alla posizione dellultimo carattere (pi uno) della stringa rino trovata.

public String group(int group) ritorna la corrispondenza trovata relativamente al


gruppo di cattura indicato dal parametro group.
public int groupCount() ritorna il numero dei gruppi di cattura definiti
nellespressione regolare senza considerare lintera stringa da comparare, che
per convenzione considerata come gruppo 0.
public boolean find(int start) cerca una stringa che soddisfi lespressione regolare

partendo, allinterno della stringa da comparare, dallindice indicato dal


parametro start.

Snippet 16.6 find.


String str = "Il mio nome e' Pellegrino e non Rino";
String regex = "(?i)rino";
Matcher m = Pattern.compile(regex).matcher(str);
boolean b = m.find(30); // true

Lo Snippet 16.6 utilizza il metodo find indicando che deve iniziare la ricerca di una
corrispondenza a partire dal carattere posto alla posizione 30. In questo caso, infatti,
la stringa trovata che corrisponder alla regex sar Rino e non rino.

public boolean lookingAt() cerca, allinizio della stringa da comparare, la sequenza


di caratteri che soddisfino lespressione regolare. Questo metodo si comporta
come il metodo matches, ma non richiede che lintera stringa da comparare
soddisfi lespressione regolare.

Snippet 16.7 lookingAt.


String str = "Java e' eccellente!";
String str_2 = "E' eccellente Java";
String regex = "Java";
Matcher m = Pattern.compile(regex).matcher(str);
boolean b = m.lookingAt(); // true
m.reset(str_2);
b = m.lookingAt(); // false

Lo Snippet 16.7 mostra come solo la ricerca allinterno della stringa str dia un
esito favorevole, perch la stringa Java presente al suo inizio. In pratica tale metodo
consente di verificare se la stringa str inizia con il pattern Java.

public Matcher usePattern(Pattern newPattern) consente di cambiare, per questoggetto


Matcher, loggetto Pattern associato con quello indicato dal parametro newPattern.

Listato 16.3 MatcherGroup.


...
public class MatcherGroup
{
public static void main(String[] args)
{
String to_compare = "Pellegrino, Rino, Gennarino";
String regex = "(?i)R(ino)";
Matcher m = Pattern.compile(regex).matcher(to_compare);

while (m.find()) // scorre tutte le occorrenze trovate


{
System.out.println("CORRISPONDENZA TROVATA:");
System.out.println("Gruppo 0: " + m.group(0) + " alla posizione: "
+ m.start(0));
System.out.println("Gruppo 1: " + m.group(1) + " alla posizione: "
+ m.start(1));
System.out.println("---------------------------------------------");
}
}
}
Output 16.3 Dal Listato 16.3 Classe MatcherGroup.

CORRISPONDENZA TROVATA:
Gruppo 0: rino alla posizione: 6
Gruppo 1: ino alla posizione: 7
---------------------------------------------
CORRISPONDENZA TROVATA:
Gruppo 0: Rino alla posizione: 12
Gruppo 1: ino alla posizione: 13
---------------------------------------------
CORRISPONDENZA TROVATA:
Gruppo 0: rino alla posizione: 23
Gruppo 1: ino alla posizione: 24
---------------------------------------------

Il Listato 16.3 mostra come utilizzare i metodi start e group, che consentono di
ottenere le corrispondenze relative ai gruppi di cattura specificati. Prima di spiegare il
risultato dellOutput 16.3, illustriamo in maggior dettaglio il concetto di gruppo.
Come si gi detto, un gruppo definibile come una sequenza di caratteri trattata
come unentit atomica. Nella terminologia delle espressioni regolari un gruppo
anche definibile come una sottocorrispondenza scritta allinterno della stringa
costituente il pattern di ricerca.
Per esempio, se avessimo lespressione regolare (\d)(\w)(\.)([a-f]) avremmo i
seguenti cinque gruppi:

il gruppo 0, che tutta lespressione regolare: (\d)(\w)(\.)([a-f]);


il gruppo 1, che il costrutto \d;
il gruppo 2, che il costrutto \w;
il gruppo 3, che il costrutto \.;
il gruppo 4, che il costrutto [a-f].

Se, avessimo, invece, la stringa da comparare 4A.f, avremmo i seguenti gruppi


trovati:

il gruppo 0 sar dato da tutta la stringa 4A.f;


il gruppo 1 sar dato dal numero 4;
il gruppo 2 sar dato dalla lettera A;
il gruppo 3 sar dato dal carattere .;
il gruppo 4 sar dato dalla lettera f.

Dalla disamina dei punti potremo pertanto dire che il gruppo 0 sar sempre riferito
allintero pattern di ricerca e allintera corrispondenza trovata.
Ritornando infine alloutput del nostro programma, vediamo che esso stamper per
ogni corrispondenza i due gruppi trovati, dove il gruppo 0 sar dato dalla stringa Rino,
che rappresenta tutto il pattern di ricerca, mentre il gruppo 1 sar dato dal sottogruppo
rappresentato dai caratteri ino posti tra parentesi tonde.
Capitolo 17
Collezioni

Una collezione costituita da un gruppo di oggetti, definiti elementi, tra loro


naturalmente collegati. Si pensi per esempio a una collezione di lettere, di numeri
telefonici, di animali, di riviste per computer e cos via per tutta una serie altri
elementi che possono essere correlati e rappresentati come ununica entit.
Una collezione anche definita utilizzando il termine contenitore, proprio perch
indica la capacit di unentit (un oggetto) di avere al suo interno tanti altri oggetti tra
loro collegati che possono essere manipolati (ricercati, aggiunti, rimossi e cos via).
Dal punto di vista della programmazione, finora abbiamo visto e utilizzato larray,
nella sua forma monodimensionale e multidimensionale, come struttura di dati atta a
contenere una serie di elementi. Tuttavia, larray, quantunque il suo utilizzo sia
semplice ed efficiente, ha la pesante limitazione che la quantit massima di elementi
gestibile pu essere indicata solamente allatto della sua definizione (compile-time)
non consentendo a run-time alcuna operazione dinamica di aggiunta o eliminazione
di elementi.
TERMINOLOGIA
Il termine struttura di dati indica il modo in cui i dati sono conservati e organizzati nella memoria e
le operazioni che si possono compiere su di essi (algoritmi). Esso pertanto generalizzabile nella
seguente forma: STRUTTURA DI DATI = INSIEME DI ELEMENTI +vOPERAZIONI.

Al fine di risolvere la situazione descritta, nellambito dellingegneria degli


algoritmi sono state progettate e sviluppate delle soluzioni che si avvalgono di
strutture di dati dinamiche, che consentono di manipolare e gestire a run-time gli
elementi di un contenitore.
Le strutture di dati dinamiche pi comuni sono mostrate nelle Figure 17.1 e 17.2 e
illustrate a seguire.
Figura 17.1 Strutture dati (parte I).

Figura 17.2 Strutture dati (parte II).

La pila (stack), che rappresenta un gruppo di elementi disposti secondo un


criterio LIFO (Last In First Out), in cui lultimo elemento inserito sar anche il
primo a essere gestito. Un esempio reale di stack pu essere una pila di libri:
lultimo libro che si dispone su di essa anche il primo che pu essere preso
senza che tutta la pila di libri cada. Generalmente le operazioni principali sono
push, con cui si aggiunge un nuovo elemento in cima alla pila, e pop, con cui si
rimuove un elemento dalla cima della pila.
La coda (queue), che rappresenta un gruppo di elementi disposti secondo un
criterio FIFO (First In First Out), in cui il primo elemento inserito sar anche il
primo a essere gestito. Un esempio concreto di coda pu essere quello di una fila
di persone che si forma a un ufficio postale per il pagamento di un bollettino di
conto corrente. In questo caso la prima persona della fila sar anche la prima che
pagher il proprio bollettino. Le altre persone che giungeranno presso lo
sportello postale si disporranno in fila a partire dalla fine. Le operazioni
principali sono enqueue, con cui un nuovo elemento aggiunto alla fine della
coda (tail o rear), e dequeue, con cui si rimuove lelemento posto in testa della
coda (front).
La coda doppia (double-ended queue o deque), che rappresenta un gruppo di
elementi lineari come la coda, ma che consente, a differenza di essa, di
aggiungere e rimuovere un elemento indifferentemente allinizio o alla fine della
coda medesima.
La lista collegata o concatenata (linked list), che rappresenta un gruppo di
elementi, definiti anche come nodi della lista, disposti in modo lineare e
sequenziale e atti a formare una collezione ordinata. Ogni nodo della lista
composto sia dal dato che rappresenta, sia da un riferimento verso un altro nodo.
Quando i nodi di una lista hanno un solo riferimento verso un altro nodo
successivo si parla di lista semplicemente collegata, mentre se i nodi hanno due
riferimenti, di cui uno verso un nodo successivo e uno verso un nodo
precedente, si parla di lista doppiamente collegata.
Larray dinamico o vettore o lista di array (dynamic array o vector o array list),
che rappresenta un gruppo di elementi disposti in modo lineare, dove qualsiasi
operazione di inserimento e cancellazione di un elemento attuabile, in modo
arbitrario, in qualsiasi posizione. Questa flessibilit consente a un array list,
rispetto a un array ordinario, di poter cambiare la propria dimensione
dinamicamente durante lesecuzione del programma, crescendo quando si
inserisce un nuovo elemento oppure riducendosi quando lelemento rimosso.
Concetti chiave di un array dinamico sono la capacit (capacity), che
rappresenta la quantit minima di elementi che esso pu contenere, e la
dimensione (size), che rappresenta il numero esatto di elementi che in un dato
momento esso contiene. Ovviamente la capacit sar come minimo uguale alla
dimensione effettiva, ma potr essere anche superiore, se specificato o previsto
dallimplementazione.
Lalbero binario (binary tree), che rappresenta un gruppo di elementi disposti in
modo gerarchico, secondo una relazione padre-figlio, dove ogni elemento padre
pu avere al massimo due elementi figli. Questa struttura di dati definita
albero proprio perch in natura un albero la migliore rappresentazione di una
relazione gerarchica in cui a partire dalle radici si dipanano altri rami ciascuno
dei quali pu a sua volta contenere altri rami. Nellambito di una relazione ad
albero abbiamo i seguenti elementi rappresentativi: radice (root), che
rappresenta il punto di partenza della relazione; padre (parent), che rappresenta
un nodo padre; figlio (child), che rappresenta un nodo figlio; fratello (sibling),
che rappresenta un nodo fratello rispetto a un altro figlio di uno stesso padre;
antenato (ancestor), che rappresenta, in una relazione composta da una certa
profondit, un nodo progenitore (un nonno, bisnonno e cos via); discendente
(descendant), che rappresenta, rispetto a un progenitore, una sorta di nodo
nipote; foglia (leaf), che rappresenta un nodo padre senza nodi figli.
La mappa (map), che rappresenta un gruppo di elementi ciascuno dei quali ha
un valore (value) mappato (collegato, riferito) a una determinata chiave (key)
che ne consente il reperimento. Ogni chiave pu avere solo un valore associato,
ovvero non vi possono essere chiavi duplicate (one-to-one mapping).
Il dizionario (dictionary), che rappresenta un gruppo di elementi simile alla
mappa, dove per sono consentite chiavi associate a valori multipli (one-to-
many mapping).

ATTENZIONE
In Java la classe Dictionary del package java.util rappresenta la struttura di dati dizionario, che
per agisce come una mappa non essendo ammesse chiavi duplicate.

Il grafo (graph), che rappresenta un gruppo di elementi costituiti come un


insieme di vertici (nodi o punti) e un insieme di archi (spigoli, lati o segmenti) e
dove ogni arco connette due vertici. In questo tipo di struttura possiamo
identificare un nodo come il luogo dove memorizzato un dato e un arco
come un segmento che crea una relazione tra i dati che unisce. Cos, per
esempio, potremmo creare un grafo dove ogni vertice rappresenta un soggetto
che naviga nel Web e gli archi rappresentano una coppia di soggetti che hanno
navigato nello stesso sito Internet.
Linsieme (set), che rappresenta un gruppo di elementi dove non possono esserci
elementi uguali e dove non c alcun ordinamento predefinito.
Il framework di Java per le collezioni
Come tutti i moderni linguaggi di programmazione, anche Java offre un
framework completo per la creazione, gestione e utilizzo delle collezioni,
consentendo allo sviluppatore di avvalersi di queste potenti strutture di dati in modo
efficiente, produttivo e standardizzato.
Tra i tanti benefici offerti da un framework di container (riusabilit, qualit,
interoperabilit, scalabilit e cos via), quello pi importante dato dalla possibilit
per il programmatore di disinteressarsi della progettazione e dello sviluppo di
strutture di dati complesse per meglio concentrarsi sugli algoritmi necessari per il
proprio programma, migliorando cos la qualit dello sviluppo software nel suo
complesso.
NOTA STORICA
Le API del collections framework sono state introdotte con il rilascio della versione 2 di Java (1.2).
Prima di allora le uniche strutture di dati presenti nel linguaggio erano gli array, i vettori e le
tabelle hash. A partire dalla versione 5 del linguaggio tutte le strutture di dati del framework sono
state riscritte per avvalersi della potenza, flessibilit e sicurezza dei generici. Dallattuale versione
8 di Java, poi, tale framework stato ulteriormente rinnovato e si evoluto principalmente con:
laggiunta di metodi di default a importanti interfacce quali Collection, List, Map e cos via;
laggiunta di nuovi metodi a importanti classi quali ArrayList o HashMap che accettano come
argomento un tipo funzione; laggiunta di unastrazione denominata stream i cui tipi, attraverso i
loro metodi, consentono di compiere operazioni aggregate su un insieme di dati in modo sia
sequenziale sia parallelo; ladeguamento di alcuni tipi preesistenti quali, per esempio, Arrays e
Collection, con laggiunta di un metodo stream che ritorna unistanza di un determinato tipo
stream.

Il collections framework costituito dai seguenti componenti architetturali:


interfacce, rappresentate dai tipi di dati astratti che modellano le strutture di dati quali
per esempio Set, List, Queue, Map e Deque, appartenenti al package java.util e disposte
secondo la gerarchia della Figura 17.3, che ne mostra un elenco completo;
implementazioni, rappresentate dai tipi di dato che concretamente realizzano le
interfacce, sempre definite nel package java.util, quali HashSet, ArrayList, HashMap,
, , , ,
TreeSet TreeMap LinkedList LinkedHashSet LinkedHashMap e cos via; algoritmi, rappresentati
dalle operazioni che possiamo effettuare sulle strutture dati, come per esempio la
ricerca (searching), lordinamento (sorting), il mescolamento (shuffling) e cos via
(essi sono implementati in appositi metodi statici della classe java.util.Collections);
costrutti di attraversamento delle collezioni, rappresentati dal costrutto for avanzato e
da un oggetto definito iteratore (Iterator).
Figura 17.3 Ereditariet delle interfacce Collection e Map.

Linterfaccia Collection
Linterfaccia Collection rappresenta la massima astrazione del concetto di
collezione; infatti posta in cima, come interfaccia root, a tutta la catena di
ereditariet delle interfacce collezioni.
In pratica essa rappresenta, una sorta di general-purpose collection, costituita da
un gruppo di oggetti denominati elementi che possono essere o meno duplicati
oppure essere o meno ordinati. Nel JDK, infatti, non vi alcuna diretta
implementazione di questinterfaccia, che impiegata sovente come parametro di
metodi cui passare argomenti contenenti tipi di collezioni pi specifici.
Linterfaccia Collection fornisce le seguenti dichiarazioni di metodi che indicano le
operazioni che, come minimo, unimplementazione di una collezione dovrebbe
offrire.

boolean add(E e) aggiunge lelemento indicato dal parametro e. Ritorna true se


laggiunta ha avuto esito favorevole e false in caso contrario (per esempio se
limplementazione non consente valori duplicati). In pi, se laggiunta
dellelemento fallisce per una ragione diversa da quella prima indicata, allora
limplementazione deve generare unapposita eccezione software e non, quindi,
il valore false.
boolean addAll(Collection<? extends E> c) aggiunge tutti gli elementi della collezione
c alla collezione corrente.
void clear() rimuove tutti gli elementi di una collezione.
boolean contains(Object o) verifica se una collezione contiene lelemento indicato
dal parametro o.
boolean containsAll(Collection<?> c) verifica se una collezione contiene tutti gli
elementi contenuti nella collezione fornita dal parametro c.
boolean isEmpty() verifica se una collezione vuota, ovvero non ha elementi.
Iterator<E> iterator() ritorna un oggetto iteratore di tipo Iterator per scorrere gli
elementi di una collezione. Questo oggetto consente di scorrere gli elementi di
una collezione solamente in avanti e di rimuovere lultimo elemento ritornato
dalliterazione.
boolean remove(Object o) rimuove da una collezione, se presente, lelemento

rappresentato dal parametro o.


boolean removeAll(Collection<?> c) rimuove da una collezione tutti gli elementi
indicati dalla collezione fornita dal parametro c.
boolean retainAll(Collection<?> c) rimuove da una collezione tutti gli elementi che
non sono presenti nella collezione fornita dal parametro c oppure, per dirla in
altro modo, conserva solo quegli elementi che sono presenti nella collezione c.
int size() ritorna il numero di elementi presenti in una collezione.
Object[] toArray() ritorna una rappresentazione come array di oggetti degli
elementi di una collezione.
<T> T[] toArray(T[] a) ritorna una rappresentazione come array di oggetti del tipo

specificato dal parametro a di una collezione. In pratica questo metodo consente


di ottenere un array in cui gli elementi, presi da una collezione, sono di uno
specifico tipo (String, Integer e cos via) e non del tipo Object come si visto per il
precedente metodo toArray. Per esempio, listruzione String[] as =
collection.toArray(new String[0]) , posto che il riferimento collection di tipo
Collection<String> , creer un array dove ogni elemento sar di tipo String cos
come gli elementi di cui la collezione collection.

TERMINOLOGIA
Riguardo i metodi containsAll, addAll, removeAll, retainAll e clear si dice che compiono
operazioni bulk, ovvero operazioni che agiscono su tutti gli elementi di una collezione.

Linterfaccia Set
Linterfaccia Set rappresenta una collezione di elementi che non pu contenere
elementi duplicati; modella il concetto matematico di insieme algebrico, e infatti i
seguenti metodi dovrebbero essere implementati in modo che eseguano le relative
operazioni matematiche di unione, intersezione e differenza e la relazione di
sottoinsieme.

boolean containsAll(Collection<?> c) ritorna true solo se la collezione fornita dal


parametro c un sottoinsieme della collezione che invoca il metodo. In pratica,
se abbiamo una collezione insieme A, la collezione insieme B sottoinsieme di A
solo se A contiene tutti gli elementi di B.
boolean addAll(Collection<? extends E> c) aggiunge tutti gli elementi della collezione
c alla collezione che invoca il metodo in modo da unire le collezioni. In pratica,
data la collezione insieme A e la collezione insieme B, loperazione di unione
dar come risultato una collezione formata da tutti gli elementi, presi una sola
volta, di entrambi gli insiemi.
boolean retainAll(Collection<?> c) rimuove tutti gli elementi della collezione che

invoca il metodo che non sono presenti nella collezione c. In pratica, il metodo
esegue unoperazione di intersezione in cui, data la collezione insieme A e la
collezione insieme B, gli elementi considerati saranno solo quelli comuni alle
due collezioni.
rimuove tutti gli elementi dalla collezione che
boolean removeAll(Collection<?> c)

invoca il metodo che sono presenti anche nella collezione c. In pratica, il metodo
effettua unoperazione di differenza asimmetrica dove, data la collezione
insieme A e la collezione insieme B, la collezione risultante sar quella avente
solo gli elementi di A che non appartengono anche a B.

I rimanenti metodi dellinterfaccia Set sono uguali a quelli dellinterfaccia Collection


e pertanto non richiedono un approfondimento specifico.

Linterfaccia SortedSet
Linterfaccia SortedSet rappresenta una collezione di elementi senza duplicati,
ordinati secondo un ordinamento ascendente naturale oppure secondo uno arbitrario
fornito mediante un oggetto comparatore (Comparator). Estende, di fatto, linterfaccia
fornendo i seguenti ulteriori metodi.
Set
Comparator<? super E> comparator() ritorna loggetto comparatore utilizzato per
lordinamento degli elementi di un insieme. Se non stato fornito alcun oggetto
comparatore, il metodo ritorner null.
E first() ritorna lelemento di un insieme al primo posto nellordinamento.
E last() ritorna lelemento di un insieme allultimo posto nellordinamento.
SortedSet<E> subSet(E fromElement, E toElement) ritorna un sottoinsieme di un insieme
formato dagli elementi compresi nel range indicato dal parametro fromElement
(incluso) e dal parametro toElement (escluso).
SortedSet<E> headSet(E toElement) ritorna un sottoinsieme di un insieme formato
dagli elementi che, secondo lordinamento, sono posti prima dellelemento
fornito dal parametro toElement (escluso).
SortedSet<E> tailSet(E fromElement) ritorna un sottoinsieme di un insieme formato
dagli elementi che, secondo lordinamento, sono posti dopo (o a partire da)
lelemento fornito dal parametro fromElement (incluso).

NOTA
Tutte le operazioni di modifica effettuate nei sottoinsiemi ritornati dai metodi subSet, headSet e
tailSet sono riflesse nellinsieme originario e viceversa.

Linterfaccia NavigableSet
Linterfaccia NavigableSet rappresenta una collezione di elementi senza duplicati,
ordinati secondo gli stessi criteri discussi per linterfaccia SortedSet da cui deriva, ma
con i seguenti metodi che consentono di effettuare operazioni di ricerca e
reperimento degli elementi in base alla pi vicina corrispondenza rispetto a un
elemento scelto.

E ceiling(E e) ritorna il pi piccolo elemento di un insieme che sia maggiore o


uguale allelemento fornito dal parametro e.
E floor(E e) ritorna il pi grande elemento di un insieme che sia minore o uguale
allelemento fornito dal parametro e.
E higher(E e) ritorna il pi piccolo elemento di un insieme che sia maggiore
dellelemento fornito dal parametro e.
E lower(E e) ritorna il pi grande elemento di un insieme che sia minore
dellelemento fornito dal parametro e.
Iterator<E> descendingIterator() ritorna un iteratore che scorre gli elementi di un
insieme dal pi grande al pi piccolo.
NavigableSet<E> descendingSet() ritorna un insieme navigabile con gli elementi

disposti in ordine discendente.


NavigableSet<E> headSet(E toElement, boolean inclusive) ritorna un sottoinsieme di un
insieme formato dagli elementi che, secondo lordinamento, sono posti prima (o
finiscono con, se il parametro inclusive true) lelemento fornito dal parametro
toElement .
NavigableSet<E> tailSet(E fromElement, boolean inclusive) ritorna un sottoinsieme di un
insieme formato dagli elementi che, secondo lordinamento, sono posti dopo (o
a partire da, se il parametro inclusive true) lelemento fornito dal parametro
fromElement .
, ,
NavigableSet<E> subSet(E fromElement boolean fromInclusive E toElement boolean ,
toInclusive) ritorna un sottoinsieme di un insieme formato dagli elementi
compresi nel range indicati dal parametro fromElement (incluso se il parametro
fromInclusive true) e dal parametro toElement (incluso se il parametro toInclusive
true).
E pollFirst() rimuove e restituisce il pi piccolo elemento da un insieme.
E pollLast() rimuove e restituisce il pi grande elemento da un insieme.

NOTA
Tutte le operazioni di modifica effettuate negli insiemi o sottoinsiemi ritornati dai metodi
descendingSet, headSet, tailSet e subSet sono riflesse nellinsieme originario e viceversa.

Linterfaccia List
Linterfaccia List rappresenta una collezione di elementi ordinati sequenzialmente
dove possono esserci anche elementi duplicati. Oltre ai metodi ereditati
dallinterfaccia Collection, dispone anche dei seguenti metodi.

void add(int index, E element) aggiunge nella lista lelemento del parametro element
alla posizione del parametro index. Lelemento posizionato allindice index e i
successivi sono spostati verso destra e il valore del loro indice incrementato di
uno.
boolean addAll(int index, Collection<? extends E> c) aggiunge in una lista gli elementi

della collezione fornita dal parametro c a partire dalla posizione indicata dal
parametro index. Anche qui, come visto per il metodo add, lelemento che si trova
alla posizione index e i successivi sono shiftati verso destra e i loro index sono
incrementati di uno. In pi, utile sapere che gli elementi della collezione c sono
inseriti nella lista nello stesso ordine di quello ritornato dal loro iteratore.
E get(int index) ritorna lelemento di una lista posizionato allindice indicato dal

parametro index.
int indexOf(Object o) ritorna la posizione nella lista della prima occorrenza
dellelemento indicato dal parametro o. Se lelemento non trovato il valore
ritornato sar -1.
ritorna la posizione nella lista dellultima occorrenza
int lastIndexOf(Object o)

dellelemento indicato dal parametro o. Se lelemento non trovato il valore


ritornato sar -1.
ritorna un iteratore per laccesso in una lista di tipo
ListIterator<E> listIterator()

che consente, rispetto a un iteratore di tipo Iterator, di scorrere gli


ListIterator

elementi della lista in entrambe le direzioni, di aggiungere un elemento, di


modificare un elemento e di reperire le posizioni dei successivi o precedenti
elementi che si otterranno.
E remove(int index) rimuove lelemento di una lista posizionato allindice indicato

dal parametro index. Gli elementi successivi a quellindice sono spostati verso
sinistra e il valore del loro indice decrementato di uno. Infine, lelemento
rimosso ritornato al chiamante.
E set(int index, E element) sostituisce lelemento posizionato dove indicato dal

parametro index con lelemento indicato dal parametro element. Lelemento


rimpiazzato ritornato al chiamante.
ritorna una sottolista formata dagli
List<E> subList(int fromIndex, int toIndex)

elementi compresi nel range indicato dal parametro fromIndex (incluso) e dal
parametro toIndex (escluso). Tutte le operazioni di modifica effettuate nella
sottolista sono riflesse nelle lista originaria e viceversa.

Linterfaccia Queue
Linterfaccia Queue rappresenta una collezione di elementi disposti secondo un
ordinamento che generalmente di tipo FIFO. Ci significa che implementazioni
particolari di questa interfaccia possono proporre criteri di ordinamento differenti,
rispettando per il principio che nella fase di rimozione di un elemento quello
eliminato sia sempre posizionato in testa alla coda.
Rispetto allinterfaccia Collection da cui eredita, linterfaccia Queue offre i seguenti
metodi.

E element() ritorna lelemento posto in testa a una coda senza rimuoverlo. Se la


coda vuota il metodo lancer uneccezione di tipo NoSuchElementException.
Possiamo utilizzare lequivalente metodo peek che per, se la coda vuota,
ritorner il valore null.
boolean offer(E e) inserisce in una coda lelemento indicato dal parametro e solo
se linserimento attuabile. Tale metodo differisce dallequivalente metodo add
in quanto, se lelemento non stato inserito (per esempio perch
limplementazione della coda ha posto dei vincoli sulla capacit massima di
elementi inseribili), non lancer alcuna eccezione ritornando semplicemente il
valore false.
E remove() ritorna lelemento posto in testa a una coda, rimuovendolo. Se la coda
vuota il metodo lancer uneccezione di tipo NoSuchElementException. Esiste un
metodo equivalente poll che per, se la coda vuota, ritorner il valore null.

Linterfaccia Deque
Linterfaccia Deque (pronunciata deck) rappresenta una collezione di elementi
lineari simile allinterfaccia Queue, ma con la differenza che un elemento pu essere
aggiunto o rimosso sia in testa sia alla fine della coda.
Linterfaccia Deque eredita dallinterfaccia Queue e offre, in pi, i seguenti metodi.

void addFirst(E e) aggiunge in testa a una doppia coda lelemento fornito dal
parametro e. Se limplementazione della doppia coda prevede dei vincoli sulla
capacit massima di elementi inseribili, preferibile utilizzare il metodo
equivalente offerFirst, che ritorner il valore false se lelemento non inseribile,
mentre il metodo addFirst lancer leccezione IllegalStateException.
void addLast(E e) aggiunge alla fine di una doppia coda lelemento fornito dal
parametro e. Se limplementazione della doppia coda prevede dei vincoli sulla
capacit massima di elementi inseribili, preferibile utilizzare il metodo
equivalente offerLast, che ritorner il valore false se lelemento non inseribile,
mentre il metodo addLast lancer leccezione IllegalStateException.
E removeFirst() restituisce, rimuovendolo, lelemento posto in testa alla doppia
coda. possibile utilizzare lequivalente metodo pollFirst, che non lancer
leccezione NuSuchElementException se la coda vuota, ma ritorner il valore null.
E removeLast() restituisce, rimuovendolo, lelemento posto alla fine della doppia
coda. possibile utilizzare lequivalente metodo pollLast, che non lancer
leccezione NuSuchElementException se la coda vuota, ma ritorner il valore null.
E getFirst() restituisce, senza rimuoverlo, lelemento posto in testa alla doppia
coda. possibile utilizzare lequivalente metodo peekFirst, che non lancer
leccezione NuSuchElementException se la coda vuota, ma ritorner il valore null.
restituisce, senza rimuoverlo, lelemento posto alla fine della doppia
E getLast()

coda. possibile utilizzare lequivalente metodo peekLast, che non lancer


leccezione NuSuchElementException se la coda vuota, ma ritorner il valore null.
boolean removeFirstOccurrence(Object o) rimuove dalla doppia coda la prima
occorrenza dellelemento uguale alloggetto o passato come argomento.
boolean removeLastOccurrence(Object o) rimuove dalla doppia coda lultima
occorrenza dellelemento uguale alloggetto o passato come argomento.

Linterfaccia Map
Linterfaccia Map rappresenta un insieme di elementi (o valori) ciascuno dei quali
associato a una chiave che lo riferisce. Ricordiamo che una mappa non pu avere
chiavi duplicate e che ogni chiave pu riferire solamente un valore. Ogni coppia
chiave/valore definita anche entry. Questa interfaccia ha i seguenti metodi.

V put(K key, V value) inserisce in una mappa il valore fornito dal parametro value
associandogli la relativa chiave fornita dal parametro key. Se la mappa contiene
gi un valore per la chiave key, allora lo stesso sar sostituito dal nuovo valore
value e il vecchio valore sar ritornato dal metodo.
void putAll(Map<? extends K,? extends V> m) inserisce in una mappa tutte le coppie
chiave/valore della mappa fornita dal parametro m.
V get(Object key) ritorna il valore associato dalla chiave indicata dal parametro key.
Se la chiave non presente sar ritornato il valore null.
V remove(Object key) rimuove lassociazione della chiave di cui il parametro key
ritornando il relativo valore oppure il valore null se la chiave non presente.
void clear() rimuove da una mappa tutte le coppie chiave/valore.
int size() ritorna la quantit di coppie chiave/valore presenti in una mappa.
boolean containsKey(Object key) verifica se una mappa contiene la chiave indicata
dal parametro key.
boolean containsValue(Object value) verifica se una mappa contiene il valore indicato
dal parametro value.
boolean isEmpty() verifica se una mappa vuota.
Collection<V> values() ritorna una collezione di tipo Collection di tutti i valori di una
mappa. Qualunque cambiamento effettuato sulla vista ritornata influenzer la
relativa mappa e viceversa.
Set<Map.Entry<K,V>> entrySet() ritorna una collezione di tipo Set di tutte le coppie

chiave/valore di una mappa i cui elementi sono di tipo Entry<K, V>. Qualunque
cambiamento effettuato sulla vista ritornata influenzer la relativa mappa e
viceversa.

DETTAGLIO
Il tipo Entry<K, V> uninterfaccia definita allinterno dellinterfaccia Map che astrae il concetto di
coppia chiave/valore. Dispone dei seguenti metodi: getKey per ottenere la chiave dellentry,
getValue per ottenere il valore dellentry e setValue che consente di modificare, con un nuovo
valore, il valore dellattuale entry.

Set<K> keySet() ritorna una collezione di tipo Set di tutte le chiavi di una mappa.
Qualunque cambiamento effettuato sulla vista ritornata influenzer la relativa
mappa e viceversa.

NOTA
Quando utilizziamo un oggetto di tipo Iterator su una delle viste ritornate dai metodi values,
entrySey e keySet, lunica operazione supportata durante uniterazione che si ripercuoter sulla
relativa mappa sar la rimozione di un elemento tramite il suo metodo remove. Nella vista ritornata
dal metodo entrySet, durante uniterazione, sar anche possibile cambiare il valore di unentry
con il metodo setValue. Inoltre, le stesse viste, indipendentemente da uniterazione, potranno
sempre rimuovere un elemento dalla relativa mappa con i propri metodi di rimozione (remove,
removeAll e cos via), ma mai aggiungerne di nuovi.

Linterfaccia SortedMap
Linterfaccia SortedMap rappresenta una mappa i cui elementi sono ordinati secondo
un ordinamento ascendente naturale oppure secondo uno arbitrario specificato
mediante un oggetto comparatore (Comparator). Essa estende, di fatto, linterfaccia Map
fornendo in pi i seguenti metodi.
Comparator<? super K> comparator() ritorna loggetto comparatore utilizzato per
lordinamento delle chiavi di una mappa. Se non stato fornito alcun oggetto
comparatore, il metodo ritorner null.
K firstKey() ritorna la chiave di una mappa nella prima posizione
nellordinamento.
K lastKey() ritorna la chiave di una mappa nellultima posizione

nellordinamento.
SortedMap<K,V> subMap(K fromKey, K toKey) ritorna una sottomappa di una mappa
formata da tutte quelle entry le cui key, secondo lordinamento, sono comprese
nel range indicato dal parametro fromKey (incluso) e dal parametro toKey (escluso).
SortedMap<K,V> headMap(K toKey) ritorna una sottomappa di una mappa formata da
tutte quelle entry le cui key, secondo lordinamento, sono poste prima della
chiave fornita dal parametro toKey (escluso).
SortedMap<K,V> tailMap(K fromKey) ritorna una sottomappa di una mappa formata da
tutte quelle entry le cui key, secondo lordinamento, sono poste dopo (o a partire
da) la chiave fornita dal parametro fromKey (incluso).

NOTA
Tutte le operazioni di modifica effettuate nelle sottomappe ritornate dai metodi subMap, headMap e
tailMap sono riflesse nella mappa originaria e viceversa.

Linterfaccia NavigableMap
Linterfaccia NavigableMap rappresenta una mappa ordinata secondo gli stessi criteri
discussi per linterfaccia SortedMap da cui deriva, ma con i seguenti metodi che
consentono di effettuare operazioni di ricerca e reperimento di entry o di key in base
alla pi vicina corrispondenza rispetto a una key scelta.

Map.Entry<K,V> ceilingEntry(K key) ritorna unentry di una mappa la cui pi piccola


chiave maggiore o uguale alla chiave fornita dal parametro key.
K ceilingKey(K key) ritorna la pi piccola chiave che sia maggiore o uguale alla
chiave fornita dal parametro key.
Map.Entry<K,V> floorEntry(K key) ritorna unentry di una mappa la cui pi grande
chiave minore o uguale alla chiave fornita dal parametro key.
K floorKey(K key) ritorna la pi grande chiave che sia minore o uguale alla chiave
fornita dal parametro key.
Map.Entry<K,V> higherEntry(K key) ritorna unentry di una mappa la cui pi piccola
chiave maggiore della chiave fornita dal parametro key.
K higherKey(K key) ritorna la pi piccola chiave che sia maggiore della chiave
fornita dal parametro key.
Map.Entry<K,V> lowerEntry(K key) ritorna unentry di una mappa la cui pi grande
chiave minore della chiave fornita dal parametro key.
K lowerKey(K key) ritorna la pi grande chiave che sia minore della chiave fornita
dal parametro key.
NavigableMap<K,V> descendingMap() ritorna una mappa navigabile con le entry disposte
in ordine discendente, ossia dalla pi grande (maggiore) alla pi piccola
(minore) chiave.
NavigableMap<K,V> headMap(K toKey, boolean inclusive) ritorna una sottomappa di una

mappa formata da tutte quelle entry le cui key, secondo lordinamento, sono
poste prima (o finiscono con, se il parametro inclusive true) della chiave fornita
dal parametro toKey.
NavigableMap<K,V> tailMap(K fromKey, boolean inclusive) ritorna una sottomappa di una
mappa formata da tutte quelle entry le cui key, secondo lordinamento, sono
poste dopo (o a partire da, se il parametro inclusive true) la chiave fornita dal
parametro fromKey.
, , ,
NavigableMap<K,V> subMap(K fromKey boolean fromInclusive K toKey boolean toInclusive)

ritorna una sottomappa di una mappa formata da tutte quelle entry le cui chiavi
sono comprese nel range di chiavi indicato dai parametri fromKey (incluso, se il
parametro fromInclusive true) e dal parametro toKey (incluso, se il parametro
toInclusive true).
Map.Entry<K,V> pollFirstEntry() rimuove e restituisce lentry associata alla pi
piccola chiave di una mappa. Per ottenere lentry senza rimuoverla, si pu
utilizzare il metodo firstEntry.
Map.Entry<K,V> pollLastEntry() rimuove e restituisce lentry associata alla pi grande
chiave di una mappa. Se si vuole solo ottenere lentry senza rimuoverla si pu
utilizzare il metodo lastEntry.

NOTA
Tutte le operazioni di modifica effettuate nelle mappe o sottomappe ritornate dai metodi
descendingMap, headMap, tailMap e subMap sono riflesse nella mappa originaria e viceversa.
Implementazioni dellinterfaccia Set
Il collections framework mette a disposizione le seguenti classi, tutte non
sincronizzate nei riguardi di un accesso concorrente da parte di pi thread, che
realizzano linterfaccia Set.

HashSet : sviluppata avvalendosi della struttura di dati di tipo mappa, a sua volta
implementata come tabella hash (hash table). Questa classe non garantisce alcun
ordinamento e ci significa che, se compiamo uniterazione su un oggetto di tipo
HashSet, gli elementi ottenuti non avranno alcun ordine prestabilito, ovvero

saranno ritornati senza un particolare criterio di disposizione.

APPROFONDIMENTO
Una tabella hash (Figura17.4) una modalit implementativa di strutture di dati che associa
chiavi a valori e i cui elementi fondamentali sono: larray dove vengono memorizzati dei valori,
detto bucket array, dove ogni cella un contenitore di unentry formata da una coppia
chiave/valore; una funzione hash, che ha il compito di associare a ogni chiave arbitraria un
numero intero che rappresenta lindice dellarray dove verr memorizzato il valore relativo.

TreeSet : sviluppata avvalendosi della struttura di dati di tipo albero binario (in
particolare un red-black tree), che garantisce un ordinamento ascendente degli
elementi.
LinkedHashSet: sviluppata avvalendosi della struttura di dati di tipo lista

doppiamente collegata e di tipo mappa con hash table. Questa classe garantisce
un ordinamento uguale allordine dinserimento degli elementi (insertion-order).
EnumSet: sviluppata avvalendosi della struttura di dati di tipo vettore. Rappresenta

unimplementazione specializzata per lutilizzo con i tipi enumerativi i cui


elementi sono le costanti enumerative definite in un tipo enum. Lordinamento
degli elementi relativo allordine in cui le costanti enumerative sono state
dichiarate nel tipo enum. Inoltre non possibile avere elementi nulli.
Figura 17.4 Rappresentazione di una hash table.
Listato 17.1 Classe SetImplementations.
...
public class SetImplementations
{
// un'enumerazione per i giorni della settimana
private enum DaysOfTheWeek
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

// stampa tutti gli elementi di una collezione


public static void printAllCollectionElements(Collection<String> c, String from)
{
System.out.println("Tutte le keywords di Java dalla collezione di tipo " + from);

int sep = 1;
System.out.println("[");
for (String k : c)
{
System.out.print(k + " ");
if (sep % 10 == 0)
System.out.println();
sep++;
}
System.out.println("]");
}

public static void main(String args[])


{
String java_keywords[] =
{
"abstract", "continue", "for", "new", "switch", "assert", "default", "goto",
"package", "synchronized", "boolean", "do", "if", "private", "this", "break",
"double", "implements", "protected", "throw", "byte", "else", "import",
"public", "throws", "case", "enum", "instanceof", "return", "transient",
"catch", "extends", "int", "short", "try", "char", "final", "interface",
"static", "void", "class", "finally", "long", "strictfp", "volatile", "const",
"float", "native", "super", "while"
};

Set<String> hs_keywords = new HashSet<>(25, 0.6f); // HashSet


for (String s : java_keywords) // aggiungo gli elementi all'insieme
hs_keywords.add(s);

Set<String> ts_keywords = new TreeSet<>(hs_keywords); // TreeSet

Collection<String> a_c = Arrays.asList(java_keywords); // LinkedHashSet


Set<String> ls_keywords = new LinkedHashSet<>(a_c);

// manda in stampa gli elementi delle collezioni in successione


printAllCollectionElements(hs_keywords, "HashSet");
printAllCollectionElements(ts_keywords, "TreeSet");
printAllCollectionElements(ls_keywords, "LinkedHashSet");

// crea un EnumSet con tutti i giorni della settimana


EnumSet<DaysOfTheWeek> all = EnumSet.allOf(DaysOfTheWeek.class);

// crea un EnumSet vuoto


EnumSet<DaysOfTheWeek> none = EnumSet.noneOf(DaysOfTheWeek.class);

// crea un EnumSet con i giorni tra mercoled (incluso) e venerd (incluso)
EnumSet<DaysOfTheWeek> range = EnumSet.range(DaysOfTheWeek.WEDNESDAY,
DaysOfTheWeek.FRIDAY);

// crea un EnumSet composto solo con gli elementi passati come argomento
EnumSet<DaysOfTheWeek> only_with = EnumSet.of(DaysOfTheWeek.MONDAY,
DaysOfTheWeek.THURSDAY,
DaysOfTheWeek.SUNDAY);

// mostra i giorni inseriti come elementi nell'EnumSet only_with


System.out.print("Elementi dell'enum set only_with: [ ");
for(DaysOfTheWeek dw : only_with)
{
System.out.print(dw + " ");
}
System.out.println("]");
}
}

Il Listato 17.1 mostra, primariamente, come creare oggetti collezione che


implementano linterfaccia Set attraverso un semplice programma che stampa gli
elementi di un array di stringhe contenente tutte le keyword di Java, dopo che sono
stati aggiunti a tutti gli oggetti collezione definiti. In particolare abbiamo definito
quanto segue.

Un oggetto di tipo HashSet (hs_keywords), avvalendoci del costruttore che prende


come argomento la capacit iniziale di contenimento, che nel nostro caso pari
a 25, e un valore tra 0.0 e 1.0, denominato load factor e impostato a 0.6, che serve
per calcolare dopo quanti elementi inseriti verr automaticamente aumentata la
capacit della struttura di dati; tale numero di elementi viene calcolato
moltiplicando il valore del load factor per la capacit dellHashSet. Cos nel nostro
caso loggetto HashSet subir inizialmente un aumento di capacit (verr
raddoppiata a 50) quando il numero di elementi inseriti sar pi grande o uguale
del valore 15 (25 * 0.6). Tale capacit determiner, poi, un nuovo limite a 30
elementi inseriti (50 * 0.6); lo stesso procedimento verr attuato ogni volta che
sar raggiunta la soglia di rapporto. In effetti il load factor pu essere anche letto
come la percentuale di riempimento (nel nostro caso del 60%) raggiunta la quale
la struttura di dati verr ridimensionata. Dopo linizializzazione delloggetto
hs_keywords gli elementi inseriti saranno disposti senza un particolare ordine.

Un oggetto di tipo TreeSet (ts_keywords), avvalendoci del costruttore che consente


di prendere come argomento un altro oggetto collezione da cui ottenere gli
elementi. Dopo linizializzazione delloggetto ts_keywords gli elementi inseriti
saranno disposti secondo il loro naturale ordinamento.
Un oggetto di tipo LinkedHashSet (ls_keywords), avvalendoci del costruttore che
consente di prendere come argomento un altro oggetto collezione da cui ottenere
gli elementi. In questo caso loggetto collezione passato come argomento stato
ottenuto utilizzando il metodo statico asList della classe Arrays del package
, che converte gli elementi di un array in un oggetto collezione di tipo
java.util

. Dopo linizializzazione delloggetto ls_keywords gli elementi inseriti saranno


List

disposti sequenzialmente secondo lordine di inserimento.

Per la stampa degli elementi delle collezioni succitate abbiamo creato il metodo
statico printAllCollectionElements, che in modo polimorfico consente di stampare gli
elementi di qualsiasi oggetto collezione passato come argomento.
Il medesimo listato mostra, infine, lutilizzo della classe EnumSet che opera
sullenumerazione DaysOfTheWeek. In dettaglio, importante precisare che tale classe
non ha dei costruttori con cui creare un determinato tipo ma ha dei metodi factory
statici come, per esempio: allOf(Class<E> elementType), per creare un enum set
contenente tutti gli elementi del tipo elementType; noneOf(Class<E> elementType), per creare
un enum set vuoto del tipo elementType; range(E from, E to), per creare un enum set
contenente gli elementi compresi nel range tra i due endpoint from e to; of(E first, E
, per creare un enum set con lelemento first ed eventualmente con gli atri
rest)

elementi forniti da rest.

Nel nostro caso creiamo gli enum set: all, che conterr come elementi tutte le
costanti enumerative dellenumerazione DaysOfTheWeek; none, che non conterr alcun
elemento dellenumerazione DaysOfTheWeek (rappresenta, quindi, un enum set vuoto
predisposto a contenere gli elementi di DaysOfTheWeek); range, che conterr come
elementi le costanti enumerative dellenumerazione DaysOfTheWeek che vanno da
DaysOfTheWeek.WEDNESDAY a DaysOfTheWeek.FRIDAY entrambe incluse; only_with, che conterr
come elementi le costanti enumerative dellenumerazione DaysOfTheWeek espressamente
indicate come argomento.
ATTENZIONE
Nel Listato 17.1 ogni riferimento delloggetto implementazione scelto stato passato a una
variabile di tipo Set<String> piuttosto che a una specifica implementazione. Questa tecnica
consente di scrivere codice flessibile e generico dove, ogniqualvolta vogliamo cambiare
implementazione, sufficiente cambiare soltanto il costruttore relativo, mentre le altri parti del
programma che utilizzano linterfaccia possono restare invariate.
Output 17.1 Dal Listato 17.1 della Classe SetImplementations.

Tutte le keyword di Java dalla collezione di tipo HashSet


[
for char package long float break byte boolean import volatile
...
]
Tutte le keyword di Java dalla collezione di tipo TreeSet
[
abstract assert boolean break byte case catch char class const
...
]
Tutte le keyword di Java dalla collezione di tipo LinkedHashSet
[
abstract continue for new switch assert default goto package synchronized
...
]
Elementi dell'enum set only_with: [ SUNDAY MONDAY THURSDAY ]

LOutput 17.1 mostra il risultato dellesecuzione del programma. Si vede come: in


un HashSet gli elementi non siano ordinati; in un TreeSet gli elementi siano ordinati in
modo ascendente; in un LinkedHashSet gli elementi siano ordinati secondo lordine di
inserimento; in un EnumSet gli elementi siano ordinati secondo lordine con cui le
costanti enumerative sono state dichiarate nella corrispondente enumerazione (nel
nostro caso DaysOfTheWeek).

Implementazioni dellinterfaccia List


Il collections framework mette a disposizione le seguenti classi, tutte non
sincronizzate nei riguardi di un accesso concorrente da parte di pi thread, che
realizzano linterfaccia List.

ArrayList : sviluppata avvalendosi della struttura di dati di tipo array dinamico, i


cui elementi possono aumentare o diminuire a run-time. Ogni oggetto di tipo
ArrayList ha una capacity che rappresenta lo spazio allocato per contenere gli

elementi dellarray e una size che rappresenta il numero di elementi che esso
effettivamente contiene. Quando il numero di elementi aggiunti supera la
capacit dellarray, questultima viene automaticamente incrementata e lo stesso
array viene ricreato.
LinkedList : sviluppata avvalendosi della struttura di dati di tipo lista doppiamente
collegata. Anche in questo caso, come per larray list, la struttura dinamica e
cresce o decresce a seconda dellinserimento o delleliminazione di elementi.

Listato 17.2 ListImplementations.


...
public class ListImplementations
{
public static void main(String args[])
{
List<Integer> al_numbers = new ArrayList<>(10); // ArrayList
List<Integer> ll_numbers = new LinkedList<>(); // LinkedList

for (int i = 0; i < 10; i++) // aggiungo 10 numeri casuali all'ArrayList


al_numbers.add(i, new Random().nextInt(20));

System.out.println("Numeri presenti nella lista ArrayList " + al_numbers);

for (int i = 0; i < 10; i++) // aggiungo 10 numeri casuali alla LinkedList
ll_numbers.add(new Random().nextInt(20));

System.out.println("Numeri presenti nella lista LinkedList " + ll_numbers);

// rimuovo dalla lista al_numbers i numeri eventualmente presenti


// nella lista ll_numbers
al_numbers.removeAll(ll_numbers);

System.out.println("Dopo la rimozione numeri presenti nella lista ArrayList "


+ al_numbers);
}
}

Output 17.2 Dal Listato 17.2 Classe ListImplementations.


Numeri presenti nella lista ArrayList [5, 10, 19, 1, 4, 1, 8, 19, 2, 17]
Numeri presenti nella lista LinkedList [13, 3, 6, 10, 17, 1, 16, 13, 10, 9]
Dopo la rimozione numeri presenti nella lista ArrayList [5, 19, 4, 8, 19, 2]

Il Listato 17.2 crea un oggetto di tipo ArrayList utilizzando il relativo costruttore,


che prende come argomento un intero indicante la capacit iniziale dellarray. Nel
nostro caso decidiamo di costruire loggetto al_numbers che avr una capacit
contenitiva di 10 elementi. Successivamente vi aggiungiamo 10 numeri casuali e li
stampiamo a video. Continuando la disamina del listato vediamo che viene creata
unaltra lista utilizzando limplementazione LinkedList. Anche alloggetto ll_numbers di
tipo LinkedList aggiungiamo 10 numeri casuali e li stampiamo a video. Infine,
sulloggetto al_numbers invochiamo il metodo removeAll, che rimuove tutti gli elementi
in esso presenti e uguali agli elementi presenti nella lista ll_numbers passata come
argomento.

ARRAYLIST E LINKEDLIST A CONFRONTO


Limplementazione di una lista con la classe ArrayList consente, rispetto allimplementazione con
la classe LinkedList, di utilizzare i metodi ensureCapacity e trimToSize, che permettono,
rispettivamente, di aumentare manualmente la capacit di contenimento minima dellarray e di
impostare una capacit pari al numero di elementi presenti (dimensione). In ogni caso, a parte la
differenza indicata, la decisione se scegliere luna o laltra implementazione dipende dalle
performance e dalle operazioni pi frequentemente effettuate dallapplicazione sviluppata. Per
esempio, mentre una LinkedList pi efficiente nei casi di inserimento e cancellazione degli
elementi, perch la lista viene modificata agendo solo sul riordinamento dei collegamenti tra i nodi,
un ArrayList lo nelle operazioni di accesso e ottenimento degli elementi tramite lindicizzazione,
perch si procede in modo arbitrario (direttamente alla posizione indicata) e non sequenziale. Per
quanto riguarda, invece, la complessit computazionale degli algoritmi usati, espressi secondo la
notazione O-grande, avremo per le comuni operazioni con un ArrayList: get O(1), add O(1),
contains O(n), remove O(n), mentre per quelle di una LinkedList avremo: get O(n), add O(1),
contains O(n), remove O(n). Chiaramente, le magnitudini delle funzioni O-grande descritte, non
tengono conto della presenza di possibili iteratori.

Implementazioni delle interfacce Queue e Deque


Il collections framework mette a disposizione le seguenti classi, tutte non
sincronizzate nei riguardi di un accesso concorrente da parte di pi thread, che
realizzano le interfacce Queue e Deque

ArrayDeque : sviluppata avvalendosi della struttura di dati di tipo array dinamico


che cresce o decresce a seconda delle necessit. Inoltre, non ha restrizioni di
capacit e gli elementi null non sono ammessi.
LinkedList : sviluppata avvalendosi della struttura di dati di tipo lista doppiamente
collegata, gi vista come implementazione del tipo List.
PriorityQueue : sviluppata avvalendosi di una particolare implementazione della
struttura di dati di tipo albero binario bilanciato, definita priority heap. In un
oggetto di tipo PriorityQueue gli elementi sono disposti in base a una priorit che
pu essere relativa al loro ordinamento naturale oppure a un ordine specificato
da un oggetto comparatore personalizzato. Inoltre, in questo tipo di coda,
lelemento pi piccolo posto in testa.

Listato 17.3 Classe QueueDequeImplementations.


...
public class QueueDequeImplementations
{
public static void main(String args[])
{
int numbers_to_put[] = {12, 44, 10, 0, -1, 4, 33, -10, -9, 100};

Queue<Integer> numbers = new PriorityQueue<>(); // PriorityQueue


for (int i = 0; i < numbers_to_put.length; i++) // aggiungo dei numeri alla coda
numbers.offer(numbers_to_put[i]);

Integer elem = numbers.poll();



while (elem != null)
{
System.out.print(elem + " "); // mostro i numeri della coda
// secondo la loro priorit
elem = numbers.poll();
}
System.out.println();
}
}

Output 17.3 Dal Listato 17.3 della Classe QueueDequeImplementations.

-10 -9 -1 0 4 10 12 33 44 100

Il Listato 17.3 mostra la creazione delloggetto numbers, che rappresenta una coda a
priorit, dove sono inseriti gli elementi rappresentati dai numeri indicati dallarray
numbers_to_put. Successivamente, per verificare che la priorit stata realizzata

sullordine naturale dei numeri, che per default ascendente (dal pi piccolo al pi
grande), otteniamo e visualizziamo a video i numeri tramite il metodo poll.

Implementazioni dellinterfaccia Map


Il collections framework mette a disposizione le seguenti classi, tutte non
sincronizzate nei riguardi di un accesso concorrente da parte di pi thread, che
realizzano linterfaccia Map.

HashMap: sviluppata avvalendosi della struttura di dati di tipo mappa a sua volta
implementata come tabella hash. Non garantisce alcun ordinamento e consente
valori e chiavi nulle.
TreeMap: sviluppata avvalendosi della struttura di dati di tipo albero binario (red-

black tree). Garantisce un ordinamento naturale degli elementi (in base ai valori
delle chiavi) oppure un ordinamento fornito da un apposito comparatore.
LinkedHashMap: sviluppata avvalendosi della struttura di dati di tipo lista

doppiamente collegata e di tipo mappa con hash table. Si pone come struttura
intermedia tra un tipo HashMap e un tipo TreeMap, garantendo un ordinamento
corrispondente allordine di inserimento delle chiavi e una velocit
computazionale inferiore a un tipo TreeMap.
WeakHashMap: sviluppata avvalendosi della struttura di dati di tipo mappa, a sua
volta implementata come tabella hash. In questo tipo di implementazione, a
differenza dellimplementazione HashMap, quando una chiave non pi utilizzata,
la relativa entry automaticamente rimossa dalla mappa.
IdentityHashMap: sviluppata avvalendosi della struttura di dati di tipo mappa, a sua

volta implementata come tabella hash. Questo tipo di implementazione


consente, a differenza delle altre implementazioni di una mappa, di comparare
gli elementi per riferimento anzich per valore. Ci significa che, quando si
inserir una entry in una mappa di tipo IndetityHashMap, la verifica se una chiave
sia gi stata inserita verr effettuata utilizzando loperatore == anzich il metodo
equals, e ci comporter che, per esempio, se inseriremo due chiavi con lo stesso
nome che punteranno a oggetti differenti il test di verifica sulleventuale
duplicazione di chiave verr eseguito sui loro riferimenti e non sui valori a cui
saranno collegate.
EnumMap: sviluppata avvalendosi della struttura di dati di tipo array. Rappresenta

unimplementazione specializzata per lutilizzo con i tipi enumerativi. Infatti, i


valori delle sue chiavi sono ottenuti dalle costanti del tipo enum indicato allo
scopo, e il loro ordinamento uguale a quello con cui le predette costanti sono
state dichiarate. Inoltre non possibile avere chiavi nulle.

Listato 17.4 Classe MapImplementations.


...
public class MapImplementations
{
private enum ProgrammingLanguages
{
JAVA, CPP, JAVASCRIPT, CSHARP
}

public static void main(String args[])


{
Map<String, String> m_city_regions = new HashMap<>(); // HashMap

// aggiungo le citt
m_city_regions.put(new String("Napoli"), "Campania");
m_city_regions.put(new String("Salerno"), "Campania");
m_city_regions.put(new String("Caserta"), "Campania");
m_city_regions.put(new String("Avellino"), "Campania");
m_city_regions.put(new String("Benevento"), "Lombardia"); // ATTENZIONE errore
// regione
m_city_regions.put(new String("Benevento"), "Campania"); // ... lo sostituisco
// con questo

System.out.println("Citt della Campania contenute in un HashMap:\n"


+ m_city_regions);

Map<String, String> im_city_regions = new IdentityHashMap<>(); // IdentityHashMap

// aggiungo le citt
im_city_regions.put(new String("Napoli"), "Campania");
im_city_regions.put(new String("Salerno"), "Campania");
im_city_regions.put(new String("Caserta"), "Campania");
im_city_regions.put(new String("Avellino"), "Campania");
im_city_regions.put(new String("Benevento"), "Lombardia"); // ATTENZIONE errore
// regione

// ... lo sostituisco con questo ma non funziona e l'entry duplicata!!!
im_city_regions.put(new String("Benevento"), "Campania");

System.out.println("Citt della Campania contenute in un IdentityHashMap:\n"


+ im_city_regions);

// creo un tipo EnumMap con le key corrispondenti alle costanti enumerative


// dell'enumerazione ProgrammingLanguages
Map<ProgrammingLanguages, String> emap = new
EnumMap<>(ProgrammingLanguages.class);
emap.put(ProgrammingLanguages.JAVASCRIPT, "Brendan Eich");
emap.put(ProgrammingLanguages.JAVA, "James Gosling");
emap.put(ProgrammingLanguages.CSHARP, "Anders Hejlsberg");
emap.put(ProgrammingLanguages.CPP, "Bjarne Stroustrup");

// mostro tutte le entry


System.out.println("Entry di emap:\n" + emap);
}
}

Output 17.4 Dal Listato 17.4 Classe MapImplementations.

Citt della Campania contenute in un HashMap:


{Salerno=Campania, Benevento=Campania, Avellino=Campania, Caserta=Campania, Napoli=Campania}
Citt della Campania contenute in un IdentityHashMap:
{Salerno=Campania, Benevento=Lombardia, Avellino=Campania, Caserta=Campania, Benevento=Campania,
Napoli=Campania}
Entry di emap:
{JAVA=James Gosling, CPP=Bjarne Stroustrup, JAVASCRIPT=Brendan Eich, CSHARP=Anders Hejlsberg}

Il Listato 17.4 mette in evidenza, inizialmente, come creare una mappa di tipo
HashMap e di tipo IdentityHashMap. In concreto vengono creati loggetto m_city_regions di

tipo HashMap e loggetto im_city_regions di tipo IdentityHashMap, che conterranno entrambi


le citt della regione Campania.
Tra i due oggetti c una differenza importante: mentre il primo non consentir di
avere due chiavi duplicate riferite alla citt Benevento, perch la comparazione avverr
sul contenuto della chiave, la seconda consentir di avere come chiave duplicata la
citt Benevento, perch la comparazione sar effettuata sulloggetto a cui la chiave si
riferir; tale oggetto, anche se conterr lo stesso valore, sar ovviamente differente.
Infine, il medesimo listato mostra come creare un tipo EnumMap a partire
dallenumerazione ProgrammingLanguages che ha come chiavi le costanti enumerative
della predetta enumerazione (rappresentano dei popolari linguaggi di
programmazione) e come valori dei tipi String (indicano i nomi dei progettisti dei
corrispettivi linguaggi).

Le interfacce Comparable e Comparator


Molte delle implementazioni che abbiamo visto, per esempio quelle fornite dalle
classi TreeSet e TreeMap, consentono un ordinamento naturale e logico sugli elementi
che contengono. Ci possibile perch il linguaggio Java fornisce automaticamente
per i tipi come String, Integer, Long, Date e cos via, una definizione del metodo compareTo,
dellinterfaccia Comparable implementata, attraverso la quale si indicano le modalit di
comparazione tra elementi.
Linterfaccia Comparable parte del package java.lang e il metodo compareTo ha la
segnatura int compareTo(T o), dove loggetto chiamante comparato con loggetto
fornito dal parametro o e il risultato della comparazione pu essere un intero con un
valore minore di 0, uguale a 0 o maggiore di 0 se, rispettivamente, loggetto chiamante
minore, uguale o maggiore delloggetto del parametro o.
Listato 17.5 ComparableWithLanguageTypes.

...
public class ComparableWithLanguageTypes
{
public static void main(String args[])
{
Set<Integer> numbers = new TreeSet<>(); // TreeSet di numeri
numbers.add(44);
numbers.add(-11);
numbers.add(2);
System.out.println("Numeri ordinati: " + numbers);

Set<String> strings = new TreeSet<>(); // TreeSet di stringhe


strings.add("Principe");
strings.add("Alonso");
strings.add("Russo");
System.out.println("Stringhe ordinate: " + strings);

Set<GregorianCalendar> dates = new TreeSet<>(); // TreeSet di date


dates.add(new GregorianCalendar(2010, 11, 10));
dates.add(new GregorianCalendar(1999, 1, 12));
dates.add(new GregorianCalendar(2006, 10, 11));
StringBuilder ordered_dates = new StringBuilder("Date ordinate: [");
for (GregorianCalendar c : dates)
{
String data = c.get(Calendar.DAY_OF_MONTH) + "/" + c.get(Calendar.MONTH) + "/"
+ c.get(Calendar.YEAR)+ ", ";
ordered_dates.append(data);
}
ordered_dates.delete(ordered_dates.length()-2, ordered_dates.length());
ordered_dates.append("]");
System.out.println(ordered_dates);
}
}

Output 17.5 Dal Listato 17.5 Classe ComparableWithLanguageTypes.

Numeri ordinati: [-11, 2, 44]


Stringhe ordinate: [Alonso, Principe, Russo]
Date ordinate: [12/1/1999, 11/10/2006, 10/11/2010]

Il Listato 17.5 crea tre oggetti di tipo TreeSet che rappresentano, in sequenza, un
insieme di elementi di tipo intero, di tipo stringa e di tipo data, tutti gi ordinati.
Infatti loggetto numbers contiene elementi di tipo intero ordinati numericamente,
loggetto strings contiene elementi di tipo stringa ordinati lessicograficamente e
loggetto dates contiene elementi di tipo data ordinati cronologicamente.

Linterfaccia Comparable non , ovviamente, solo ad appannaggio dei tipi del


linguaggio, ma utilizzabile anche con le classi che rappresentano i tipi creati per le
nostre applicazioni.
Il Listato 17.6 mostra come creare una classe dotata di una propria logica di
comparazione tra oggetti del suo tipo e come tali oggetti vengono aggiunti in un
TreeSet ordinati secondo tale logica.

Listato 17.6 Classe ComparableWithCustomTypes.


...
class Employee implements Comparable<Employee> // una classe che modella un impiegato
{
private Integer _id;
private GregorianCalendar _hiring_date;

public Employee(int id, GregorianCalendar hiring_date)


{
_id = id;
_hiring_date = hiring_date;
}

public int getId() { return _id; }

public GregorianCalendar getHiringDate() { return _hiring_date; }

// se gli impiegati sono stati assunti nella stessa data allora considera anche l'id
public int compareTo(Employee o)
{
int res = _hiring_date.compareTo(o._hiring_date);
if (res == 0)
res = _id.compareTo(o._id);
return res;
}

public String toString()


{
return _id + " assunto in data " + _hiring_date.get(Calendar.DAY_OF_MONTH) + "/"
+ _hiring_date.get(Calendar.MONTH) + "/"
+ _hiring_date.get(Calendar.YEAR);
}
}

public class ComparableWithCustomTypes


{
public static void main(String args[])
{
Set<Employee> employees = new TreeSet< >(); // TreeSet di impiegati

employees.add(new Employee(4200, new GregorianCalendar(2005, 10, 1)));


employees.add(new Employee(1250, new GregorianCalendar(2005, 10, 1)));
employees.add(new Employee(3058, new GregorianCalendar(2005, 10, 1)));
employees.add(new Employee(100, new GregorianCalendar(2008, 5, 1)));
employees.add(new Employee(3058, new GregorianCalendar(2001, 11, 1)));

System.out.println(employees);
}
}

Output 17.6 Dal Listato 17.6 Classe ComparableWithCustomTypes.


[3058 assunto in data 1/11/2001, 1250 assunto in data 1/10/2005, 3058 assunto in data 1/10/2005,
4200 assunto in data 1/10/2005, 100 assunto in data 1/5/2008]

Il Listato 17.6 crea la classe Employee che implementa linterfaccia Comparable e ne


definisce il metodo compareTo in modo che gli oggetti di questa classe possano essere
ordinati considerando la data di assunzione degli impiegati e, ove vi siano date di
assunzione uguali, anche secondo il numero identificativo.
Analizzando il metodo compareTo possiamo notare come sia la data di assunzione sia
lidentificativo siano comparati utilizzando il metodo compareTo, che per gli oggetti di
tipo intero (_id) e di tipo data (_hiring_date) messo a disposizione dal linguaggio; ci
ci consente, in questo caso, di non dover scrivere una specifica logica di
comparazione.

COMPARETO E IL METODO EQUALS


In alcuni casi pu essere opportuno implementare il metodo compareTo in modo consistente con il
metodo equals affinch, dati per esempio due oggetti x e y, linvocazione del metodo
x.compareTo(y) == 0 dia lo stesso valore booleano (true o false) dellinvocazione del metodo
x.equals(y). Nel listato esaminato in precedenza, nella classe Employee non vi sar congruenza tra
il metodo compareTo e il metodo equals (ereditato da Object), perch questultimo effettuer un test
di eguaglianza sui riferimenti di due oggetti Employee, mentre nel nostro caso la comparazione
verter sulla data di assunzione ed eventualmente anche sul codice identificativo. Nel caso della
nostra applicazione, tale mancanza non determiner alcun problema, ma in altri casi sar sempre
bene valutare la necessit o lopportunit di implementare tale corrispondenza.

Oltre allinterfaccia Comparable, sin qui esaminata, esiste anche linterfaccia


Comparator , che utile impiegare quando vogliamo dotare di una logica di
comparazione delle classi che non implementano di per s linterfaccia Comparable,
oppure quando vogliamo fornire una logica di comparazione differente per classi che,
di default, implementano attraverso linterfaccia Comparable un ordinamento naturale
degli elementi.
Linterfaccia Comparator parte del package java.util e fornisce il seguente metodo
da implementare per fornire una propria logica di comparazione: int compare(T o1, T
o2), dove loggetto indicato dal parametro o1 comparato con loggetto fornito dal
parametro o2 e il risultato della comparazione pu essere un intero con un valore
minore di 0, uguale a 0 o maggiore di 0 se, rispettivamente, loggetto o1 minore,
uguale o maggiore delloggetto del parametro o2.
Listato 17.7 Classe ComparatorWithLanguageTypes.
...
public class ComparatorWithLanguageTypes
{
public static void main(String args[])
{
// TreeSet di numeri con un comparatore custom
Set<Integer> numbers = new TreeSet<>(new Comparator<Integer>()
{
public int compare(Integer o1, Integer o2) { return o2.compareTo(o1); }
});

numbers.add(44);
numbers.add(-11);
numbers.add(2);

System.out.println("Numeri ordinati in ordine inverso: " + numbers);


}
}

Output 17.7 Dal Listato 17.7 Classe ComparatorWithLanguageTypes.


Numeri ordinati in ordine inverso: [44, 2, -11]

Il Listato 17.7 mostra come la creazione e lattribuzione di un oggetto comparatore


passino attraverso le seguenti fasi: creazione di un oggetto che implementa
linterfaccia Comparator e definizione del metodo compare; assegnamento delloggetto
Comparator creato come argomento del costruttore della classe collezione utilizzata.

Nel nostro caso loggetto comparatore creato consentir di sovrascrivere


lordinamento naturale ascendente effettuato comunemente sui numeri dal TreeSet con
quello da noi definito nel metodo compare, che preveder invece un ordinamento degli
elementi invertito, ovvero discendente.

Le interfacce Iterator, ListIterator e Iterable


Come abbiamo visto fin qui, gli elementi di una collezione possono essere ottenuti
avvalendosi di un semplice ciclo che utilizzi il costrutto for avanzato.
In alternativa possiamo comunque ricorrere a particolari oggetti, definiti iteratori
(forniti dagli oggetti collezione), che consentono di iterare attraverso gli elementi di
una collezione e anche di apportare modifiche agli elementi stessi.
In particolare, un oggetto che implementa linterfaccia Iterator, del package
, ha i seguenti metodi.
java.util

boolean hasNext() ritorna true se la collezione ha un successivo elemento.


E next() ritorna il successivo elemento della collezione.
void remove() rimuove lultimo elemento ritornato dalliteratore.

Un oggetto di tipo ListIterator (che discende dalla classe Iterator ed disponibile


solo da oggetti di tipo List), appartenente al package java.util, ha, oltre ai metodi gi
esaminati per un Iterator, anche i seguenti metodi.

void add(E e) aggiunge lelemento indicato dal parametro e nella lista corrente. Se
nella lista sono presenti altri elementi, lelemento e inserito prima del
successivo elemento che sarebbe ritornato dal metodo next e dopo il precedente
elemento che sarebbe ritornato dal metodo previous.
void set(E e) modifica lultimo elemento ritornato dai metodi next o previous con
lelemento specificato dal parametro e.
boolean hasPrevious() ritorna true se la collezione ha un precedente elemento.
E previous() ritorna il precedente elemento della collezione.
int nextIndex() ritorna lindice del prossimo elemento che sarebbe ritornato dal
metodo next.
int previousIndex() ritorna lindice del precedente elemento che sarebbe ritornato
dal metodo previous.

CURSORI
Un oggetto di tipo ListIterator ha un cursore che rappresenta una sorta di indicatore per la
posizione dove literatore si trover in un determinato momento durare literazione. Questa
posizione non sar mai sullelemento da processare, ma sar sempre nel mezzo di due elementi
(Figura 17.5).

Figura 17.5 Cursore di un ListIterator.

Per spiegare la Figura 17.5 consideriamo che cosa accadrebbe se invocassimo i


seguenti metodi a partire dal cursore posizionato allindice 2:

hasNext ritornerebbe true perch c lelemento C;


hasPrevious ritornerebbe true perch c lelemento B;
next ritornerebbe lelemento C;
previous ritornerebbe lelemento B;
nextIndex ritornerebbe lindice 2;
previousIndex ritornerebbe lindice 1;
add inserirebbe un elemento tra lelemento C e lelemento B e prima del cursore;
set modificherebbe lelemento C se prima fosse invocato il metodo next,
altrimenti modificherebbe lelemento B se prima fosse invocato il metodo
previous ;
remove eliminerebbe lelemento C se prima fosse invocato il metodo next,
altrimenti eliminerebbe lelemento B se prima fosse invocato il metodo previous.

ATTENZIONE
I metodi set e remove agiscono sempre sullelemento corrente ritornato dal metodo next o
previous, mentre il metodo add agisce sulla posizione corrente del cursore.
Listato 17.8 Classe Iterators.

...
public class Iterators
{
public static void main(String args[])
{
String os[] = {"GNU/Linux","Windows 7","Solaris","Amiga OS","FreeBSD","Mac OS X"};

// Lista di sistemi operativi


LinkedList<String> al_operating_systems = new LinkedList<>(Arrays.asList(os));
// oggetto ListIterator
ListIterator<String> it_os = al_operating_systems.listIterator();

System.out.println("Sistemi operativi partendo dall'inizio della lista");
// parte dall'inizio
while (it_os.hasNext())
System.out.print(it_os.next() + " ");

System.out.println("\nSistemi operativi partendo dalla fine della lista");


// parte dalla fine
while (it_os.hasPrevious())
System.out.print(it_os.previous() + " ");

// modifica e aggiunta di elementi


System.out.println("\nSistemi operativi partendo dall'inizio della lista" +
"con modifiche");

while (it_os.hasNext())
{
String n_e = it_os.next();
if (n_e.contains("Solaris"))
{
it_os.set(n_e + " OS 10");

n_e = it_os.previous(); // ritorniamo indietro e poi avanti


// per far vedere la modifica!
n_e = it_os.next();
}

if (n_e.contains("FreeBSD"))
{
System.out.print(n_e + " "); // stampa FreeBSD
it_os.add("OpenBSD"); // aggiungo un elemento nuovo

n_e = it_os.previous(); // ritorniamo indietro e poi avanti
// per far vedere l'aggiunta!
n_e = it_os.next();
}

System.out.print(n_e + " ");


}
System.out.println();
}
}

Output 17.8 Dal Listato 17.8 Classe Iterators.

Sistemi operativi partendo dall'inizio della lista


GNU/Linux Windows 7 Solaris Amiga OS FreeBSD Mac OS X
Sistemi operativi partendo dalla fine della lista
Mac OS X FreeBSD Amiga OS Solaris Windows 7 GNU/Linux
Sistemi operativi partendo dall'inizio della lista con modifiche
GNU/Linux Windows 7 Solaris OS 10 Amiga OS FreeBSD OpenBSD Mac OS X

Il Listato 17.8 mostra la creazione di una lista di tipo LinkedList che contiene una
serie di voci relative ai sistemi operativi. Dalla lista al_operating_systems invochiamo il
metodo listIterator, che ritorner un iteratore che ci consentir di iterare attraverso gli
elementi della lista e di effettuare anche operazioni di modifica.
Infatti, grazie a tale iteratore eseguiamo uniterazione dallinizio della lista,
uniterazione dalla fine della lista e uniterazione allinterno della quale
modifichiamo lelemento Solaris e aggiungiamo lelemento OpenBSD.

In conclusione, linterfaccia Iterable, appartenente al package java.lang, consente di


dotare le classi che la implementano della possibilit che i loro oggetti siano utilizzati
direttamente allinterno di un costrutto for avanzato, il quale scandir
automaticamente, tramite un iteratore fornito da un apposito metodo iterator, gli
elementi di un oggetto indicato (generalmente un array).
Listato 17.9 Classe MyIterable.

...
class Numbers implements Iterable<Integer>
{
private int nrs[];

public Numbers(int how_many)


{
nrs = new int[how_many];
doRandomNr(); // genera numeri casuali da mettere in nrs
}

private void doRandomNr()


{
Random rnd = new Random();

for (int n = 0; n < nrs.length; n++)


nrs[n] = rnd.nextInt(100);
}

public Iterator<Integer> iterator()


{
// creo un oggetto che implementa l'interfaccia Iterator
return new Iterator<Integer>()
{
private int pos = 0;

public boolean hasNext() { return pos >= nrs.length ? false : true; }


public Integer next() { return nrs[pos++]; }
public void remove()
{
throw new UnsupportedOperationException("Not supported yet.");
}
};
}
}

public class MyIterable


{
public static void main(String args[])
{
Numbers nrs_obj = new Numbers(5); // creo un oggetto Numbers con 5 elementi

for (Number n : nrs_obj) // ciclo i suoi elementi


System.out.print(n + " ");
}
}

Output 17.9 Dal Listato 17.9 Classe MyIterable.

33 25 10 99 55
Il Listato 17.9 crea la classe Numbers, che implementa linterfaccia Iterable e
definisce il relativo metodo iterator che ritorner un oggetto di tipo Iterator con i
metodi hasNext, next e remove utilizzati per gestire larray nrs.

Successivamente, allinterno della classe MyIterable creiamo loggetto nrs_obj di tipo


Numbers con 5 elementi che viene utilizzato direttamente allinterno del ciclo for che
scandir gli elementi del suo array nrs.

Di fatto, il ciclo for avanzato opera in modo trasparente, perch il compilatore ha


sostituito la sua sintassi con quella mostrata nel seguente Decompilato 17.1, dove ha
utilizzato le consuete invocazioni dei metodi di un oggetto iteratore.
Decompilato 17.1 File MyIterable.class.

...
public class MyIterable
{
public MyIterable() {}
public static void main(String args[])
{
Numbers nrs_obj = new Numbers(5);
Number n;
for (Iterator i$ = nrs_obj.iterator(); i$.hasNext(); System.out.println(n))
n = (Number) i$.next();
}
}

Algoritmi polimorfici sulle collezioni


La classe Collections, appartenente al package java.util, contiene, tra gli altri, i
seguenti metodi statici che consentono di effettuare le comuni operazioni di
ordinamento, ricerca, mescolamento e cos via.

public static <T extends Comparable<? super T>> void sort(List<T> list) ordina in modo
naturale e ascendente gli elementi passati dalla lista del parametro list.
public static <T> void sort(List<T> list, Comparator<? super T> c) ordina, nelle
modalit indicate dal comparatore del parametro c, gli elementi passati dalla lista
del parametro list.
public static void shuffle(List<?> list) mescola (disordina) gli elementi della
lista del parametro list utilizzando una sorgente di casualit predefinita.
public static void shuffle(List<?> list, Random rnd) mescola (disordina) gli
elementi della lista del parametro list utilizzando la sorgente di casualit fornita
dal parametro rnd.
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key)

effettua una ricerca dellelemento del parametro key nella lista del parametro
list. La lista fornita deve gi essere ordinata. Se lelemento key trovato, il
metodo ritorner un valore numerico indicante la sua posizione allinterno della
lista, altrimenti ritorner un valore negativo calcolato secondo la seguente
espressione: (-(insertion point) - 1) oppure, in modo pi leggibile: -(insertion
point + 1) , dove insertion point rappresenta la posizione allinterno della lista dove
sarebbe stato inserito lelemento oggetto della ricerca. (In pratica, la ragione di
questespressione risiede nel fatto che, poich un insertion point pari a 0
rappresenterebbe un risultato valido, si deve negare il risultato e aggiungere -1
per eliminare tale ambiguit e far intendere che lelemento non stato trovato.)
possibile utilizzare anche il metodo in overloading public static <T> int
, ,
binarySearch(List<? extends T> list T key Comparator<? super T> c) , che effettua una
ricerca sugli elementi ordinati secondo loggetto comparatore del parametro c.
public static void reverse(List<?> list) inverte lordine degli elementi della lista del
parametro list.
public static void rotate(List<?> list, int distance) ruota gli elementi della lista del
parametro list secondo il valore indicato dal parametro distance con la seguente
espressione: (ix + distance) % list.size(), dove ix rappresenta lindice
dellelemento da ruotare. Inoltre, se il valore di distance negativo, allora gli
elementi saranno ruotati allindietro.

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T>

coll) ritorna il pi grande elemento presente nella collezione del parametro coll
secondo un ordinamento naturale. possibile utilizzare il metodo in
overloading: public static <T> T max(Collection<? extends T> coll, Comparator<? super T>
comp) trova lelemento massimo tra gli elementi ordinati secondo le regole
delloggetto comparatore del parametro comp.

public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T>

coll) ritorna il pi piccolo elemento presente nella collezione del parametro coll
secondo un ordinamento naturale. possibile utilizzare il metodo in
overloading: public static <T> T min(Collection<? extends T> coll, Comparator<? super T>
comp) trova lelemento minimo tra gli elementi ordinati secondo le regole
delloggetto comparatore del parametro comp.
public static void swap(List<?> list, int i, int j) scambia, nella lista indicata dal
parametro list, gli elementi inseriti alle posizioni indicate dai parametri i e j.
Listato 17.10 Classe Algorithms.

...
public class Algorithms
{
public static void main(String args[])
{
String os[] = {"GNU/Linux","Windows 7","Solaris","Amiga OS","FreeBSD",
"Mac OS X"};

LinkedList<String> al_operating_systems = new LinkedList<>(Arrays.asList(os));


// Lista di sistemi operativi
Collections.sort(al_operating_systems); // ordina la collezione
System.out.print("Collezione ordinata: ");
System.out.println(al_operating_systems);

// ricerca l'elemento Be OS
int ix = Collections.binarySearch(al_operating_systems, "Be OS", null);
if (ix < 0)// se non presente aggiungilo
al_operating_systems.add(-(ix + 1), "Be OS");

System.out.print("Collezione con elemento Be OS aggiunto: ");
System.out.println(al_operating_systems);
Collections.rotate(al_operating_systems, 3); // ruota gli elementi di 3 posizioni
System.out.print("Collezione con elementi ruotati: ");
System.out.println(al_operating_systems);

System.out.print("Elemento della collezione minore: ["); // elemento pi piccolo


System.out.println(Collections.min(al_operating_systems) + "]");

System.out.print("Elemento della collezione maggiore: ["); // elemento pi grande


System.out.println(Collections.max(al_operating_systems) + "]");
}
}

Output 17.10 Dal Listato 17.10 Classe Algorithms.

Collezione ordinata: [Amiga OS, FreeBSD, GNU/Linux, Mac OS X, Solaris, Windows 7]


Collezione con elemento Be OS aggiunto: [Amiga OS, Be OS, FreeBSD, GNU/Linux, Mac OS X, Solaris,
Windows 7]
Collezione con elementi ruotati: [Mac OS X, Solaris, Windows 7, Amiga OS, Be OS, FreeBSD,
GNU/Linux]
Elemento della collezione minore: [Amiga OS]
Elemento della collezione maggiore: [Windows 7]

Il Listato 17.10 mostra come utilizzare alcuni degli algoritmi esaminati in


precedenza. Alloggetto collezione al_operating_systems sono applicate le operazioni di
ordinamento, ricerca binaria, rotazione e ricerca del minimo e del massimo elemento.
Per quanto attiene alla ricerca binaria utile illustrare passo passo
limplementazione per chiarire meglio il risultato della computazione.

1. Si cerca lelemento Be OS con il metodo binarySearch. Il risultato della ricerca


ritorna nella variabile ix il valore -2 indicante il fallimento della ricerca e anche
la posizione dove, secondo lordinamento naturale, sarebbe stato inserito Be OS se
fosse stato presente. In dettaglio, il risultato dato dalla valutazione
dellespressione. (-(insertion point) - 1), dove insertion point 1 perch Be OS nella
lista ordinata sarebbe andato nella seconda posizione dopo lelemento Amiga OS
(la lista parte dallindice 0).
2. Si verifica che lelemento non stato trovato e lo si aggiunge nella lista alla
corretta posizione ordinata, grazie alla valutazione dellespressione: -(ix + 1) che
dar come valore 1 perch, ricordiamo, ix varr -2.

Collezioni concorrenti
Gli oggetti collezioni sin qui studiati sono tutti thread-unsafe: se pi thread
accedono simultaneamente a una struttura di dati la stessa pu risultare corrotta,
poich non esiste alcun meccanismo di lock e di sincronizzazione. Al fine di evitare
questo importante problema, Java mette a disposizione le seguenti interfacce, con
relative implementazioni, appartenenti al package java.util.concurrent:

interfaccia BlockingQueue con le implementazioni fornite dalle classi


, , ,
ArrayBlockingQueue DelayQueue LinkedBlockingDeque LinkedBlockingQueue ,
,
LinkedTransferQueue PriorityBlockingQueue e SynchronousQueue;
interfaccia BlockingDeque con limplementazione fornita dalla classe
LinkedBlockingDeque ;
interfaccia ConcurrentMap con le implementazioni fornite dalla classi
ConcurrentHashMap e ConcurrentSkipListMap;
ConcurrentNavigableMap con limplementazione fornita dalla classe
ConcurrentSkipListMap .

Oltre alle implementazioni citate, possiamo utilizzare anche le seguenti classi:


ConcurrentLinkedQueue, ConcurrentSkipListSet, CopyOnWriteArrayList, CopyOnWriteArraySet,

,
ConcurrentSkipListMap ConcurrentLinkedDeque e ConcurrentHashMap.

Come possiamo vedere dallelenco indicato, i progettisti di Java hanno realizzato


una vasta serie di classi che consentono di utilizzare le pi comuni strutture di dati.
Tuttavia, se abbiamo la necessit che una data collezione sia thread-safe e ci
accorgiamo che essa non nativamente supportata da una particolare
implementazione concorrente, possiamo utilizzare i seguenti metodi statici della
classe Collections, che forniscono un meccanismo generale di sincronizzazione per
ogni tipo di collezione.

public static <T> Collection<T> synchronizedCollection(Collection<T> c) ritorna una


collezione sincronizzata della collezione fornita dal parametro c.
public static <T> List<T> synchronizedList(List<T> list) ritorna una collezione
sincronizzata della collezione di tipo lista fornita dal parametro list.
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) ritorna una collezione
sincronizzata della collezione di tipo mappa fornita dal parametro m.
public static <T> Set<T> synchronizedSet(Set<T> s) ritorna una collezione
sincronizzata della collezione di tipo insieme fornita dal parametro s.
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) ritorna una
collezione sincronizzata della collezione di tipo mappa ordinata fornita dal
parametro m.
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s) ritorna una
collezione sincronizzata della collezione di tipo insieme ordinato fornita dal
parametro s.

ATTENZIONE
Per utilizzare correttamente le collezioni sincronizzate, fornite dai metodi appena elencati,
occorre ricordare: di utilizzare sempre i metodi delle collezioni ritornate e non quelli delle
collezioni passate come argomenti; di chiudere in un blocco synchronized le operazioni di
iterazione, sia che vengano effettuate attraverso un oggetto iteratore o attraverso un ciclo for
avanzato, poich gli oggetti iteratori non vengono sincronizzati automaticamente (Snippet 17.1).
Snippet 17.1 Iterazione sincronizzata.
...
LinkedList<String> al_operating_systems = new LinkedList<>(Arrays.asList(os));
List<String> sync_al_operating_systems = Collections.synchronizedList(al_operating_systems);
synchronized (sync_al_operating_systems)
{
Iterator<String> it = sync_al_operating_systems.iterator();
while (it.hasNext())
{
// fai qualcosa
}
}

NOTA
Fino alla versione 1.2 di Java si potevano normalmente utilizzare le classi thread-safe Vector
(array dinamico) e Hashtable (hash table), oggi sostituite rispettivamente dalle classi non thread-
safe ArrayList e HashMap.
Stream
Le lambda expression, come in pi parti evidenziato, sono state introdotte nel
linguaggio Java per consentire agli sviluppatori di approcciare il problem solving in
un modo differente, ancorch complementare, a quello offerto dalla comune
programmazione ad oggetti. Esse, in pratica, permettono di dotare il codice di una
certa espressivit, chiarezza e semplicit grazie alla loro caratteristica di esprimere
cosa si desidera da una computazione piuttosto di come tale computazione deve
essere eseguita.
Tuttavia, c unaltra importante ragione che ha spinto lintroduzione delle lambda
expression e che strettamente legata alla necessit di far evolvere e modernizzare
limportante libreria delle collezioni cos che le operazioni di accesso e
manipolazione dei relativi dati possano avvenire sfruttando la concorrenza e il
parallelismo in modo efficiente, semplificato e con gli idiomi della programmazione
funzionale (in pratica, allorquando si utilizzer una collezione, si indicher, tramite
una funzionalit, solo cosa deve essere computato sugli elementi della stessa e mai,
dunque, come tale computazione dovr essere eseguita, il cui dettaglio
implementativo sar nascosto e lasciato alla discrezione del progettista della relativa
API).
Quanto detto, dal punto di vista delle API delle collezioni, si concretizzato con
lintroduzione di una nuova astrazione denominata stream, che rappresenta una
sequenza di elementi su cui possibile compiere operazioni aggregate, sia
sequenziali sia parallele.

Il package java.util.stream
Il package java.util.stream contiene i seguenti tipi fondamentali che consentono di
utilizzare lastrazione stream.

public interface Stream<T> extends BaseStream<T,Stream<T>> extends AutoCloseable indica


una sequenza di elementi di un determinato tipo T.
public interface IntStream extends BaseStream<Integer,IntStream> indica una sequenza di
elementi di tipo int.
indica una sequenza di
public interface LongStream extends BaseStream<Long,LongStream>

elementi di tipo long.


public interface DoubleStream extends BaseStream<Double,DoubleStream> indica una
sequenza di elementi di tipo double.
public interface Collector<T,A,R> indica unoperazione di riduzione mutabile per
accumulare degli elementi di input in un container di risultato mutabile.
public final class Collectors extends Object unimplementazione di Collector che

fornisce diverse utili operazioni di riduzione.

Concetti preliminari
Posto il seguente sorgente (Listato 17.11):
Listato 17.11 Classe StreamTutorial.
package com.pellegrinoprincipe;
...

enum Colors { RED, BLUE, BLACK, YELLOW, GREEN, WHITE }

class Car
{
private String model;
private Colors color;
private int code;

public Car(String model, Colors color, int code)


{
this.model = model;
this.color = color;
this.code = code;
}

public String getModel() { return model; }


public Colors getColor() { return color; }
public int getCode() { return code; }
public String toString() { return getModel(); }
public boolean equals(Object obj)
{
return ((Car)obj).getModel().equals(this.getModel());
}
public int hashCode(){ return model.hashCode(); }
}

public class StreamTutorial


{
public static int nr;

public static void makeAverage(Collection<Car> cars)


{
// trova tutte le macchine con il colore BLACK e per ognuna
// ritorna il codice e poi ne fa la media
OptionalDouble average = cars.stream()
.filter(car -> car.getColor() == Colors.BLACK) // operazione intermedia
.mapToInt(car -> car.getCode()) // operazione intermedia
.average(); // operazione terminale

System.out.println("La media dei codici degli elementi di tipo Car : " +


average.getAsDouble());
}

public static void makeExternalIteration(Collection<Car> cars)


{
int nr = 0;

// utilizzo esplicito di iterator per mostrare l'iterazione esterna


// quanto scritto come il compilatore traduce il seguente for avanzato:
// for (Car car : cars) { if (car.getColor() == Colors.BLACK) nr++; }
Iterator<Car> iterator = cars.iterator();
while (iterator.hasNext())
{
Car car;
if ((car = iterator.next()).getColor() != Colors.BLACK)
continue;
++nr;
}
System.out.println("Nella collezione corrente ci sono " + nr +
" macchine con il colore BLACK");
}

public static void makeInternalIteration(Collection<Car> cars)


{
// utilizzo del metodo forEach del tipo Stream che usa un'iterazione
// interna per processare gli elementi della collezione
// in base alla funzionalit fornita dalla lambda expression indicata
cars.stream()
.forEach(car -> // operazione terminale
{
if (car.getColor() == Colors.BLACK)
nr++;
});
System.out.println("Nella collezione corrente ci sono " + nr +
" macchine con il colore BLACK");
}

public static void shortCircuiting(Collection<Car> cars)


{
// qui il metodo findFirst eager ma pu usare uno short-circuiting
// nel momento in cui trova la prima macchina BLACK
// findFirst ritorna un tipo Optional<Car> che rappresenta un oggetto container
// che pu o non pu contenere un non-null value
Optional<Car> firstBlack = cars.stream()
.filter(car -> car.getColor() == Colors.BLACK) // operazione intermedia
.findFirst(); // operazione terminale di tipo short-circuiting
System.out.println("La prima macchina BLACK trovata nella collezione una " +
firstBlack.get());
}

public static void executeParallelOperations(Collection<Car> cars)


{
// esegue, in parallelo, un conteggio di tutte le macchine della
// collezione cars escludendo, per, quelle che hanno la stessa marca
long count = cars.parallelStream()
.distinct() // operazione intermedia stateful
.count(); // operazione terminale
System.out.println("Numero di macchine escludendo quelle con la stessa marca: " +
count);
}

public static void ordering(Collection<Car> cars)


{
// esegue in parallelo le seguenti operazioni:
// ritorna per ogni macchina il modello e lo stampa.
// La I e la II computazione ritorneranno un risultato differente
// e nessun eventuale ordinamento rispettato.
// Con forEachOrdered invece, nella III computazione l'ordinamento incontrato
// rispettato

// I COMPUTAZIONE
System.out.print("Prima computazione in ordering: [ ");
cars.parallelStream()
.map(car -> car.getModel()) // operazione intermedia
.forEach(model -> System.out.print(model + " ")); // operazione terminale
System.out.println("]");

// II COMPUTAZIONE
System.out.print("Seconda computazione in ordering: [ ");
cars.parallelStream()
.map(car -> car.getModel()) // operazione intermedia
.forEach(model -> System.out.print(model + " ")); // operazione terminale
System.out.println("]");
// III COMPUTAZIONE
System.out.print("Terza computazione in ordering: [ ");
cars.parallelStream()
.map(car -> car.getModel()) // operazione intermedia
.forEachOrdered(model -> System.out.print(model + " ")); // operazione
// terminale
System.out.println("]");
}

public static void makeReduction(Collection<Car> cars)


{
// otteniamo la somma dei codici di tutte le macchine
int sum = cars.stream()
.mapToInt(car -> car.getCode()) // operazione intermedia
.reduce(0, (a, b) -> a + b); // operazione terminale

System.out.println("La somma di tutti i codici : " + sum);


}

public static void makeMutableReduction(Collection<Car> cars)


{
List<String> model_list
= cars.stream()
.map(car -> car.getModel()) // operazione intermedia
// operazione terminale
.collect(() -> new ArrayList<>(), // supplier - ok anche ArrayList::new
// accumulator - ok anche ArrayList::add
(container, element) -> container.add(element),
// combiner - ok anche ArrayList::addAll
(container_1, container_2) -> container_1.addAll(container_2));

System.out.println("Collezione dei modelli: " + model_list);


}

public static void main(String[] args)


{
Collection<Car> cars = Arrays.asList(
new Car("BMW", Colors.BLACK, 123),
new Car("RENAULT", Colors.BLACK, 205),
new Car("FIAT", Colors.RED, 10),
new Car("MASERATI", Colors.YELLOW, 99),
new Car("FIAT", Colors.WHITE, 10),
new Car("MASERATI", Colors.RED, 99),
new Car("TOYOTA", Colors.BLACK, 89));

makeAverage(cars);
makeExternalIteration(cars);
makeInternalIteration(cars);
shortCircuiting(cars);
executeParallelOperations(cars);
ordering(cars);
makeReduction(cars);
makeMutableReduction(cars);
}
}

Output 17.11 Dal Listato 17.11 Classe StreamTutorial.


La media dei codici degli elementi di tipo Car : 139.0
Nella collezione corrente ci sono 3 macchine con il colore BLACK
Nella collezione corrente ci sono 3 macchine con il colore BLACK
La prima macchina BLACK trovata nella collezione una BMW
Numero di macchine escludendo quelle con la stessa marca: 5
Prima computazione in ordering: [ FIAT RENAULT BMW FIAT MASERATI TOYOTA MASERATI ]
Seconda computazione in ordering: [ FIAT TOYOTA BMW RENAULT FIAT MASERATI MASERATI ]
Terza computazione in ordering: [ BMW RENAULT FIAT MASERATI FIAT MASERATI TOYOTA ]
La somma di tutti i codici : 635
Collezione dei modelli: [BMW, RENAULT, FIAT, MASERATI, FIAT, MASERATI, TOYOTA]

che utilizzeremo come modello pratico per spiegare i concetti legati agli stream,
vediamo che il metodo statico makeAverage della classe StreamTutorial: crea uno stream di
oggetti di tipo Car grazie al metodo stream invocato sul riferimento cars di tipo
Collection<Car> ; vi applica un filtro, grazie al metodo filter del tipo Stream, al fine di
produrre un nuovo stream che conterr solo oggetti Car di colore BLACK; trasforma
questultimo stream in uno stream di valori interi che rappresentano il codice di
ciascun oggetto Car grazie al metodo mapToInt del tipo Stream; applica a tale stream
unoperazione di calcolo della media dei relativi valori, grazie al metodo average del
tipo IntStream, che ritorna, per lappunto, un valore che la media dei valori dei codici
dei relativi oggetti Car.
TERMINOLOGIA
Le operazioni compiute sullo stream sono definite filter-map-reduce.

Dallanalisi dei succitati passi vediamo come, nella sostanza, una computazione su
un insieme di elementi di una collezione eseguita mediante la costruzione di una
pipeline di stream dove indichiamo le operazioni da eseguire, intermedie e finali,
mediante linvocazione di particolari metodi ai quali passiamo delle specifiche
lambda expression. Di fatto, una pipeline di stream pu essere considerata come una
sorta di vista o query su di una collezione (che normalmente la sorgente dello
stream stesso) sulla quale eseguire operazioni di massa (bulk operations) attraverso
delle iterazioni interne.
DETTAGLIO
In termini pi precisi possiamo dire che una pipeline di stream costituita dai seguenti
componenti: una sorgente (ricavabile da una collezione, un array, una funzione generatore o un
canale di I/O); zero o pi operazioni intermedie; unoperazione terminale.

Per quanto concerne le operazioni applicabili sugli stream, i relativi parametri sono
sempre istanze di una qualche interfaccia funzionale, fornite generalmente come
lambda expression o riferimenti a metodi. In pi, per preservare lintegrit sui dati, o
un determinato comportamento atteso, di collezioni concorrenti non modificabili che
sono viste di stream, le funzionalit fornite non dovrebbero mai provare a modificare
tali sorgenti originarie (non-interference), e gli eventuali risultati non dovrebbero mai
dipendere da stati intermedi che potrebbero cambiare durante lesecuzione della
pipeline (stateless behaviors).

Tipologie di iterazioni
La modalit usuale per laccesso agli elementi di una collezione, conosciuta con il
nome di iterazione esterna, quella che prevede lutilizzo di una struttura di
iterazione (for o while) e di un iteratore (metodo makeExternalIteration della classe
StreamTutorial). Questo tipo di iterazione, quantunque semplice nellutilizzo, porta con
s i seguenti problemi: la logica di attraversamento della collezione frapposta alla
funzionalit da applicare a ogni elemento della stessa; le strutture selettive di Java
sono intrinsecamente sequenziali (single-threaded) e devono processare gli elementi
nellordine specificato dalla collezione; c ridondanza di codice nel verificare e
processare il successivo elemento (si devono invocare, per esempio, i metodi hasNext e
di un oggetto di tipo Iterator). Di fatto, quindi, tutti gli aspetti progettuali ed
next

elaborativi delliterazione esterna sono demandati al client utilizzatore.


A partire da Java 8 abbiamo unaltra opportunit per accedere agli elementi di una
collezione, ovvero tramite uniterazione interna che si concretizza nella possibilit di
invocare un metodo di una collezione o di uno stream (per esempio il metodo forEach
invocato nellambito del metodo makeInternalIteration della classe StreamTutorial);
questo, al suo interno, ha il codice di attraversamento delle relativa collezione e il
codice di applicazione della funzionalit ottenuta (in questo caso si dice che la
collezione stessa che controlla i dettagli del processo iterativo e come applicare a
ogni elemento la relativa funzionalit). Il client utilizzatore, quindi, dovr solo
preoccuparsi di passare al predetto metodo la funzionalit che sar applicata per ogni
elemento della collezione. In buona sostanza nelliterazione interna verificabile
pienamente come sia fondamentale il principio di una netta separazione tra il cosa si
deve processare dal come si deve farlo. Infatti, tale separazione lascia aperta la
possibilit agli implementatori di tali API di compiere diverse ottimizzazioni
elaborative (lazy evaluation o out-of-order execution), processare gli elementi in
parallelo piuttosto che sequenzialmente e cos via.

Operazioni sugli stream


Dato uno stream possibile compiere con esso due tipi di operazioni definite
intermedie (intermediate operations) e terminali (terminal operations). Le operazioni
intermedie hanno le seguenti caratteristiche: producono un nuovo stream; sono lazy,
ovvero non sono immediatamente eseguite; possono essere stateless, ovvero ogni
nuovo elemento pu essere processato indipendentemente dal processing di altri
elementi, oppure stateful, ovvero ogni nuovo elemento quando viene processato
potrebbe avere bisogno dello stato degli altri elementi (per esempio, il metodo sorted
del tipo Stream unoperazione intermedia di tipo stateful perch necessario
processare lintero input prima di produrre come risultato lo stream ordinato);
possono essere di tipo short-circuiting se dato un input infinito possono produrre
come risultato uno stream finito.
Le operazioni terminali hanno, invece, le seguenti caratteristiche: non producono
un nuovo stream ma un valore primitivo, una collezione o nessun risultato in
particolare (per esempio il metodo forEach); sono principalmente eager, ovvero
effettuano subito la loro computazione che si concretizza nellattraversamento della
sorgente della pipeline per eseguire le relative operazioni; consumano la pipeline di
stream, ovvero dopo che hanno completato su di essa le relative operazioni non la
possono riutilizzare; possono essere di tipo short-circuiting se dato un input infinito
possono terminare la computazione in un tempo finito.
TERMINOLOGIA
Unoperazione definibile di tipo short-circuiting se pu terminare il relativo processing nel
momento in cui ha raggiunto lobiettivo della computazione e senza, quindi, dover
necessariamente analizzare tutti gli elementi di una sorgente (vedere il metodo shortCircuiting
della classe StreamTutorial).

Parallelismo
Come gi detto, scrivere programmi che fanno uso di pi thread sicuramente
difficile perch richiede notevoli competenze e unattenta comprensione del processo
di parallelizzazione delle operazioni. Questa difficolt diventa ancora pi insidiosa e
problematica quando utilizziamo le collezioni che sono, di base, non thread-safe:
laccesso simultaneo di pi thread, se non gestito nel dovuto modo, pu causare gravi
problemi di corruzione dei dati relativi. Da questo punto di vista, fortunatamente, le
stream API, sono state disegnate con lobiettivo di rendere il parallelismo accessibile
e nel contempo non intrusivo o invisibile (ovvero non mai eseguito in automatico
senza una decisione esplicita).
Infatti, data per esempio una collezione, possibile creare uno stream che esegue
le sue computazioni in parallelo invocando semplicemente il metodo parallelStream
(vedere il metodo executeParallelOperations nella classe StreamTutorial) su un oggetto di
tipo Collection.
ATTENZIONE
Per compiere operazioni in parallelo su uno stream in modo sicuro e deterministico, quando la
sorgente non thread-safe, bisogna soddisfare il principio della non interferenza, che prevede
che tale sorgente non sia modificata durante il suo attraversamento. In caso contrario, tale
comportamento potrebbe causare la generazione di eccezioni (per esempio quelle di tipo
java.util.ConcurrentModificationException), risposte computazionali inattese o comportamenti
esecutivi non corretti.

Ordinamento
Una pipeline di stream pu processare i relativi elementi in modo ordinato o meno
e ci in dipendenza della sorgente dei dati e delle operazioni intermedie o terminali
eseguite. Infatti, possiamo: avere sorgenti ordinate come quelle di tipo TreeSet oppure
non ordinate come quelle di tipo HashSet; utilizzare delle operazioni intermedie come
per ordinare uno stream non ordinato; utilizzare operazioni terminali come
sorted

forEach che, in caso di stream paralleli, ignorano un determinato ordinamento cos che
ogni esecuzione compiuta sul medesimo stream produrr un risultato differente
(metodo ordering della classe StreamTutorial, prima e seconda computazione).

In questultimo caso possiamo impiegare il metodo forEachOrdered che processa gli


elementi di uno stream in base allordinamento specificato dalla sorgente e
indipendentemente, quindi, se lo stream viene eseguito in parallelo (metodo ordering
della classe StreamTutorial, terza computazione). Chiaramente, limposizione di
costrizioni sullordinamento, per lesecuzione parallela delle operazioni su uno
stream, pu inficiare le performance e lefficienza della medesima computazione
parallela.
In ogni caso, laddove uno stream ha un certo ordinamento, che per in un
determinato contesto elaborativo non ha importanza incontrare, si pu esplicitamente
eliminare la costrizione sullordinamento con il metodo unordered del tipo BaseStream al
fine di migliorare lefficienza computazionale per alcune operazioni terminali dovuta
al parallelismo.

Riduzione
Nellambito della programmazione funzionale vi una famiglia di funzioni di
ordine superiore denominate reduction (riduzione) o fold che hanno come obiettivo
computazionale quello di processare secondo un determinato ordine una struttura di
dato, come per esempio una lista di elementi, e ottenere un valore di ritorno.
TERMINOLOGIA
Unaltra famiglia di funzioni di ordine superiore denominata unfold; a differenza delle fold
function, prendono come input un determinato valore iniziale e lo danno in pasto a una funzione
che genera una struttura di dato.

In termini meno astratti possiamo pensare a una riduzione come a unoperazione


che prende come input una sequenza di elementi nei confronti dei quali applica
ripetutamente una specifica operazione (combining operation) al fine di produrre un
risultato come, per esempio la somma o la media di tale sequenza. In pratica tale
operazione consente di collassare, ridurre, piegare una collezione in un singolo
valore.
Le classi del package java.util.stream forniscono sia un insieme di operazioni di
riduzione specializzate attraverso i metodi max, count, min, sum, average e cos via sia
operazioni di riduzioni generiche attraverso i metodi reduce e collect del tipo Stream.

Il metodo makeReduction della classe StreamTutorial mostra come costruire una


riduzione sulla collezione cars che consente di ritornare la somma di tutti i codici
degli elementi di tipo car.

In pratica abbiamo effettuato una riduzione avvalendoci del metodo reduce con la
seguente segnatura T reduce(T identity, BinaryOperator<T> accumulator) dove: il parametro
identity il valore iniziale della riduzione oppure il valore di default se la collezione
da ridurre non ha elementi; il parametro accumulator una funzione che prende due
argomenti, dove il primo rappresenta un valore parziale mentre il secondo
rappresenta il prossimo elemento da processare, e ritorna un nuovo risultato parziale.
Nel nostro caso, dunque, il valore 0 valore iniziale della somma mentre la lambda
expression (a, b) -> a + b indica laccumulatore dove il parametro a conterr dei valori
parziali come 0, 123, 328 e cos via, il parametro b conterr i valori da processare come
,
123 205 10, e cos via, il valore di ritorno parziale sar 123, 328, 338 e cos via sino al
valore finale di 635 (Tabella 17.1).

Tabella 17.1 Passi di elaborazione di reduce.


Valore parziale in a Valore successivo in b Valore di ritorno parziale
0 123 123
123 205 328
328 10 338
338 99 437
437 10 447
447 99 546
546 89 635

Lintera invocazione di reduce, in questo contesto elaborativo sequenziale, pu


essere vista come equivalente al seguente frammento di codice soprattutto nella parte
di costruzione, iterazione e applicazione dellaccumulatore (Snippet 17.2).
Snippet 17.2 Forma equivalente di reduce.
Integer array[] = {123, 205, 10, 99, 10, 99, 89};
List<Integer> stream = Arrays.asList(array);
BinaryOperator<Integer> accumulator = (a, b) -> a+ b;
int identity = 0;
int result = identity;
for (int element : stream)
result = accumulator.apply(result, element);
System.out.println(result); // 635

Riduzione mutabile
Esiste una particolare forma di riduzione che ha come obiettivo computazionale
quello di accumulare degli elementi di input in un container di risultato mutabile,
come per esempio un tipo ArrayList o un tipo StringBuilder.

In questo tipo di operazione tutti gli elementi sono aggiunti in un container


aggiornando lo stato del risultato piuttosto che sostituendolo di volta in volta come
abbiamo visto per la precedente forma di riduzione.
Questo tipo di riduzione eseguibile mediante lutilizzo del metodo collect del tipo
Stream e ha la segnatura <R> R collect(Supplier<R> supplier, BiConsumer<R,? super T>
,
accumulator BiConsumer<R,R> combiner) , dove il parametro supplier una funzione che crea
un nuovo container di risultato, il parametro accumulator una funzione che si occupa
di aggiungere un elemento nel container e il parametro combiner una funzione che si
occupa di unire il contenuto di un container di risultato in un altro container di
risultato.
In pi utile precisare che: il parametro supplier un tipo Supplier, ovvero
uninterfaccia funzionale che ha come scopo quello di ritornare un risultato (metodo
funzionale get); il parametro accumulator e combiner sono di tipo BiConsumer, ovvero sono
delle interfacce funzionali che hanno come scopo quello di eseguire unoperazione,
processando i due argomenti di input forniti, senza per ritornare un risultato (metodo
funzionale accept).

Il metodo makeMutableReduction della classe StreamTutorial mostra come utilizzare il


metodo collect per creare un container di tipo List<String> formato da elementi che
rappresentano i nomi di tutti i modelli dello stream ricavato dalla collezione cars.

Linvocazione di collect, in questo contesto elaborativo sequenziale ed escludendo


il combiner, pu essere vista come equivalente al seguente frammento di codice
soprattutto nella parte di costruzione, iterazione e applicazione dellaccumulatore
(Snippet 17.3).
Snippet 17.3 Forma equivalente di collect.
String array[] = {"BMW", "RENAULT", "FIAT", "MASERATI", "FIAT", "MASERATI", "TOYOTA"};
List<String> stream = Arrays.asList(array);
Supplier<ArrayList<String>> supplier = () -> new ArrayList<>();
BiConsumer<List<String>, String> accumulator = (a, b) -> a.add(b);
ArrayList<String> result = supplier.get();
for (String element : stream)
accumulator.accept(result, element);
System.out.println(result); // [BMW, RENAULT, FIAT, MASERATI, FIAT, MASERATI, TOYOTA]

Alcuni metodi del tipo Stream


boolean allMatch(Predicate<? super T> predicate) ritorna true se gli elementi dello
stream corrispondono a quanto indicato dal predicato predicate oppure se lo
stream vuoto. Ritorna false in caso contrario. unoperazione terminale di tipo
short-circuiting.
boolean anyMatch(Predicate<? super T> predicate) ritorna true se tutti gli elementi dello
stream corrispondono a quanto indicato dal predicato predicate. Ritorna false in
caso contrario. unoperazione terminale di tipo short-circuiting.
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) ritorna uno
stream che la concatenazione dello stream a con lo stream b. Tale stream
risultante ordinato se entrambi gli stream a e b sono ordinati e parallelo se uno
dei due stream parallelo.
static <T> Stream<T> empty() ritorna uno stream sequenziale vuoto.

Stream<T> limit(long maxSize) ritorna uno stream costituito da un numero di


elementi dello stream su cui invocato che limitato a maxSize.
Optional<T> max(Comparator<? super T> comparator) ritorna un tipo Optional che
rappresenta il massimo elemento di uno stream in accordo con le regole fornite
dal parametro comparator. Tale Optional sar vuoto se lo stream sar vuoto.
Optional<T> min(Comparator<? super T> comparator) ritorna un tipo Optional che
rappresenta il minimo elemento di uno stream in accordo con le regole fornite
dal parametro comparator. Tale Optional sar vuoto se lo stream sar vuoto.
Stream<T> skip(long n) ritorna uno stream costituito dagli elementi dello stream su
cui invocato dopo aver saltato il numero di elementi indicati dal parametro n.

Alcuni metodi del tipo Collectors


public static <T> Collector<T,?,List<T>> toList() ritorna un Collector che accumula
degli elementi di input in un tipo List.
public static <T> Collector<T,?,Set<T>> toSet() ritorna un Collector che accumula
degli elementi di input in un tipo Set.

public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K>
keyMapper, Function<? super T,? extends U> valueMapper) ritorna un Collector che
accumula degli elementi in un tipo Map le cui chiavi e i cui valori sono il risultato
dellapplicazione delle funzioni di mapping (parametro keyMapper e parametro
valueMapper ) agli elementi di input. Per lutilizzo di questo metodo importante
sapere che, se le chiavi mappate contengono dei duplicati, allora sar generata
uneccezione di tipo IllegalStateException. Se tuttavia le chiavi mappate possono
avere dei duplicati, allora possibile utilizzare il seguente metodo in
overloading: public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,?

extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U>

mergeFunction) .

Snippet 17.4 Esempio di utilizzo di toList, toSet e toMap.


List<Integer> a_list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
Set<String> a_set = new TreeSet<>();
a_set.add("Pellegrino");
a_set.add("Marco");
a_set.add("Carlo");
a_set.add("Domenico");
a_set.add("Roberto");

// ritorna una lista con gli elementi 4,5,6,7


List<Integer> another_list = a_list.stream().skip(3).collect(Collectors.toList());
System.out.println(another_list); // [4, 5, 6, 7]

// ritorna un set con gli elementi Carlo, Domenico


Set<String> another_set = a_set.stream().limit(2).collect(Collectors.toSet());
System.out.println(another_set); // [Carlo, Domenico]

// ritorna una mappa dove le chiavi sono il nome dei modelli delle car
// della collezione Cars e i valori sono i rispettivi colori
Map<String, Colors> model_map = cars.stream()
.distinct()
.collect(Collectors.toMap(car -> car.getModel(), car -> car.getColor()));
for (Map.Entry<String, Colors> entry : model_map.entrySet())
{
String key = entry.getKey();
Colors value = entry.getValue();
System.out.println(key + " " + value);
};
Capitolo 18
Programmazione concorrente

La programmazione concorrente consente di scrivere programmi che sono in grado


di eseguire pi operazioni in parallelo e in modo indipendente, garantendo che se
unoperazione sta eseguendo un compito oneroso e potenzialmente bloccante,
lutente possa comunque eseguire altre operazioni senza dover attendere che la prima
abbia terminato il suo compito.
Si pensi per esempio a un programma che effettua operazioni di input/output su
disco. In un modello di programmazione tradizionale, dove solo unoperazione alla
volta pu svolgersi, lesecuzione della lettura o della scrittura di un file potrebbe
causare un blocco del programma finch la stessa non sia stata completata, causando
allutente il disagio di avere lapplicazione in stallo e inutilizzabile per altri compiti.
In un modello di programmazione concorrente, invece, loperazione di lettura o di
scrittura del file eseguita in modo indipendente e non bloccante per lapplicazione
nel suo complesso, permettendo allutente di svolgere altre operazioni come quella,
per esempio, di continuare a editare un altro file.
Il linguaggio Java consente di utilizzare il tipo di programmazione descritto perch
ne offre un supporto: a livello del linguaggio stesso, tramite le keyword synchronized e
volatile; attraverso lutilizzo di classi e interfacce a basso livello (Thread, Runnable e cos
via) presenti nel package java.lang, che permettono di utilizzare i thread in modo
basico; attraverso lutilizzo di classi e interfacce a un pi alto livello
(ArrayBlockingQueue, Executors, FutureTask, Callable, Semaphore, AtomicBoolean e cos via)
presenti nei package java.util.concurrent, java.util.concurrent.locks e
java.util.concurrent.atomic, che consentono di utilizzare i thread in modo pi completo
e sicuro.
Processi e thread
Prima di addentrarci nella spiegazione di come utilizzare la programmazione
concorrente in Java, soffermiamoci sui concetti di processo e di thread, fondamentali
e propedeutici per una corretta comprensione del meccanismo.
Un processo definibile come un ambiente di esecuzione allinterno del quale gira
il programma che lha creato. ununit di elaborazione, indipendente dagli altri
processi, costituita dal proprio spazio di memoria assegnato, dal proprio codice
eseguibile e da riferimenti a eventuali risorse di sistema allocate per esso.
Un thread invece definibile come ununit di elaborazione in cui pu essere
diviso un processo. Esso vive, pertanto, allinterno di un processo dove condivide,
con altri thread eventualmente presenti, le risorse, la memoria e le informazioni di
stato del processo medesimo.
Un processo ha sempre un thread di esecuzione rappresentato da se stesso, ma pu
avere anche altri thread creati al suo interno per girare in parallelo.
TERMINOLOGIA
Il termine inglese thread si pu tradurre in italiano come filo; visivamente, se immaginiamo il
processo come una fune, possiamo vedere i suoi thread come i vari fili in esso avviluppati.

I processi e i thread, prima ancora di essere supportati nei linguaggi di


programmazione, devono essere implementati a livello di sistema operativo. Oggi
tutti i moderni sistemi operativi (Windows, GNU/Linux, Mac OS e cos via) sono
multitasking e multithreading, ovvero consentono lesecuzione di pi task (processi e
thread) contemporaneamente. Nei sistemi a singolo processore il parallelismo
garantito da sofisticati e complessi algoritmi software che garantiscono sia il cambio
di contesto esecutivo, sia la ripartizione del tempo CPU tra processi. In effetti, nei
sistemi a singolo processore questo parallelismo virtuale, perch se volessimo
realizzare una vera multiprocessualit dovremmo avere per ogni processo una CPU
separata a esso dedicata. In ogni caso, grazie alla straordinaria potenza e velocit
delle moderne CPU, il parallelismo attuabile anche nei sistemi a singola CPU,
poich lutente non si accorge dei cambi di contesto quando utilizza pi applicazioni
in contemporanea.
NOTA
Esistono sistemi operativi che non sono multithreading, ovvero non supportano a livello del kernel
i thread. In questo caso il supporto per la programmazione parallela dei thread dato da apposite
librerie software dedicate.
Stati di un thread
Un thread pu assumere i seguenti stati operativi, che determinano nel loro
complesso il suo ciclo di vita:

created: il thread viene creato;


ready: il thread in attesa di poter eseguire i suoi processi computazionali;
running: il thread avviato e sta eseguendo i suoi compiti. In realt questo stato
spesso definito runnable, perch il fatto che abbiamo avviato un thread non
implica necessariamente che lo stesso sia contestualmente eseguito. Infatti il
sistema operativo che decide quando assegnare tempo CPU a un thread per la
sua esecuzione (tramite lo scheduling); pertanto, in un determinato momento il
sistema potrebbe mettere in pausa il thread per consentire ad altri thread di
compiere le loro operazioni;
waiting: il thread posto in attesa che un altro thread completi le sue operazioni;
sleep: il thread posto in attesa per un determinato periodo di tempo. Questo
stato definito anche come timed waiting;
blocked: il thread bloccato perch vuole eseguire delle operazioni su risorse di
sistema o su altri oggetti che per sono gi in uso ad altri thread;
terminated: il thread ha terminato il suo processo computazionale, naturalmente
oppure perch si verificata una condizione anomala. Questo stato definito
anche dead state.

DETTAGLIO
Durante lo stato runnable un thread pu essere messo in attesa per decisione esterna, allo
scadere del tempo CPU assegnato, se il sistema operativo ha algoritmi di scheduling di tipo
preemptive, oppure pu mettersi in attesa autonomamente, se il sistema operativo ha algoritmi di
scheduling di tipo cooperative. Ovviamente i moderni sistemi operativi utilizzano un
multithreading di tipo preemptive, dove le delicate decisioni di gestione dei thread sono a carico
loro e non della singola applicazione.

Priorit dei thread


Quando il sistema operativo decide a quale thread assegnare tempo CPU per la sua
esecuzione, lo fa in base a un sistema di priorit dove il thread con la pi alta priorit
, ovviamente, preferito rispetto a uno con priorit pi bassa. Se i thread hanno
uguale priorit, allora il sistema operativo assegner circolarmente, per ciascuno,
unuguale quantit di tempo CPU (time slice) entro la quale dovranno eseguire i
propri processi computazionali (round-robin sheduling). Tuttavia, se un thread non
avr ancora terminato il suo processo computazionale quando il suo quantum di
tempo sar cessato, il sistema operativo gli toglier il relativo tempo CPU e lo
passer per lutilizzo al successivo thread di pari priorit, se presente. Infine,
questo meccanismo, si ripeter finch tutti i thread avranno terminato la propria
elaborazione.
Ci significa, per esempio, che se abbiamo nellordine i thread X, Y e Z con un
valore di priorit n e poi i thread K e W con valore di priorit n-1, lo scheduler del
sistema operativo compir i seguenti passi (Figura 18.1).

1. Definir una quantit di tempo di esecuzione per i thread X, Y, Z, K e W.


2. Verificher se il thread X ha terminato il suo compito allo scadere del suo time
slice e, in caso contrario, lo sospender e allocher lo stesso tempo CPU al
successivo thread di pari livello ossia Y. Far lo stesso per i thread Y e Z.
3. Ripeter il passo 2 finch i thread avranno completato le proprie operazioni.
4. Passer lesecuzione elaborativa ai thread di cui la priorit n - 1, ovvero K e W.
5. Verificher se il thread K ha terminato il suo compito allo scadere del suo time
slice e, in caso contrario, lo sospender e allocher lo stesso tempo CPU al
thread successivo di pari livello, ossia W. Far lo stesso per il thread W.
6. Ripeter il passo 5 finch i thread avranno completato le proprie operazioni.

Figura 18.1 Round-robin.

ROUND-ROBIN
La modalit di comportamento dello scheduler nellassegnare e gestire i time slice dei thread con
uguale priorit utilizza un algoritmo software denominato round-robin. Il termine trae la sua origine
dallusanza che si aveva in passato di firmare in modo circolare le lettere di petizione alle autorit in
modo che non si potesse risalire, in modo gerarchico, a un leader proponente. In questo caso,
dunque, ciascun cittadino poneva, per cos dire, un quantum di firma che per non era pi o meno
importante di quello apposto da un altro cittadino, perch tutti avevano pari dignit (priorit).

I valori di priorit assegnati ai thread dipendono dal sistema operativo in cui gira
lapplicativo. Nel caso di Windows il thread con pi bassa priorit avr il valore 0,
mentre il thread con pi alta priorit avr il valore 31; nel caso di GNU/Linux il
thread con pi bassa priorit avr il valore 19, mentre il thread con pi alta priorit
avr il valore 20.
La classe Thread e linterfaccia Runnable
Un thread rappresentato da un oggetto istanza della classe Thread, mentre
loperazione che il thread eseguir rappresentata dalla definizione di un apposito
metodo denominato run.
Listato 18.1 Classe SimpleThread.

...
class DoJobRun implements Runnable // run tramite l'interfaccia Runnable
{
private String msg = " sar occupato per i prossimi ";

public void run()


{
for (int i = 0; i < 5; i++)
{
try
{
int ms = 6000; // 6 secondi
System.out.println(Thread.currentThread().getName()
+ msg + ms + " millisecondi");
Thread.sleep(ms); // metti in timed waiting il thread
// per n millisecondi
}
catch (InterruptedException ex) {}
}
}
}

class DoJobTh extends Thread // run tramite la classe Thread


{
private String msg = " sar occupato per i prossimi ";

public void run()


{
for (int i = 0; i < 5; i++)
{
try
{
Random r = new Random();
int ms = r.nextInt(3000); // random tra 0 e 3 secondi
System.out.println(getName() + msg + ms + " millisecondi");
Thread.sleep(ms); // metti in timed waiting il thread
// per n millisecondi
}
catch (InterruptedException ex) {}
}
}
}

public class SimpleThread


{
public static void main(String args[])
{
DoJobRun job = new DoJobRun(); // creo il primo thread
Thread nt = new Thread(job, "***THREAD 1***");
nt.start();

DoJobTh thread_2 = new DoJobTh(); // creo il secondo thread


thread_2.setName("***THREAD 2***");
thread_2.start();

System.out.println("Thread principale (" + Thread.currentThread().getName()


+ ") finito");
}
}
Il Listato 18.1 mostra che la definizione del metodo run, che rappresenta
loperazione che sar eseguita dal thread, pu essere effettuata utilizzando due
modalit implementative: attraverso la definizione di una classe che implementa
linterfaccia Runnable, oppure attraverso la definizione di una classe che estende la
classe Thread. Nel primo caso avremo una maggiore flessibilit, sia per la netta
separazione tra il worker, rappresentato da un oggetto della classe Thread, e il work,
rappresentato da un oggetto di una classe che ha implementato il tipo Runnable, sia per
la possibilit di far derivare la classe work da una qualsiasi altra classe. Nel secondo
caso, invece, pur avendo una maggiore facilit di utilizzo, abbiamo lo svantaggio che
la classe worker pu derivare solo dalla classe Thread.

Nellapplicazione di esempio creiamo la classe DoJobRun, che implementa


linterfaccia Runnable e la classe DoJobTh, che estende la classe Thread. In entrambe le
classi definiamo il metodo run, che in un ciclo stamper per cinque volte, a differenti
intervalli di tempo, un messaggio indicante il nome del thread corrente e il tempo per
cui sar occupato dalloperazione in corso di svolgimento.
Dopo la definizione delle suddette classi, nel metodo main della classe SimpleThread
creiamo e avviamo i thread attraverso i seguenti passi.

Per la classe DoJobRun creiamo una sua istanza e ne passiamo il riferimento


denominato job come argomento al costruttore della classe Thread, insieme al
nome del thread. Avviamo poi il thread, invocando il metodo start delloggetto
nt di tipo Thread precedentemente creato.
Per la classe DoJobTh creiamo una sua istanza invocandone direttamente il
costruttore e ne impostiamo il nome attraverso linvocazione del suo metodo
setName. Avviamo poi il thread, invocando il metodo start delloggetto thread_2 di

tipo DoJobTh precedentemente creato.

Continuando lesame del codice vediamo che abbiamo utilizzato i metodi statici
sleep e currentThread della classe Thread, che consentono, nellordine, di porre un thread

nello stato di timed waiting per un certo numero di millisecondi (di fatto, se ne
interrompe momentaneamente lesecuzione per il tempo indicato) e di ottenere il
riferimento del thread corrente in esecuzione.
NOTA
importante precisare che, qualunque delle due modalit implementative si decida di scegliere,
occorre sempre definire il metodo run, perch esso conterr il codice che il thread effettivamente
eseguir e perch sar il metodo invocato automaticamente dal metodo start.
Output 18.1 Dal Listato 18.1 Classe SimpleThread.

***THREAD 1*** sar occupato per i prossimi 6000 millisecondi


***THREAD 2*** sar occupato per i prossimi 628 millisecondi
Thread principale (main) finito
...

LOutput 18.1 evidenzia che le operazioni dei thread procedono autonomamente e


in parallelo. Infatti, quando per esempio il thread ***THREAD 1*** sar occupato per 6000
millisecondi, il programma non si fermer per attendere la scadenza di tale intervallo
di tempo, ma proceder comunque, stampando le informazioni sullattivit del thread
denominato ***THREAD 2***. Rileviamo, inoltre, che esiste anche un thread denominato
main, che si riferisce al thread che la virtual machine di Java crea per lapplicazione
rappresentata dalla classe SimpleThread. Tale thread, come nel nostro caso, potr
terminare anche prima che siano terminati gli altri thread, senza per questo far
terminare tutta lapplicazione, che invece cesser di esistere soltanto quando saranno
terminati gli altri thread in esecuzione.
Possiamo pertanto asserire che unapplicazione Java ha sempre almeno un thread
in esecuzione che esegue il codice scritto allinterno del metodo main, ed
eventualmente altri thread creati ed eseguiti in parallelo.

Alcuni metodi del tipo Thread


public long getId() ritorna il numero dellidentificativo assegnato al thread.
Questo numero univoco e rimane assegnato per tutto il ciclo di vita del thread.
Quando, poi, il thread sar terminato tale id potr essere riutilizzato.
public final String getName() ritorna il nome associato al thread.

public final int getPriority() ritorna il valore di priorit associato al thread.


public Thread.State getState() ritorna lo stato in cui si trova il thread, che pu
essere NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING e TERMINATED.
public void interrupt() interrompe il thread.
public final boolean isAlive() ritorna true se il thread ancora in esecuzione.
public final void join() throws InterruptedException pone in attesa il thread finch
non cessa di esistere.
public final void setPriority(int newPriority) consente di impostare un valore di
priorit per il thread mediante il parametro newPriority. Questo valore deve essere
incluso tra i valori Thread.MIN_PRIORITY (1) e Thread.MAX_PRIORITY (10), e dopo la sua
assegnazione, lo stesso sar mappato nel corrispondente valore dipendente dal
sistema operativo in uso. Per esempio, un valore di priorit Java di 9
corrisponder al valore THREAD_PRIORITY_HIGHEST per Windows e al valore -4 per
GNU/Linux.
consente di fornire allo scheduler lindicazione, non
public static void yield()

vincolante, che il corrente thread vuole cedere spontaneamente il proprio tempo


CPU ad altri thread.
Sincronizzazione fra i thread
La sincronizzazione fra i thread indica la capacit, da parte di un sistema che
gestisce la concorrenza, di evitare che pi thread possano accedere simultaneamente
a dati condivisi, garantendo, tramite operazioni definite di locking, blocking e
releasing, che solo un thread alla volta possa avere, in un determinato tempo,
laccesso esclusivo ai dati (mutual exclusion o mutex).
Obiettivo della sincronizzazione dunque quello di garantire che un thread possa
utilizzare dei dati, in lettura e in scrittura, e che sia mantenuta sempre la coerenza del
risultato atteso dalla loro manipolazione, poich, nel frattempo, nessun altro thread ha
potuto avere accesso agli stessi dati.
Java gestisce la sincronizzazione fra i thread attraverso un meccanismo di controllo
e protezione fornito a livello del linguaggio stesso denominato intrinsic lock (o anche
monitor lock o pi semplicemente monitor).
La virtual machine di Java associa automaticamente un monitor a ogni oggetto e a
ogni classe, al fine di proteggere nel caso degli oggetti le loro variabili di istanza e
nel caso delle classi le loro variabili di classe.
Un monitor pu essere visto come una sorta di casa, dotata di una stanza, al cui
interno sono poste le informazioni, o pi nello specifico il codice, da controllare e
proteggere, e dove solo un thread alla volta pu entrare.
Nellambito di un programma possiamo decidere di proteggere pi blocchi di
codice creando delle monitor region, ovvero delle regioni (aree o sezioni) di codice
che devono essere eseguite in modo esclusivo, atomico e sincronizzato da un solo
thread alla volta. Un blocco di codice sincronizzato (region) si crea utilizzando la
seguente sintassi.
Sintassi 18.1 Blocco di codice synchronized.

synchronized (object) { statements; }

Utilizziamo la keyword synchronized seguita dalloggetto per il quale richiesto il


monitor e dalle istruzioni da eseguire esclusivamente.
Possiamo avere, oltre a blocchi di codice, anche interi metodi sincronizzati,
utilizzando la seguente sintassi.
Sintassi 18.2 Metodo synchronized.
public synchronized return-type method_name(parameters) { statements; }

La keyword synchronized posta prima del tipo di ritorno del metodo.


NOTA
La sintassi per la definizione di un metodo sincronizzato in realt unabbreviazione sintattica,
perch lo stesso effetto si otterrebbe scrivendo il metodo normalmente e poi racchiudendo il suo
corpo di istruzioni allinterno della keyword synchronized con this come oggetto.

Snippet 18.1 Equivalenza di metodo sincronizzato con blocco sincronizzato.

public synchronized void doStuff() { /* statements */ } // metodo sincronizzato

public void doStuff() // blocco sincronizzato EQUIVALENTE!!!


{
synchronized(this) { /* statements */ }
}

Quando un thread ha necessit di utilizzare il codice di una particolare region deve


porre in atto una procedura di acquisizione (o lock) del monitor, mediante la quale ne
diviene proprietario esclusivo se, ovviamente, nessun altro thread ha gi ottenuto il
medesimo lock.
La procedura di acquisizione del monitor si concretizza, di fatto, nellinvocazione
del metodo sincronizzato o del metodo non sincronizzato che per contiene il blocco
di codice sincronizzato.
Un thread perde la propriet esclusiva sul monitor quando lo stesso rilasciato, e
ci pu avvenire quando il metodo sincronizzato termina naturalmente con
unistruzione return, al raggiungimento della fine del suo blocco di codice o
improvvisamente se occorre uneccezione software non intercettata. Per un blocco di
codice sincronizzato il rilascio avviene, invece, quando si raggiunge la fine del suo
blocco, oppure se occorre uneccezione software non catturata.
Oltre alla sincronizzazione di tipo mutex fin qui esaminata, possiamo utilizzare
anche quella definita cooperativa, dove i thread cooperano per il raggiungimento di
un obiettivo comune.
Si immagini per esempio un thread che deve manipolare un buffer di dati che gli
deve essere fornito da un altro thread. In questo caso il primo thread (reader thread),
per poter completare positivamente le proprie operazioni, dovr sempre attendere che
il secondo thread (writer thread) abbia scritto i dati nel buffer.
In Java la sincronizzazione di tipo cooperativo ottenuta con il meccanismo dei
monitor definiti wait and notify (chiamati anche signal and continue).
In questo tipo di monitor, un thread che sta eseguendo codice di una regione
sincronizzata pu decidere di rilasciare temporaneamente tale monitor sospendendo
la sua esecuzione tramite linvocazione del comando wait. A quel punto un altro
thread prende possesso del monitor, esegue la regione di codice associata e tramite il
comando notify notifica al thread in sospeso che pu tentare di riprendere possesso
del monitor al fine di completare le sue operazioni.
Ovviamente le operazioni suddette funzionano in modo lineare solo se abbiamo a
che fare con due thread dove, in modo abbastanza deterministico, possiamo sempre
fare affidamento sul fatto che il thread notificante segnaler allunico thread in attesa
che potr riprendere il proprio lavoro.
Dobbiamo tuttavia considerare che in programmi che utilizzano pi thread, tale
determinismo non sar mai attuabile, poich vi saranno sempre molteplici thread in
competizione, e quale sar scelto per primo dal sistema dipender dallalgoritmo di
selezione implementato dalla virtual machine in uso.
In questultimo caso, al posto del comando notify consigliabile utilizzare il
comando notifyAll, che consente di notificare tutti i thread in attesa che possono
tentare di riprendere possesso del proprio blocco di codice sincronizzato.
I comandi wait, notify e notifyAll sono disponibili per tutti gli oggetti di qualsiasi
tipo di classe, poich sono implementati nella classe Object da cui, ricordiamo, tutte le
altre classi derivano implicitamente.
DETTAGLIO
I comandi notify e notifyAll consentono di far transitare i thread dallo stato di waiting allo stato
di runnable. Ci significa che, se sono in attesa pi thread, tutti diverranno eleggibili per la
riacquisizione di un monitor, ma solo uno di essi vincer la competizione e otterr il lock
sulloggetto desiderato.

Un esempio di mutex
Lesempio seguente fa eseguire a un thread delle operazioni di somma e a un altro -
thread delle operazioni di sottrazione. Le operazioni dovranno essere effettuate dal
thread additivo, che dovr raggiungere il valore 50, e dal thread sottrattivo, che a
partire da quel valore dovr poi raggiungere il valore 0.
Listato 18.2 Classe SynchronizedThread.

...
class MakeOperations
{
private int data; // dato condiviso

public synchronized void doOp(int v)


{
System.out.print("Il valore del dato dal thread "
+ Thread.currentThread().getName() + " e' di ");
for (int i = 0; i < 5; i++)
{
try
{
Thread.sleep(1000); // un po' di attesa
data += v;
System.out.print(i != 4 ? getRes() + " " : getRes() + "\n");
}
catch (InterruptedException ex){}
}
}
public synchronize int getRes() { return data; }
}

class RunThread1 implements Runnable // Runnable per il thread 1


{
private MakeOperations mop;

public RunThread1(MakeOperations mop) { this.mop = mop; }


public void run() { mop.doOp(10); } // somma
}

class RunThread2 implements Runnable // Runnable per il thread 2


{
private MakeOperations mop;

public RunThread2(MakeOperations mop) { this.mop = mop; }


public void run() { mop.doOp(-10); } // sottrazione
}

public class SynchronizedThread


{
public static void main(String args[])
{
MakeOperations mop = new MakeOperations(); // oggetto per eseguire
// delle operazioni

Thread t_1 = new Thread(new RunThread1(mop), "T_SOMMA");
Thread t_2 = new Thread(new RunThread2(mop), "T_SOTTRAZIONE");

// avvio i thread
t_1.start();
t_2.start();
}
}

Output 18.2 Dal Listato 18.2 della Classe SynchronizedThread.


Il valore del dato dal thread T_SOMMA e' di 10 20 30 40 50
Il valore del dato dal thread T_SOTTRAZIONE e' di 40 30 20 10 0

Nel Listato 18.2 definiamo la classe MakeOperations con il metodo doOp, di tipo
sincronizzato, che esegue una somma algebrica tra la variabile di istanza data e il
valore del suo parametro v. Nella stessa classe definiamo anche il metodo
sincronizzato getRes, che mostra il valore della variabile data.

Successivamente definiamo due classi che implementano linterfaccia Runnable e


che utilizzano un oggetto di tipo MakeOperations per effettuare operazioni di somma
(RunThread1) e sottrazione (RunThread2).

Nel metodo main della classe SynchronizedThread istanziamo un oggetto di tipo


MakeOperations denominato mop, che passiamo, in modo condiviso, al costruttore della
classe RunThread1 e della classe RunThread2.

Creiamo poi loggetto t_1 di tipo Thread, che eseguir il codice del metodo run
delloggetto della classe RunThread1, e loggetto t_2 di tipo Thread, che eseguir il codice
del metodo run delloggetto della classe RunThread2.

Infine avviamo i thread con linvocazione su t_1 e t_2 del metodo start.

LOutput 18.2 mostra come siano stati eseguiti, in modo esclusivo, prima il metodo
run delloggetto Runnable associato al thread T_SOMMA e poi il metodo run delloggetto

Runnable associato al thread T_SOTTRAZIONE.

Nel caso del thread T_SOMMA il metodo run invoca il metodo doOp passando il valore 10
come argomento; ci consente lincremento della variabile data fino al valore 50. Nel
caso del thread T_SOTTRAZIONE il metodo run invoca il metodo doOp passando il valore -10
come argomento; ci consente il decremento della variabile data fino al valore 0,
facendo cos raggiungere lobiettivo del programma.
Listato 18.3 Classe UnSynchronizedThread.

...
class MakeOperations
{
private int data; // dato condiviso

public void doOp(int v) // no sync


{
for (int i = 0; i < 5; i++)
{
try
{
Thread.sleep(new Random().nextInt(6000)); // 6 sec di attesa
data += v;
System.out.println("Il valore del dato dal thread " +
Thread.currentThread().getName() + " e' di " + getRes());
}
catch (InterruptedException ex){}
}
}
public int getRes() { return data; } // no sync
}
...
public class UnSynchronizedThread
{
public static void main(String args[]) { ... }
}

Output 18.3 Dal Listato 18.3 della Classe UnSynchronizedThread.

Il valore del dato dal thread T_SOTTRAZIONE e' di -10


Il valore del dato dal thread T_SOMMA e' di 0
Il valore del dato dal thread T_SOMMA e' di 10
Il valore del dato dal thread T_SOTTRAZIONE e' di 0
...

Il Listato 18.3 uguale al Listato 18.2 eccetto per il fatto che i metodi doOp e getRes
non sono pi sincronizzati e che abbiamo aggiunto unattesa (sleep) variabile tra 0 e 6
secondi al fine di rendere pi evidente la sovrapposizione dellaccesso dei due thread
allo stesso dato. LOutput 18.3 mostra chiaramente come i thread utilizzino senza
alcuna sincronizzazione il valore di data. Infatti, dopo che il thread T_SOTTRAZIONE ha
sottratto il valore 10 alla variabile data, il sistema passa del tempo CPU al thread
T_SOMMA, che utilizza subito quel valore per effettuare degli incrementi, non essendo
stato bloccato loggetto che lo contiene. Le operazioni di context switch fra i thread si
ripetono poi fino alla fine delle relative operazioni.

Un esempio di cooperazione
Lesempio che segue mostra limplementazione di un sistema che consente di far
cooperare due thread per il raggiungimento di un obiettivo comune, per esempio
quello di sincronizzare le operazioni di scrittura e di lettura su un buffer.
Avremo pertanto un thread PRODUCER che dovr scrivere in un buffer composto da un
array di interi e un altro thread CONSUMER che dovr, invece, leggere dallo stesso buffer
(condiviso fra i thread) i valori precedentemente scritti.
Le operazioni di scrittura e di lettura dovranno per avvenire in modo che il
CONSUMER possa leggere un valore solo dopo che il PRODUCER lavr scritto, e che il PRODUCER

possa scrivere il valore successivo solo dopo che il CONSUMER avr letto il valore
precedente.
Listato 18.4 Classe CoopSynchronizedThread.
...
class Buffer
{
private int data[]; // dato condiviso
private int nr_elem = 10; // max elem. di default
private boolean empty = true; // stato buffer all'inizio

public Buffer(int elem) // inizializzo il buffer


{
if (elem != -1)
{
data = new int[elem];
nr_elem = elem;
}
else
data = new int[nr_elem];
}

public synchronized void write(int val) throws InterruptedException // scrivo nel buffer
{
while (!empty) // finch il consumer non ha letto il dato aspetto
{
System.out.println(Thread.currentThread().getName()
+ " attende che CONSUMER legga il dato");
wait(); // sospende l'esecuzione e rilascia il monitor
}

data[val] = val; // scrivo il dato


empty = false; // aggiorno lo stato

System.out.println(Thread.currentThread().getName()
+ " ha scritto all'indice " + val + " il valore: " + data[val]);

notifyAll(); // notifica della possibilit di riacquisizione del monitor


}
public synchronize int read(int ix) throws InterruptedException
{
while (empty) // finch il producer non ha scritto il dato aspetto
{
System.out.println(Thread.currentThread().getName()
+ " attende che PRODUCER scriva il dato");
wait(); // sospende l'esecuzione e rilascia il monitor
}

empty = true; // aggiorno lo stato


System.out.println(Thread.currentThread().getName()
+ " ha letto all'indice " + ix + " il valore: " + data[ix]);

notifyAll();// notifica della possibilit di riacquisizione del monitor


return data[ix];
}

public int getBufferElements(){ return nr_elem; }


}

class RunProducer implements Runnable // Runnable per il thread 1


{
private Buffer b;

public RunProducer(Buffer b) { this.b = b; }

public void run()


{
for (int i = 0; i < b.getBufferElements(); i++)
{
Random r = new Random();
int ms = r.nextInt(5000);
try
{
Thread.sleep(ms); // un po' di attesa
b.write(i);
}
catch (InterruptedException ex) {}
}
}
}

class RunConsumer implements Runnable // Runnable per il thread 2


{
private Buffer b;

public RunConsumer(Buffer b) { this.b = b; }

public void run()


{
for (int i = 0; i < b.getBufferElements(); i++)
{
Random r = new Random();
int ms = r.nextInt(5000);
try
{
Thread.sleep(ms); // un po' di attesa
b.read(i);
}
catch (InterruptedException ex) {}
}
}
}

public class CoopSynchronizedThread


{
public static void main(String args[])
{
Buffer b = new Buffer(-1); // buffer

Thread t_1 = new Thread(new RunProducer(b), "PRODUCER"); // creo il PRODUCER


Thread t_2 = new Thread(new RunConsumer(b), "CONSUMER"); // creo il CONSUMER

// avvio i thread
t_1.start();
t_2.start();
}
}
Output 18.4 Dal Listato 18.4 Classe CoopSynchronizedThread.

PRODUCER ha scritto all'indice 0 il valore: 0


PRODUCER attende che CONSUMER legga il dato
CONSUMER ha letto all'indice 0 il valore: 0
...
CONSUMER attende che PRODUCER scriva il dato
PRODUCER ha scritto all'indice 9 il valore: 9
...

Il Listato 18.4 definisce la classe Buffer i cui elementi fondamentali sono:

larray di interi data, che conterr i dati scritti e letti;


la variabile di controllo empty, che consentir di sapere se il PRODUCER ha scritto il
dato oppure se il CONSUMER ha letto il dato;
il metodo sincronizzato write, dove il PRODUCER scriver i dati nel buffer solo se lo
stesso sar vuoto, altrimenti si porr in attesa invocando il comando wait;
il metodo sincronizzato read, dove il CONSUMER legger i dati dal buffer solo se lo
stesso non sar vuoto, altrimenti si porr in attesa invocando il comando wait;
il comando notifyAll, presente nei metodi write e read, che notificher il thread in
attesa (PRODUCER o CONSUMER) che pu provare a riprendere possesso del proprio
blocco di codice sincronizzato per continuare la propria operazione.

Abbiamo poi la definizione delle classi RunProducer e RunConsumer, che invocano nei
rispettivi metodi run i metodi write e read delloggetto di tipo Buffer condiviso.

Il Listato 18.4 conclude la definizione della classe CoopSynchronizedThread, dove nel


metodo main vengono creati il buffer b e i thread t_1 e t_2 che lo gestiranno.

LOutput 18.4 mostra chiaramente come fra il thread PRODUCER e il thread CONSUMER vi
sia una sincronizzazione che consente al PRODUCER di scrivere il dato successivo solo
dopo che il CONSUMER ha letto il precedente e, viceversa, al CONSUMER di leggere il dato
solo dopo che il PRODUCER lha scritto.
Listato 18.5 CoopUnSynchronizedThread.
...
class Buffer
{
...
public void write(int val) // no sync
{
data[val] = val;

System.out.println(Thread.currentThread().getName()
+ " ha scritto all'indice " + val + " il valore: " + data[val]);
}
public int read(int ix) // not sync
{
System.out.println(Thread.currentThread().getName()
+ " ha letto all'indice " + ix + " il valore: " + data[ix]);
return data[ix];
}
public int getBufferElements(){ return nr_elem; }
}

class RunProducer implements Runnable // Runnable per il thread 1


{
...
public void run()
{
for (int i = 0; i < b.getBufferElements(); i++)
{
Random r = new Random();
int ms = r.nextInt(5000);
...
}
}
}

class RunConsumer implements Runnable // Runnable per il thread 2


{
...
public void run()
{
for (int i = 0; i < b.getBufferElements(); i++)
{
Random r = new Random();
int ms = r.nextInt(1500);
...
}
}
}

public class CoopUnSynchronizedThread { ... }

Output 18.5 Dal Listato 18.5 della Classe CoopUnSynchronizedThread.


CONSUMER ha letto all'indice 0 il valore: 0
CONSUMER ha letto all'indice 1 il valore: 0
...
CONSUMER ha letto all'indice 4 il valore: 0
PRODUCER ha scritto all'indice 0 il valore: 0
CONSUMER ha letto all'indice 5 il valore: 0
CONSUMER ha letto all'indice 6 il valore: 0
CONSUMER ha letto all'indice 7 il valore: 0
PRODUCER ha scritto all'indice 1 il valore: 1
...

Nel Listato 18.5 abbiamo definito le stesse classi del Listato 18.4, ma con i metodi
write e read senza alcuna sincronizzazione e di conseguenza senza i comandi wait e

notifyAll, cos come senza il ciclo while e la variabile empty.

Inoltre, al fine di rendere la simulazione pi realistica, abbiamo impostato nel


metodo run della classe RunProducer un timer di sleep tra 0 e 5 secondi, mentre nel
metodo run della classe RunConsumer il timer di sleep va da 0 a 1,5 secondi.

Tali impostazioni consentiranno di evidenziare come il thread CONSUMER legga quasi


sempre prima che il thread PRODUCER possa scrivere; ci avviene perch, oltre al fatto
che non vi alcun controllo di sincronizzazione, il timer di sleep del CONSUMER sempre
inferiore al timer di sleep del PRODUCER.
LOutput 18.5 mostra come il thread CONSUMER legga sempre dal buffer il valore 0,
perch il thread PRODUCER non riesce mai a scrivere il dato prima che il CONSUMER lo possa
leggere.
Liveness dei thread
Per liveness dei thread intendiamo la capacit di un programma concorrente di far
vivere e progredire nel tempo i propri thread fino al compimento delle operazioni
relative, evitando condizioni che possano determinarne un blocco.
Un programma concorrente pu bloccarsi per effetto delle seguenti cause.

Deadlock: due o pi thread rimangono bloccati indefinitamente perch aspettano


reciprocamente che ciascuno rilasci il lock sulloggetto richiesto.
Starvation: un thread, generalmente con unalta priorit, ottiene un lock su un
oggetto condiviso e compie delle operazioni che non consentono mai di
rilasciarlo agli altri thread con una pi bassa priorit.
Livelock: due o pi thread, pur continuando a essere operativi (non sono bloccati
come nel caso del deadlock), non riescono a progredire nel compimento delle
loro rispettive azioni perch non comunicano tra loro alcuna risposta che indica
un cambiamento di stato e che permetta di fare terminare i rispettivi task.

Un esempio di deadlock
Lesempio che segue mostra come ottenere una situazione di deadlock dove un
thread rimane bloccato perch attende che un altro thread rilasci il lock su un oggetto
da esso richiesto, e laltro thread rimane altres bloccato perch attende che il primo
thread rilasci il lock su un oggetto da questultimo richiesto.
Listato 18.6 Classe Deadlock.
...
class Door
{
private Door other_door;
private String porta;

public Door(String p) { porta = p; }



public synchronized void openDoor() // apre una porta
{
try
{
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " ha aperto la " + porta);
System.out.println(Thread.currentThread().getName()
+ " sta tentando di aprire l'altra porta");
other_door.openOtherDoor();
}
catch (InterruptedException ex){}
}
public synchronized void openOtherDoor() // apre l'altra porta
{
try
{
Thread