Cosa
sono i tipi generici
Prima di iniziare un discorso sui Generics è utile
capire cosa si intenda in Java per "Tipo Generico".
A differenza di atri linguaggi OO, in Java esiste un'unica
gerarchia di oggetti, che fa capo ad Object. Object è
la superclasse di qualunque oggetto: essa è il Tipo
Generico, dal momento che qualunque classe è, tra le
altre cose, un Object. Grazie a questa proprietà di
O-bject, è possibile creare oggetti Container come
Vector, un vettore dinamico in grado di con-tenere qualunque
tipo di oggetto:
Vector
v = new Vector();
v.add(new String("Tanto va la gatta al lardo");
v.add(new String("Che ci lascia lo zampino");
Il
difetto di questo approccio, come noto, è la necessità
di ricorrere al casting per recuperare gli oggetti presenti
nel contenitore:
String
s = (String)v.get(1);
In
secondo luogo non esiste nessun controllo sul tipo di oggetti
che effettivamente vengono inseriti nel Vector: se per errore
si inserisce in v un oggetto diverso da String, la cosa verrà
no-tata soltanto in fase di esecuzione, quando ormai è
troppo tardi: una ClassCastException ter-minerà bruscamente
il programma.
I Generics, ovvero il Polimorfismo Parametrico in Java
Se
si osserva la documentazione Javadoc di Vector su Java 1.5
si noterà una differenza nella definizione della classe,
che ora si chiama Vector<E>. La E tra parentesi angolari
denota un Tipo Parametrico, vale a dire un tipo la cui definizione
è a carico dell'utilizzatore della classe:
Vector<String>
v = new Vector<String>();
La
riga precedente definisce è un Vector di String, ossia
un contenitore in cui possono entrare ed uscire solamente
oggetti di tipo String. Grazie ai tipi parametrici, la Type
Safety può ora essere effettuata dal compilatore, che
rifiuterà di compilare chiamate del tipo:
v.add(new
Integer(19));
Osservando
la documentazione Javadoc di Vector, si noterà che
l'identificatore di tipo para-metrico E ha preso il posto
del tipo generico Object in tutti i metodi in cui prima esso
compa-riva per inserire o estrarre elementi:
boolean
add(E o)
EelementAt(intindex)
E firstElement()
E get(int index)
Il
simbolo E (che ovviamente sta per Element) verrà ora
sostituito automaticamente con il tipo dichiarato in fase
di creazione:
String
s = v.get(1); // non è più necessario ricorrere
al casting
Nessun
bisogno di ricorrere al casting, nessun errore a Runtime:
l'adozione dei Generics non può destare altro che entusiasmo.
Anche Doppio!
All'interno del framework Collection è possibile trovare
classi che richiedono una coppia di Tipi Parametrici, come
l'interfaccia Map da cui derivano le familiari Hashtable e
HashMap:
HashMap<K,V>
In
questa dichiarazione, la lettera K denota il tipo della chiave
(Key), mentre V denota il valo-re (Value). Per chi non avesse
mai usato una Map, è sufficiente precisare che una
mappa Hash è un vettore associativo, che invece di
mantenere gli elementi in ordine posizionale (come avviene
negli array), associa ogni elemento ad una chiave univoca.
Un elenco telefoni-co, che associa nomi di persone a numeri
di telefono, è un buon esempio di come sia possibi-le
usare una mappa hash.
La
tradizionale HashMap utilizzava il tipo Object sia per le
chiavi che per i valori, cosa che cau-sava non pochi problemi
in fase di utilizzo non solo per l'usuale problema del casting,
ma anche per la confusione generata dal metodo:
public
void put(Object key, Object value)
in
cui era facile confondere le chiavi con il valore, data la
uguaglianza dei tipi. Vediamo come l'uso dei Generics permetta
di risolvere entrambi i problemi:
HashMap<String,Integer>
telephoneDirectory = new HashMap<String,Integer>();
telephoneDirectory.put("Aldo Rossi",new Integer(347657241));
telephoneDirectory.put("Elisa Bianchi",new Integer(340934170));
Dal
momento che chiavi e valori vengono associati a tipi diversi,
non si crea più alcuna con-fusione. Nessun problema
neppure in fase di interrogazione, dove non occorre più
ricorrere al casting:
Integer
n = telephoneDirectory.get("Aldo Rossi"); // niente
casting
System.out.println("Il numero di telefono di Aldo Rossi
e "+n.toString());
Si
noti che non esiste limite al numero di tipi parametrici definibili
per una determinata clas-se; nell'uso pratico è comunque
difficile che ne servano più di due.
Tipi Parametrici come valori di ritorno
Gli Identificatori di Tipo Parametrico possono essere usati
anche in modo più avanzato: una API parametrica può
ad esempio restituire come valore di ritorno una classe parametrica
del-lo stesso tipo di quello dichiarato al costruttore. Il
metodo
public
Iterator iterator()
presente
in tutte le implementazioni dell'interfaccia Collection restituisce
un oggetto che per-mette di iterare tra gli elementi di una
Collection in questo modo:
Iterator
i = v.iterator();
while(i.hasNext()) {
String s = (String)i.next();
System.out.println(s);
}
La nuova implementazione del metodo iterator() presenta un
ulteriore uso dei Generics;
public
Iterator<E> iterator();
In
questo modo, quando si chiama il metodo iterator() su una
Collection parametrica, viene restituito un Iterator dello
stesso tipo, in questo caso String:
Iterator<String>
i = v.iterator(); // Il Vector restituisce un Iterator<String>
while(i.hasNext()) {
String s = i.next(); // Non è necessario ricorrere
al casting
System.out.println(s);
}
Anche
in questo caso, l'uso del casting non è più
necessario.
Metodi Parametrici
Il polimorfismo parametrico può presentarsi anche a
livello di singolo metodo, perfino in una classe non parametrica.
I dettagli di questa possibilità verranno analizzati
dettagliata-mente nel prossimo articolo, per ora ci limiteremo
a considerare un metodo presente nell'in-terfaccia Collection
e in tutte le sue sottoclassi, tra le quali il Vector già
visto nei precedenti e-sempi. Tutte le Collection dispongono
di un metodo
public
Object[] toArray(Object[] a)
Lo
scopo di questo metodo è di trasferire il contenuto
di una Collection nell'array passato come parametro. L'array
può essere di tipo Object o, ancora meglio, dello stesso
tipo degli oggetti contenuti nella Collection; per quanto
riguarda la dimensione, esso viene automati-camente ridimensionato
qualora fosse troppo piccolo. L'uso caratteristico di questo
metodo era il seguente:
String[]
strings = (String[])v.toArray(new String[0]);
Si
noti che il metodo riceve un array di dimensione 0 creato
all'interno della chiamata stessa: come già detto sarà
il metodo stesso a ridimensionare l'array in modo che risulti
di dimensio-ne adeguata a contenere tutti gli elementi della
Collection. Si noti tuttavia che è ancora neces-sario
ricorrere al casting, dal momento che il metodo restituisce
un vettore di Object.
La
nuova versione del metodo toArray() ricorre, per motivi di
retrocompatibilità che non è il caso di analizzare
in questa sede, ad un metodo parametrico:
public
<T> T[] toArray(T[] a);
Come
si può vedere in questo esempio, la T tra parentesi
angolari compare prima della di-chiarazione del valore di
ritorno. La particolarità dei metodi parametrici è
che non richiedo-no la dichiarazione esplicita del tipo in
oggetto, visto che questo viene dedotto automatica-mente dal
tipo del parametro:
String[]
strings = v.toArray(new String[0]);
In
questo esempio, avendo passato come parametro un array di
String, avrò come valore di ritorno un array dello
stesso tipo. Anche in questo caso il ricorso ai Generics permette
di evi-tare errori e complicazioni.
Tipi Parametrici Nidificati
L'ultimo uso dei Generics che vedremo in questo articolo è
la possibilità di utilizzare i Gene-rics in modo nidificato,
per ottenere strutture dati particolari. Sappiamo che è
possibile defi-nire array a due dimensioni con una chiamata
di questo tipo:
String[][]
s = new String[10][10];
E'
possibile fare la stessa cosa con le librerie generiche? Fortunatamente
si, anche se ovvia-mente il prezzo da pagare è una
leggera complicazione sintattica:
Vector<Vector<String>>
v = new Vector<Vector<String>>();
La
precedente istruzione crea un Vector in grado di accettare
solamente Vector di String, in modo da costituire un vettore
dinamico rettangolare. Si noti l'eleganza con cui è
ora possibile iterare tra gli elementi di questa particolare
struttura dati:
Iterator<Vector<String>>
i1 = v.iterator();
while(i1.hasNext()) {
Iterator<String> i2 = i1.next().iterator();
while(i2.hasNext())
System.out.println(i2.next());
La
versione senza Generics richiede così tante operazioni
di casting da far passare la voglia a chiunque di utilizzarla,
anche se in una forma compatta come la seguente:
Iterator
i1 = v.iterator();
while(i1.hasNext()) {
Iterator i2 = ((Vector)((Iterator)i1).next()).iterator();
while(i2.hasNext())
System.out.println((String)i2.next());
}
Non
esiste limite al livello di nidificazione raggiungibile: è
possibile creare Collection a 3,4 o più dimensioni,
sempre che la cosa sia realmente utile. Vediamo un esempio
di Vector a 3 dimensioni:
Vector<Vector<Vector<String>>>
v = new Vector<Vector<Vector<String>>>();
Esistono
comunque numerose combinazioni di reale utilità, come
la seguente mappa hash che usa stringhe come chiavi e Vector
di String come valori:
HashMap<String,Vector<String>>
persone = new HashMap<String,Vector<String>>();
In
una simile struttura è possible associare a dei nomi
una lista di attributi:
Vector<String>
attributi = new Vector<String>();
attributi.add("Bello");
attributi.add("Bravo");
attributi.add("Intelligente");
attributi.add("Tenace");
attributi.add("Arguto");
attributi.add("Modesto");
persone.put("Andrea
Gini",attributi);
Le
possibilità dei Generics non si esauriscono qui, come
vedremo nel prossimo articolo. Tut-tavia, già ora è
possibile vedere che la fantasia è l'unico vero limite
alle enormi possibilità of-ferte da questo nuovo costrutto.
Conclusioni
In questo articolo abbiamo scoperto cosa sono i Generics,
il nuovo rivoluzionario costrutto inserito in Java 5. Dopo
una breve introduzione teorica abbiamo visto, attraverso esempi
di difficoltà crescente, come sia possibile utilizzarli
nella programmazione di tutti i giorni, in particolare in
abbinamento al framework Collection, che maggiormente ne fa
uso. Nel pros-simo articolo impareremo i costrutti più
avanzati, ma soprattutto vedremo come creare classi che fanno
uso di tipi generici.
|