8
Pellegrino Principe
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
StringBuilder
MY_JAVA_SOURCES
Time_Client.java
Capitolo 1
Introduzione al linguaggio
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
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
Figura 1.1 Struttura ad albero del JDK 1.8 a 32 bit creata in un sistema Windows.
derby.jar
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 runtime 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",
e cos via).
OS_VERSION="5.1"
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 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 //.
main
void
static
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
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.
Capitolo 2
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:
di 8 bit con un range di valori true o false;
boolean
di 16 bit con un range di valori da \u0000 a \uFFFF come definiti dallo standard
char
ISO Unicode;
byte di 8 bit con un range di valori da -128 a +127;
di 16 bit con un range di valori da 32.768 a +32.767;
short
int
long
+9.223.372.036.854.775.807
float
double
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
65535
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
}
}
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
, 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.
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.
}
}
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
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);
}
}
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 il backslash;
per il Tab;
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";
323
Infine, in espressioni con valori diversi da un tipo int (per esempio valori di tipo
o short) Java convertir automaticamente gli operandi nel tipo int, e se tale valore
byte
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
allora tutta lespressione sar double.
double
Nello Snippet 2.14 lintera espressione sar valutata ed eseguita come segue:
1.
(c*i)
2.
(f*b)
3.
(d/s)
ris
Capitolo 3
Array
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.
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
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).
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
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();
}
}
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.
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.
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).
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
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++)
{
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]
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
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
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);
}
}
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.
Espressione 2
Risultato
false
false
false
false
true
false
true
false
false
true
true
true
Espressione 1
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
false
!false
true
Risultato
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
e char:
int long
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
~A
A & B
A | B
A ^ B
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);
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);
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;
Operatore
Operazione
Associativit
[]
indice array
sinistra
&&&
()
sinistra
&&&
accesso di membro*
sinistra
&&&
++
postincremento
destra
&&&
--
postdecremento
destra
++
preincremento
destra
&&&
--
predecremento
destra
&&&
pi unario
destra
&&&
meno unario
destra
&&&
bitwise NOT
destra
&&&
NOT logico
destra
(type)
cast
destra
&&&
new
creazione di un oggetto*
destra
moltiplicazione
sinistra
&&&
divisione
sinistra
&&&
modulo
sinistra
addizione
sinistra
&&&
sottrazione
sinistra
<<
sinistra
&&&
>>
sinistra
&&&
>>>
sinistra
<
minore di
sinistra
&&&
<=
minore di o uguale a
sinistra
&&&
>
maggiore di
sinistra
&&&
>=
maggiore di o uguale a
sinistra
&&&
instanceof
test di referenza*
sinistra
==
uguale a
sinistra
&&&
!=
diverso da
sinistra
&
bitwise AND
sinistra
&&&
&
sinistra
10
bitwise XOR
sinistra
&&&
sinistra
11
bitwise OR
sinistra
&&&
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
&&&
<<=
destra
&&&
>>=
destra
&&&
>>>=
destra
&&&
&=
AND e assegnamento
destra
&&&
^=
XOR e assegnamento
destra
&&&
|=
OR e assegnamento
destra
Capitolo 5
Strutture di controllo
Nel Listato 5.1 listruzione if valuta lespressione a < 10, la quale ritorna un valore
, e pertanto viene stampata in output la stringa "a < 10".
true
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
}
}
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");
}
}
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".
case
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");
}
}
}
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");
}
}
}
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;
}
}
}
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-- + " ");
}
}
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.
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
}
}
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.
for
La keyword for seguita dalle parentesi tonde ( ) che racchiudono tre espressioni:
che inizializza delle variabili, expression2 che controlla se la condizione di
expression1
expression1
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.
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-- + " ");
}
}
}
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!!!
}
}
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");
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
da cui sar estratto il valore da assegnare alla variabile indicata.
Iterable
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
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;
}
...
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
{
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.
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
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
}
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.
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
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
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"); }
}
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
LOutput 6.3 mostra chiaramente che an_object non punta pi a un oggetto di tipo
, ma a un oggetto di tipo AClass_child_2, mentre la Figura 6.2 evidenzia la
AClass_child_1
Figura 6.2 Differenze tra Java e C# nella manipolazione di un riferimento passato come argomento.
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
}
}
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.
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);
}
convergere il metodo verso il suo caso base e far cos ritornare, in successione, tutti i
metodi invocati (Figura 6.3).
number
number
number
number
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
comando java, in accordo con le regole citate nellIntroduzione, tale file si trover nella cartella
MY_JAVA_CLASSES.
Capitolo 7
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
}
Il Listato 7.1 mostra la definizione di una classe denominata Time con modificatore
e che deriva (discende) dalla classe Object.
public
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.
: indica che i membri sono utilizzabili da membri di altre classi esterne alla
public
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
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.
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
crea una stringa formata dallunione di "Time con i valori impostati: "
impostati: " + t
...
public int getOra() // ottengo l'ora
{
return ora;
}
public int getMinuti() // ottengo i minuti
{
return minuti;
}
public int getSecondi() // ottengo i secondi
{
return 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
corrente avr come dati per lorario i dati delloggetto passato come
Time_REV_3
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() + "]");
}
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());
}
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.
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.
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
}
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);
}
}
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
e utilizzare il riferimento this o super, poich tali membri esistono solo se
static
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
di una unit.
how_many
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();
}
}
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
e poi invoca il suo metodo display() che accede direttamente alla variabile outer_x
Guest
della classe Host. Successivamente, il medesimo metodo main invoca anche il metodo
di outer, che crea loggetto a_local di tipo LC e ne invoca il metodo show che
local
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()
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
}
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;
}
}
}
Capitolo 8
Triangle
8.1).
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
. Si pu anche dire che la classe Circle indirettamente una sottoclasse della
Shape
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,
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
ereditato da Object stessa, al fine di stampare loggetto che rappresenta in
toString
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 + " } "; }
}
getCoords
area
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
e protected della classe genitrice Rectangle (e indirettamente anche quelli della
public
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:
getCoords
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 {}
NOTA
Se utilizziamo la keyword static su un metodo, di fatto lo rendiamo sia metodo di classe sia
metodo non sovrascrivibile.
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:
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
(che andr a sovrascrivere il metodo draw della classe Shape ereditato
draw
draw
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.
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
}
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 ";
}
}
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 {}
NO = 0 YES = 1
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
che saranno, tutti, specificamente implementati dalle figure geometriche che
perimeter
le realizzeranno.
Listato 8.12 Interfaccia Shape.
package com.pellegrinoprincipe;
public interface Shape
{
public void draw(); // metodo per il disegno
}
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
definiamo specificamente i metodi area, perimeter e draw.
Rectangle_REV_1
Nel Listato 8.15 vediamo che la classe Square_REV_1 eredita dalla classe
dando una sua definizione in overriding del metodo draw, il quale,
Rectangle_REV_1
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();
}
}
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() {}
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);
}
}
doShape
doEmployee
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.
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
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 };
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
{
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()
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]);
...
}
}
Object
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; }
}
Il Decompilato 9.3 evidenzia come il metodo get abbia come tipo di ritorno un
che lupper bound usato dal compilatore in fase di erasure per il parametro di
Object
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.
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--];
}
}
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
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 + " ");
}
}
}
Snippet 9.6 Riferimento di tipo raw con oggetto creato non di tipo raw.
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);
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 + " ");
}
}
}
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
possiamo asserire che, mentre vero che Foo un sottotipo di Bar e
extends Bar {}
ArrayList<Double>
ArrayList<Number>
double
ClassCastException
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.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>()
ArrayList<Number>().
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
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
Capitolo 10
Programmazione funzionale
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.
casella di testo.
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.
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.
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))
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 *
, ossia ha visibilit (scope) solo in quel corpo, e ha la variabile y libera, ossia che
y)
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();
}
<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>
...
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
{
Il Listato 10.1 definisce la classe CheckingAccount come final e le variabili name, bank e
di tipo String e accounts di tipo List<Integer> anchesse tutte final e dunque non
city
city
Per quanto concerne la copia di accounts abbiamo usato il costruttore della classe
,
sono di tipo primitivo (int). Nel caso in cui, invece, gli elementi fossero stati
accounts
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);
println
orderAccounts
NOTA
Approfondiremo tra breve in che modo Java ha implementato il concetto di funzione come firstclass 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);
}
}
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.
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.
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).
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 */ }
...
}
protected
implementazione.
Sintassi 10.3 Metodo di interfaccia statico.
[modifiers] interface MyInterface
{
static void bar() { /* implementazione */ }
...
}
Tabella 10.1 Differenze dei metodi static tra uninterfaccia e una classe.
Invocati con un riferimento
Invocati con il nome
Si ereditano
classe
interfaccia
No
No
private
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();
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.
// 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);
}
}
{
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);
}
}
Nellambito del metodo accept della classe MyDoubleConsumer utile ricordare che per
accedere alla classe contenitrice Operations dobbiamo utilizzare listruzione
, mentre per accedere alla superclasse della classe contenitrice Operations
Operations.this
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
int
int
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
MyIntBinaryOperator
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
apply(String t)
Function<String, 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
Capitolo 11
Errori software
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;
Le eccezioni
Le eccezioni sono gestite con lutilizzo delle keyword.
: la clausola try consente di specificare un blocco di codice al cui interno
try/catch
throw
throws
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
ArithmeticException
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.
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());
}
}
}
loopIn
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());
}
}
}
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
di cui leccezione e pertanto il sistema, non trovando in quel
ExceptionDivideByZero_REV_1
main
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()); }
}
}
Esempi di eccezioni unchecked sono quelle del tipo RuntimeException come, per
esempio:
, che si verifica quando si cerca di manipolare gli elementi
ArrayOutOfBoundsException
IOException
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;
FileScanner
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
si prender limpegno di gestire leccezione.
try/catch
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)) {}.
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
, invece, definisce un metodo main che prima crea un
ConstructorDestructorExceptions
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
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.
ritorna un oggetto di tipo Throwable the rappresenta la
{
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");
}
}
}
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.
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.
Error
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 runtime 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).
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,
,
e cos via.
Event Font
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 {}
com.pellegrinoprincipe
import static
}
}
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; }
}
com\pellegrinoprincipe\hardware
Computer.class
Computer
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; }
}
Il comando della Shell 12.4 compila il file Software.java e crea, a partire dalla
directory corrente indicata dal carattere punto (.), la struttura
, allinterno della quale saranno posti i file
com\pellegrinoprincipe\software
e Software.class.
Software$Graphic.class
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
), tenendo presente che anche la JVM, come il compilatore, deve
ComputerClient_REV_1
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.
set CLASSPATH=.;C:\MY_JAVA_PACKAGES;C:\MY_JAVA_SOURCES
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
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 *
MANIFEST.MF
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)
conterr il metodo main la classe ComputerClient_REV_1, che porremo (il relativo file
), unitamente alla sua struttura di directory (com\thp), nella
ComputerClient_REV_1.class
HardwareAndSoftwareAPI.jar
java
NOTA
possibile estrarre tutti i file dallarchivio non indicando nessun singolo file.
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
).
Printer.class
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.
Files (x86)\Java\jdk1.8.0\jre
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.
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
), avr di default quello di tipo package.
protected
getPrinterName
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)
il
file Computer_REV_1.java
(javac
-cp
protected
public
myPackage.MyClass.ex
myPackage.ExtendMyClass.ex
no
myPackage.OtherClass.ex
no
otherPackage.ExtendMyClassOtherPackage.ex
no
no
otherPackage.OtherClassOtherPackage.ex
no
no
no
ExtendMyClass
name
otherPackage.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
}
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.
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()
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) {}
}
Annotazione @Deprecated
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();
}
}
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.
Il Listato 13.4 mostra come si scrive unannotazione che accetta valori multipli.
Infatti, sul metodo manyWarnings insiste lannotazione @SuppressWarnings({"fallthrough",
, che indica al compilatore di non avvisare se durante la compilazione
"divzero"})
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 , che indica al compilatore di generare solo i tipi di warning
Xlint:fallthrough,divzero
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
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
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);
@SafeVarargs
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
, che dopo lerasure diventato di tipo List[], al riferimento an_array di tipo
lists
Object[]
List<String>
String
HeapPollution.java
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();
}
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> { ...
;
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,
,
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.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 metaannotations.
, con cui si specifica su quale elemento del programma lannotazione
@Target
LOCAL_VARIABLE
@Retention
javadoc
@Repeatable
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
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() {}
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")}).
SupportedSourceVersion
@SupportedSourceVersion
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.
C:\MY_JAVA_CLASSES WorkToDo_REV_1.java
).
C:\MY_JAVA_CLASSES AnnCustomProcessor.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 runtime 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!"); }
...
}
C:\MY_JAVA_CLASSES WorkToDo_REV_2.java
C:\MY_JAVA_CLASSES AnnCustom_REV_2.java
).
Capitolo 14
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.
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:
, che indica la sezione Parameters e descrive i parametri del metodo; @throws,
@param
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
description
{@code text}
<code>
@see
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() {}
{@literal text}
>
{@value package.class#field}
membro costante.
Snippet 14.4 Tag {@value}.
/**
* __do Valore costante: {@value}
*/
private final int __do = 10;
{@docRoot}
{@inheritDoc}
* 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;
indica file che contengono le opzioni di javadoc, i nomi dei package e i nomi dei
@files
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
Capitolo 15
Caratteri e stringhe
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
, che accetta come argomento il carattere da incapsulare, sia assegnando
Character
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
. Lo Snippet 15.2 mostra che il metodo ha ritornato il valore 1 a
anotherCharacter
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.
verifica se il carattere ch pu
verifica se il carattere ch pu
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.
public String()
a 0.
Snippet 15.12 String.
String s1 = new String(); // stringa vuota
String s2 = ""; // stringa vuota
value
copia una
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
public boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset,
int len)
toffset
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.
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!
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
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));
}
}
, che ha aggiunto alla fine della stringa i caratteri passati come argomento.
append
insert
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.
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!
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 + " ");
}
}
}
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
crea un oggetto
Capitolo 16
Espressioni regolari
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
Il carattere x
\\
Il carattere backslash
\0n
\xhh
\uhhhh
\t
Il carattere TAB
\n
\r
\f
Il carattere FORM-FEED
\a
Il carattere BELL
\e
Il carattere ESCAPE
Corrispondenza
Esempio
[abc]
a, b oppure c
[^abc]
[a-zA-Z]
[a-d[m-p]]
[a-z&&[def]]
[a-z&&[^bc]]
[a-z&&[^m-p]]
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
\d
\D
\s
\S
\w
\W
Corrispondenza
Esempio
\b
\B
\A
\Z
\z
\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()
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).
Corrispondenza
Esempio
x?
x*
x+
x{n}
x{n,}
x{n,m}
Costrutto
Esempio
xy
x | y
(x)
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.
Corrispondenza
Esempio
(?<name>x)
(?:x)
x(?=y)
x(?!y)
(?<=y)x
(?<!y)x
(?<numword>\d\w)\k<numword>
a cui segue il nome dato al gruppo di cattura tra le parentesi angolari < >.
\k
0\d/\d{6}
[a-z]+(\s[A-Z][a-z]+)?
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
, per creare un oggetto di tipo Pattern che conterr lespressione regolare
Pattern
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.
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
), con cui consentiamo la ricerca di corrispondenza senza
CASE_INSENSITIVE
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
il carattere new line. Se avessimo omesso tale flag e avessimo invocato il
dotall
CASE_INSENSITIVE
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.
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.
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.
ritorna la corrispondenza trovata relativamente al
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.
cerca, allinizio della stringa da comparare, la sequenza
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.
consente di cambiare, per questoggetto
Matcher
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
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
stream.
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).
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.
aggiunge lelemento indicato dal parametro e. Ritorna true se
boolean add(E e)
void clear()
boolean contains(Object o)
dal parametro o.
verifica se una collezione contiene tutti gli
boolean containsAll(Collection<?> c)
boolean isEmpty()
Iterator<E> iterator()
boolean removeAll(Collection<?> c)
boolean retainAll(Collection<?> c)
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.
ritorna il numero di elementi presenti in una collezione.
int size()
Object[] toArray()
collection.toArray(new String[0])
Collection<String>
Linterfaccia Set
boolean containsAll(Collection<?> c)
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
E first()
E last()
formato dagli elementi compresi nel range indicato dal parametro fromElement
(incluso) e dal parametro toElement (escluso).
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.
ritorna il pi piccolo elemento di un insieme che sia maggiore o
E ceiling(E e)
E floor(E e)
E higher(E e)
E lower(E e)
Iterator<E> descendingIterator()
insieme formato dagli elementi che, secondo lordinamento, sono posti prima (o
finiscono con, se il parametro inclusive true) lelemento fornito dal parametro
.
toElement
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
toInclusive)
fromInclusive
).
true
E pollFirst()
E pollLast()
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.
aggiunge nella lista lelemento del parametro element
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.
ritorna la posizione nella lista della prima occorrenza
int indexOf(Object o)
int lastIndexOf(Object o)
ListIterator<E> listIterator()
ListIterator
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.
E element()
boolean offer(E e)
E remove()
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.
aggiunge in testa a una doppia coda lelemento fornito dal
void addFirst(E e)
void addLast(E e)
E removeFirst()
E removeLast()
E getFirst()
E getLast()
boolean removeFirstOccurrence(Object o)
boolean removeLastOccurrence(Object o)
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.
inserisce in una mappa il valore fornito dal parametro value
value
V get(Object key)
V remove(Object key)
void clear()
int size()
boolean isEmpty()
Collection<V> values()
Set<K> keySet()
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.
lordinamento delle chiavi di una mappa. Se non stato fornito alcun oggetto
comparatore, il metodo ritorner null.
ritorna la chiave di una mappa nella prima posizione
K firstKey()
nellordinamento.
K lastKey() ritorna la chiave di una mappa nellultima posizione
nellordinamento.
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).
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).
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.
ritorna unentry di una mappa la cui pi piccola
K ceilingKey(K key)
K floorKey(K key)
K higherKey(K key)
K lowerKey(K key)
NavigableMap<K,V> descendingMap()
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.
,
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
true).
toInclusive
Map.Entry<K,V> pollFirstEntry()
Map.Entry<K,V> pollLastEntry()
HashSet
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
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
List
rest)
DaysOfTheWeek.WEDNESDAY
ArrayList
LinkedList
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);
}
}
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
ArrayDeque
LinkedList
PriorityQueue
System.out.println();
}
}
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.
HashMap
WeakHashMap
equals
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).
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;
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
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.
Comparator
o2)
Comparator
java.util
boolean hasNext()
E next()
void remove()
void add(E e)
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.
modifica lultimo elemento ritornato dai metodi next o previous con
void set(E e)
boolean hasPrevious()
E previous()
int nextIndex()
metodo next.
int previousIndex()
hasNext
hasPrevious
ritornerebbe lelemento C;
next
ritornerebbe lelemento B;
previous
ritornerebbe lindice 2;
nextIndex
ritornerebbe lindice 1;
previousIndex
add
set
previous
remove
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.
Numbers
public static <T extends Comparable<? super T>> void sort(List<T> list)
naturale e ascendente gli elementi passati dalla lista del parametro list.
ordina, nelle
modalit indicate dal comparatore del parametro c, gli elementi passati dalla lista
del parametro list.
mescola (disordina) gli elementi della
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
. La lista fornita deve gi essere ordinata. Se lelemento key trovato, il
list
point + 1)
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
,
parametro list.
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)
comp)
public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T>
coll)
comp)
parametro list, gli elementi inseriti alle posizioni indicate dai parametri i e j.