Java Generics

di  Antonio Coschignano, marted́ 19 gennaio 2010
Con il rilascio del JDK 1.5 la Sun ha introdotto insieme a tante altre nuove funzionalità i cosiddetti Generics che aggiungono al linguaggio java la possibilità di 'parametrizzare' i tipi di dati gestiti ad esempio nei contenitori (Collection, Map, Set etc..). Questo approccio fornisce un meccanismo per comunicare al compilatore il tipo di dato che viene gestito in un contenitore, in modo che possa essere controllato in fase di compilazione. Una volta che il compilatore conosce il tipo dell'elemento del contenitore, esso può verificare se esso viene utilizzato in modo coerente. Tutto ciò comporta una drastica riduzione dell'uso dei casting espliciti ad opera dell'utente e garantiscono a tempo di compilazione una garanzia della verifica sui vincoli dei tipi.

Esempi di sintassi

Vediamo adesso un semplice esempio utilizzando i Generics per una LinkedList di tipo String:
LinkedList <String> list = new LinkedList<String>();
Questo codice non fa altro che comunicare al compilatore che la LinkedList accetterà al suo interno solo ed esclusivamente oggetti di tipo String. Infatti se leggiamo un elemento dalla lista non sarà più necesserio effettuare il casting:
String str = list.get(0);
Allo stesso modo l'inserimento è limitato solo ed esclusivamente all'oggetto di cui abbiamo espresso il vincolo nella dichiarazione uilizzando i Generics che in questo caso corrisponde al tipo String. Qualsiasi tentativo di inserimento o lettura di un tipo che non rispetta il vincolo genererà una ClassCastException durante la compilazione. Un altro caso molto importante è un po diverso riguarda l'utilizzo delle mappe dove è necessario definire due tipi, cioè la chiave ed il valore. Ad esempio implementando una mappa che ha come chiave un intero e come valore una stringa procediamo in questo modo:
Map <Integer, String> map = new HashMap<Integer, String>();
Infatti se diamo un'occhiata all'interfaccia Map essa definisce due tipi generici:
public interface Map<K, V> {
  ...
  public void put(K key, V value);
  public V get(K key);
  public Collection<V> values();

  ...
}
K e V rappresentano rispettivamente il tipo generico della chiave e quello del valore. E' chiaro quindi che già dalle dichiarazioni come ad esempio nel metodo get(...) si definiscono i rapporti tra i tipi che vengono usati. Infatti questo metodo riceve un generico <K> associato al tipo della chiave e ritorna il generico <V> associato al valore.

Controllo sui tipi

Per capire meglio la differenza del controllo sui tipi a runtime e compile time, vediamo un medesimo esempio con/senza Java Generics, implementando una Collection dove inseriamo delle stringhe e cercheremo di estrarre volutamente degli interi:
Collection c = new LinkedList();
c.add("Antonio");
c.add("Giovanni");
c.add("Giuseppe");
Iterator it = c.iterator();
while(it.hasNext()) {
	Integer n = (Integer)it.next();//Errore in fase di esecuzione
}
In questo caso ci verrà generata una eccezione poichè il tipo che abbiamo inserito non è lo stesso per cui abbiamo effettuato il casting, infatti abbiamo tentato di estrarre un intero da una lista di stringhe. Quindi il risultato in fase di esecuzione sarà un eccezione del tipo:
Exception in thread "main" java.lang.ClassCastException:
java.lang.String cannot be cast to java.lang.Integer
Mentre se utilizziamo i Java Generics l'errore ci verrà segnalato in fase di compilazione:
Collection <String> c = new LinkedList<String>();
c.add("Antonio");
c.add("Giovanni");
c.add("Giuseppe");
Iterator <String> it = c.iterator();
while(it.hasNext()) {
	Integer n = it.next();//Errore in fase di compilazione
}

Classi e metodi con i Generics

Come abbiamo accennato sopra con l'interfaccia Map, quindi tutte le classi o metodi statici che permettono di utilizzare i Java Generics devono avere una propria struttura sintattica che permette questo tipo di approccio. Se dobbiamo definire un tipo generico che viene manipolato in una classe bisogna inserirlo accanto alla dicharazione di classe. Vediamo un esempio di implementazione di una Pila che gestisce un generico elemento <T> al suo interno. Questo è il codice della classe:
class Pila<T> {

  T [] array;
  private int cursor = 0;

  public Pila(T[] array) {
    this.array = array;
  }

  public void put(T element) {
    if (cursor == array.length) throw new ArrayIndexOutOfBoundsException();
    array[cursor] = element;
    cursor++;
  }

  public T pop() {
    if (isEmpty()) throw new NegativeArraySizeException();
    T element = array[cursor];
    cursor--;
    return element;
  }

  public boolean isEmpty() {
    return cursor == 0;
  }
}
Un esempio con il metodo main che utilizza la pila:
public static void main(String [] argv) {
  LinkedList<Integer> list = new LinkedList<Integer>();
  Integer [] array = {1,2,2,6,7,8,6,4,3,9};
  Pila <Integer> p = new Pila<Integer>(array);
  p.put(5);
  ....
  int n = p.pop();
}
Adesso un esempio dove implementiamo invece un metodo statico che gestisce un tipo generico al suo interno. Questo metodo semplicemente aggiunge ad una Collection un array di oggetti riferiti al generico <T>:
public class SampleGenerics {
  public static <T> void addArrayToCollection(T [] array, Collection<T> c) {
    for(T t:array)
      c.add(t);
  }
}
Questo è un tipico esempio dell'utilizzo dei Generics a livello di metodo, infatti notate che è stato inserita subito dopo la keyword static la sintassi <T>. Questo indica che nel metodo viene utilizzato un tipo generico T che compare anche negli argomenti del metodo. Ecco un metodo main di esempio:
public static void main(String [] argv) {
  LinkedList<Integer> list = new LinkedList<Integer>();
  Integer [] array = {1,2,2,6,7,8,6,4,3,9};
  SampleGenerics.addArrayToCollection(array, list);
  System.out.println(list);
}
Ci sono tante altre caratteristiche dei Generics un po più complesse, come ad esempio i Wildcards con la quale si possono definire intere famiglie di tipi. Per un maggiore approfondimento consiglio di consultare il Java Generics Tutorial della Sun in PDF.

I Java Generics sono quindi dei costrutti sintattici che non danno maggiore efficienza al linguaggio ma bensi un maggiore potere espressivo. Ciò dipende dal fatto che questa 'parametrizzazione' è effettuata solo a livello di codice e quindi il bytecode rimane invariato. Di conseguenza permangono i problemi riguardanti l'efficienza legata al cast che rimane comunque a tempo di esecuzione. Per quanto riguarda la compatibilità è possibile optare sia per l'utilizzo dei Generics che continuare nella vecchia maniera. L'unico vincolo è limitato all'utilizzo dei Generics solo ed esclusivamente per versioni della virtual machine 1.5 o succesive.
Altri link che potrebbero interessarti
  • » Una lista concatenata in java
  • » Tutorial JFileChooser in Java Swing
  • » La gestione delle Stringhe in java
  • » La gestione degli eventi in java Swing
  • » La classe Collections
  • » Gestire la System Tray in Java
  • » Gestire la clipboard in Java Swing
  • » Connessioni HTTP in Java