Un
primo esempio
Per rompere il ghiaccio, non c'é niente
di meglio che un esempio pratico. Il seguente
esempio è tratto direttamente dal tutorial
di Pizza [1], un'estensione di Java che già
nel '96 permetteva l'uso dei Generics in Java:
public
class Pair<A,B> {
private A a;
private B b;
public Pair(A a, B b) {
this.a = a;
this.b = b;
}
public A getA() {
return a;
}
public B getB() {
return b;
}
}
Si
osservi il frammento di codice: se si escludono
i simboli racchiusi tra parentesi angolari, dovrebbe
risultare piuttosto familiare. Gli identificatori
<A,B> presenti nella dichiarazione della
classe sono i Tipi Parametrici che verranno usati
all'interno della classe. Questi identificatori
denotano tipi invece che variabili: all'interno
della classe Pair tali simboli compaiano nelle
dichiarazioni di variabili, tra i parametri del
costruttore e come valori di ritorno sempre ed
esclusivamente al posto di un normale identificatore
di tipo.
Una
classe come Pair può tornare utile in tutti
quei casi in cui si desidera che un metodo restituisca
una coppia di valori, invece che uno solo:
public
class Lotto {
public static Pair<Integer,Integer>
getAmbo() {
int primoEstratto = (int)(1+89*Math.random());
int secondoEstratto =
(int)(1+89*Math.random());
return new Pair<Integer,Integer>(primoEstratto,secondoEstratto);
}
}
Si
noti che nel precedente metodo è stata
utilizzata la nuova funzionalità di Autoboxing
di tipi primitivi: sebbene i parametri formali
della Pair fossero Integer, al costruttore sono
stati passati due numeri interi, dal momento che
il nuovo compilatore si occupa automaticamente
della conversione tra tipi interi e Wrapper Type
(per approfondimenti cfr [2]). Ecco un esempio
di utilizzo del metodo precedente:
Pair<Integer,Integer>
a = Lotto.getAmbo();
System.out.println("I numeri estratti sono:
" +a.getA()+" e "+a.getB());
Dopo
questa breve introduzione, è il momento
di passare ad una trattazione più formale
e completa.
Cenni
sull'implementazione
Una breve digressione sulla reale implementazione
dei Generics in Java può aiutare a capire
meglio l'argomento. In C++, una classe generica
(detta in questo caso "Template") veniva
riscritta e compilata ad hoc per ogni sua realizzazione
concreta. Questo meccanismo dava luogo ad un fenomeno
detto "Code Bloat" (esplosione di codice)
che fu la principale ragione per cui il polimorfismo
parametrico venne abbandonato dai progettisti
di Java.
In
Java 5, i Generics vengono implementati con un
meccanismo di espansione macro detto "Erasure
and Translation" (Cancellazione e Traduzione).
Dopo aver effettuato gli opportuni controlli di
consistenza tra tipi, il compilatore cancellata
(Erasure) tutte le informazioni relative ai tipi
parametrici, e sostituisce le relative occorrenze
con il tipo generico Object. La classe Pair vista
nel paragrafo precedente verrebbe riscritta più
o meno come segue:
public
class Pair {
private Object a;
private Object b;
public Pair(Object a, Object b) {
this.a = a;
this.b = b;
}
public Object getA() {
return a;
}
public Object getB() {
return b;
}
}
A
questo punto il compilatore genera il bytecode
relativo a questa classe. Nel contempo, tutte
le chiamate ai metodi di una classe parametrica
presenti in codice che utilizza tale classe come
client vengono tradotti (Translated) mediante
l'inserimento di un'operazione di casting:
Pair
a = Lotto.getAmbo();
System.out.println("I numeri estratti sono:
" +(Integer)a.getA()+" e "+(Integer)a.getB());
Questa
spiegazione, per quanto semplice e incompleta,
non è molto lontana dalla realtà.
I Generics eseguono in modo automatico lo stesso
lavoro che prima era necessario eseguire manualmente,
operando nel contempo i controlli che garantiscono
la type safety, senza problemi di code bloat o
altre idiosincrasie tipiche dell'implementazione
C++.
Compromessi
implementativi e comportamenti inattesi
L'implementazione dei Generics descritta nel precedente
paragrafo non è priva di compromessi, che
danno luogo ad alcuni comportamenti inattesi.
Il primo di questi è che tutte le istanze
di una classe generica condividono la stessa classe
di base, anche se vengono specializzate in modo
differente. Si osservi il seguente frammento di
codice:
Pair<Integer,Integer>
c1 = new Pair<Integer,Integer>(24,5);
Pair<String,Integer> c2 = new Pair<Integer,Integer>("Ciao",73);
System.out.println(c1.getClass() == c2.getClass());
Intuitivamente
ci si aspetterebbe che le classi c1 e c2 differiscano
tra loro per tipo;. Nella realtà, dal momento
che le due istanze condividono la stessa classe
di base, si otterrà come risposta "true".
Il
secondo comportamento inatteso è l'impossibilità
di assegnare un'istanza di una classe parametrica
ad un reference con tipo parametrico più
generico:
Vector<String>
v1 = new Vector<String>();
Vector<Object> v2 = v1; // errore di compilazione
La
ragione di questo comportamento è in realtà
abbastanza semplice: se una simile operazione
fosse consentita, si otterrebbero due reference
di tipo parametrico diverso che puntano allo stesso
Vector di String, e pertanto potremmo inserire
all'interno di tale vettore oggetti differenti
da String usando il reference v2:
v2.add(new
Object); // violazione della type safety
Bisogna
comunque tener presente che il primo obbiettivo
dei Generics è garantire la type safety,
pertanto queste limitazioni non vanno interpretate
come difetti, ma come normali compromessi di un'implementazione
che per tutti gli altri versi appare geniale.
Tipo
Parametrico Formale e Tipo Parametrico Reale
Quando si parla dei parametri di un metodo si
pone l'accento sulla differenza esistente tra
i nomi con cui i parametri vengono dichiarati
(parametri formali) e i valori con cui vengono
invocati in una chiamata (parametri reali),
public
static void myMethod(int a, String s) {
}
// a e s sono parametric formali
myMethod(10,"pluto"); // 10 e "pluto"
sono parametri reali
Allo
stesso modo, quando si usano i tipi parametrici,
bisogna distinguere tra i due possibili utilizzi
degli identificatori presenti tra parentesi angolari.
Quelli presenti nella dichiarazione di una classe,
come i simboli A e B nel frammento di codice seguente,
vengono detti tipi parametrici formali:
public
class Pair<A,B> {
....
}
Essi
denotano gli identificatori con cui vengono designati
i tipi parametrici all'interno della classe. Al
contrario, gli identificatori presenti in un'invocazione
vengono detti parametri reali, e rappresentano
i tipi che verranno effettivamente usati dall'istanza:
Pair<Integer,Integer>
= new Pair<Integer,Integer>(12,15);
Nota:
la letteratura in lingua Italiana spesso utilizza
la definizione "Parametro Attuale" al
posto di quella presente in questa trattazione
("Parametro Reale"). La ragione è
legata ad una traduzione approssimativa del termine
inglese "Actual", il cui significato
non è, come ci si aspetterebbe, "Attuale",
ma perl'appunto "Reale".
Carattere Jolly (Wildcard)
Un'aspetto fino ad ora trascurato dei Generics
è l'esistenza di un carattere jolly (wildcard)
e le sue importanti implicazioni. Come visto nel
precedente paragrafo, se si desidera definire
una Collection in grado di accettare qualunque
tipo, non possiamo ricorrere alla definizione
Vector<Object> che, al contrario, denota
una Collection in grado di accettare unicamente
oggetti di tipo Object. Per ovviare a questo problema,
è stato introdotto il carattere jolly "?"
che permette di definire Collection di tipo parametrico
sconosciuto:
Vector<?>
v = new Vector<?>();
Si
noti che per definire una Collection di tipo sconosciuto
è possibile ricorrere anche alla sintassi
tradizionale:
Vector
v = new Vector();
Il
carattere jolly serve anzitutto in tutte le situazioni
in cui i tipi parametrici sono più di uno:
HashMap<?,String>
h = new HashMap<?,String>();
In
secondo luogo, come verrà mostrato nei
prossimi paragrafi, il carattere jolly permette
di porre limiti superiori o inferiori al tipo
parametrico di una determinata classe.
Limiti
superiori
Come dobbiamo comportarci se vogliamo definire
una Collection in grado di ospitare qualunque
componente grafico sottoclasse di JComponent?
La sintassi dei Generics prevede un uso specifico
della parola chiave extends in unione con il carattere
jolly "?":
Vector<?
extends JComponent> v = new Vector<? extends
JComponent>();
La
riga precedente significa letteralmente "crea
un Vector in grado di contenere JComponent e qualunque
sua sottoclasse". Questo uso del carattere
Jolly prende il nome di "limite superiore"
(Upper Bound).
Ovviamente
possiamo usare i limiti superiori anche nelle
dichiarazioni di classe, e porre un limite superiore
direttamente ad un parametro formale:
public
class MyClass <N extends Number> {
....
}
Una
simile dichiarazione richiede che il parametro
reale della classe MyClass sia di tipo Number
o una sua sottoclasse (Integer, Double, Float
e così via).
Nota:
In questo caso il processo di Cancellazione e
Traduzione descritto nel secondo paragrafo sostituirà
tutte le occorrenze del simbolo N con Number,
anzichè con Object.
L'uso
di limiti superiori all'interno di una dichiarazione
di metodo comportano un ulteriore comportamento
inaspettato. Si osservi il seguente frammento
di codice:
public
void addComponent(Vector<? extends JComponent>
v) {
v.add(new JButton("Hello World");
// errore di compilazione!
}
Il
motivo è che se da una parte il compilatore
si aspetta che il Vector v contenga una qualche
sottoclasse di JComponent, non ha alcun indizio
di quale sarà usata in pratica). Pertanto
è costretto a rifiutare la compilazione.
Alcuni metodi di Collection con il carattere jolly
Una volta introdotti i concetti di carattere jolly
e di limite superiore, è possibile spiegare
alcuni metodi dell'interfaccia Collection tralasciati
nel precedente articolo:
boolean
addAll(Collection<? extends E> c)
aggiunge
alla Collection un insieme di elementi dello stesso
tipo del parametro formale E o di una sua sottoclasse.
boolean
containsAll(Collection<?> c)
verifica
se gli elementi presenti nella Collection c sono
presenti anche nella Collection su cui viene invocato
il metodo. Ci si può chiedere come mai
questo metodo ricorra al carattere jolly, invece
di richiedere esplicitamente una Collection dello
stesso tipo di quella su cui si sta lavorando.
La ragione è semplice: nessuno vieta di
creare una Collection<String> e una Collection<?>
piena di String: un confronto tra queste sarebbe
assolutamente legale. L'uso di un vincolo diverso
da "?" porrebbe in questo caso dei limiti
di utilizzo inaccettabili dell'API Collection.
Limiti
Inferiori
Ci sono casi in cui invece di fissare un limite
superiore può risultare comodo fissare
un limite inferiore. L'implementazione Java dei
Generics prevede anche questa possibilità
attraverso un uso della parola chiave super. Un
esempio di uso dei limiti inferiori può
essere il metodo containsAll() di Collection descritto
nel precedente paragrafo, che potrebbe essere
riscritto in questo modo:
boolean
containsAll(Collection<? Super E> c)
In
questo modo esso accetterebbe di verificare la
presenza di elementi dello stesso tipo E della
collection, o di un suo supertipo (ad esempio
Object). Nel prossimo paragrafo vedremo ulteriori
applicazioni dei limiti inferiori nei metodi parametrici.
Nota:
Il motivo per cui il metodo containsAll() di Collection
non utilizza questa possibilità è
la necessità di rendere la libreria retro
compatibile: il metodo infatti deve funzionare
anche con Collection di un tipo qualunque, anche
se scorrelato dal parametro reale del contenitore.
In casi come questo, la chiamata è assolutamente
legale, e il metodo si limita a restituire false.
Un
altro esempio preso dal framework Collection è
il metodo comparator() dell'interfaccia SortedMap,
che denota una famiglia di mappe hash in cui le
chiavi sono oridinate in direzione ascendente
secondo i criteri stabiliti da un opportuno Comparator:
Comparator<?
super K> comparator()
Questo
metodo restituisce il Comparator associate alla
SortedMap, che è a sua volta un'interfaccia
parametrica, il cui tipo deve, in questo caso,
corrispondere al tipo parametrico K dell'interfaccia
Map o ad una sua superclasse.
Metodi
Generici
Come si è visto nel precedente articolo,
i tipi parametrici possono essere usati anche
nelle firme di metodi, anche all'interno di classi
non parametriche. I metodi generici permettono
di usare i tipi parametrici per esprimere una
dipendenza tra il tipo degli argomenti di un metodo
e il suo tipo di ritorno, come si è già
visto nell'esempio del metodo toArray() dell'interfaccia
Collection:
public
<T> T[] toArray(T[] a);
I
metodi generici servono anche a sottolineare la
dipendenza tra il tipo di un argomento e quello
di un'altro; il seguente metodo, preso dalla classe
Collections, è un esempio di questo secondo
tipo di uso:
static
<T> void fill(List<? super T> list,
T obj)
Questo
metodo riempie la lista passata come parametro
con l'elemento specificato nel secondo parametro.
Si noti la necessità di porre un limite
inferiore al primo parametro del metodo: dal momento
che il tipo parametrico reale deve essere dedotto
in modo automatico dal tipo dei parametri reali,
come già sottolineato nel precedente articolo,
è necessario limitare il tipo parametrico
della List, dal momento che il tipo parametrico
reale è quello dell'oggetto obj, non quello
della Collection, che in questo caso può
essere anche di tipo più generico (ad esempio
Object).
Convenzioni
di Naming
Un ultimo argomento, la cui importanza non va
sottovalutata, sono le convenzioni di naming degli
identificatori di tipo presenti nelle classi parametriche.
Come si è visto in tutti gli esempi, si
preferisce usare nomi composti da un singolo carattere
maiuscolo, Qualora si usi più di un carattere,
si raccomanda di non usarne più di 2 o
3, tutti maiuscoli in modo che sia possibile distinguere
ad occhio gli identificatori di tipo da quelli
di variabile. Infine i nomi, per quanto sintetici,
devono sempre essere
evocativi, come si è visto negli esempi:
E per Element, T per Type, K per Key e così
via.
Conclusioni
Questo mese abbiamo portato a termine lo studio
sui tipi generici. Dapprima abbiamo imparato come
creare una semplice classe parametrica; quindi
abbiamo accennato a come i Generics vengono implementati
dal compilatore; dopo abbiamo appreso l'esistenza
di un carattere jolly "?" e dei suoi
possibili usi; infine abbiamo approfondito la
conoscenza dei metodi parametrici, già
introdotti il mese scorso. Il mese prossimo impareremo
ad usare le enum, un altro nuovo costrutto di
Java 5.
Bibliografia
[1] Pizza Tutorial:
http://pizzacompiler.sourceforge.net/doc/tutorial.html
[2] Andrea Gini, "Java2 Standard Edition
1.5 beta 1", Mokabyte 3/2004
http://www.mokabyte.it/2004/03/jdk1_5.htm
|