MokaByte 91- Dicembre 2004 
Il JDK 1.5 ed i generics
I parte: usare i generics
di
Andrea Gini

Il JSDK 1.5, o Java 5, come è stato ribattezzato a causa delle numerose innovazioni, è finalmen-te arrivato alla prima release ufficiale. La novità più dirompente tra quelle presentate sono i Generics, un'implementazione del polimorfismo parametrico, già presente in altri linguaggi O-bject Oriented come Eiffel e C++. A differenza delle assert, introdotte in sordina con Java 1.4 e ignorate dai più, i Generics irrompono nella vita del programmatore come un uragano: è suffi-ciente sfogliare distrattamente la nuova Javadoc per trovare definizioni piene dei nuovi caratte-ristici identificatori tra parentesi angolari: <E>, <K> e così via. Non c'è dunque alternativa: che lo si voglia o meno, è giunto il momento di imparare ad usare i Generics.
Come in passato per altri argomenti, l'utilizzo client dei Generics è molto più semplice di quan-to non sia la creazione di codice parametrico: pertanto questa rassegna si comporrà di due artico-li: Usare i Generics e Programmare con i Generics. Rompiamo dunque gli indugi e diamo il via alle prime esperienze di uso dei Generics.


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.


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it