Lavorare con i generics

I generics, introdotti dalla versione 5 di Java, aiutano a scrivere codice più sicuro, evitando rischi di invocazione errata o utilizzo improprio di collection. Vediamo un esempio di codice pericoloso che utilizza le collection senza l’uso dei generics:

static void stampa(List lista) {
  for (int i = 0 ; i < lista.size() ; i++) {
    String nome = (String)lista.get(i);
    System.out.println(nome);
  }
}
public static void main(String... a) {
  List nomi = new ArrayList<>();
  nomi.add(new StringBuilder("carlo"));
  stampa(nomi);
}

Questo codice compila perfettamente, ma in esecuzione otterremmo un bel ClassCastException !

Questo perchè il metodo stampa prende in input una lista senza generics, di fatto permette una lista contenente qualsiasi tipo di oggetto, ma al suo interno esegue un cast a String supponendo che la lista fornita contenga String, ma nel nostro metodo main lo abbiamo invocato passandogli una lista contenente StringBuilder.

I generics ci permettono di evitare questo tipo di errori a runtime e di darci eventualmente un errore di compilazione, ad esempio :

List<String> nomi = new ArrayList<>();
nomi.add(new StringBuilder("carlo")); //ERRORE DI COMPILAZIONE

Generics nelle classi

Notiamo che ArrayList permette la dichiarazione mediante generics, vediamo come possiamo definire una nostra classe per accettare un tipo generico:

public class Hangar<T> {
  private T contenuto;
  public T svuotaHangar() {
    return contenuto;
  }
  public void mettiNellHangar(T contenuto) {
    this.contenuto = contenuto;
  }
}

Stiamo dicendo al nostro compilatore che T sarà un qualsiasi tipo di classe definito in fase di creazione dell’istanza della classe Hangar, possiamo ad esempio utilizzarla per mettere in un hangar un aereo :

Hangar<Aereo> h = new Hangar<>();
h.mettiNellHangar(new Boeing747());
System.out.println(h.svuotaHangar());

Ma potremmo mettere nell’hangar anche una Jeep per esempio :

Hangar<Automobile> = new Hangar<>();
h.mettiNellHangar(new Jeep());

Di fatto potremmo inserire nell’hangar qualsiasi cosa, da un aereo a una persona ad una chiave inglese e via discorrendo, la nostra classe potrà lavorare con ogni tipo di oggetto.

Non siamo limitati ad 1 solo parametro generico, possiamo anche definirne più di uno :

public class Hangar<T, U> {
  private T contenuto;
  private U peso;
  public Hangar(T contenuto, U peso) {
    this.contenuto = contenuto;
    this.peso = peso;
  }
}

Ora possiamo utilizzare la nostra classe in questo modo :

Aereo md80 = new Md80();
Integer peso = 1_500_000;
Hangar<Aereo, Integer> hangar = new Hangar<>(md80, peso);

In compilazione, Java sostituisce i tipi generici specificati con i tipi reali, di fatto traduce tutto in Object e inserisce del cast espliciti.

Interfacce generiche

Cosi come una classe, anche un interfaccia può specificare un tipo generico, vediamo un esempio per un interfaccia che si occupa di spedizioni :

public inteface Shippable<T> {
  void spedisci(T t);
}

Ci sono 3 modi per utilizzare questa interfaccia, il primo è specificare il tipo generico nella classe, vediamo in questo esempio come la classe concreta specifica che lavorerà con oggetti di tipo Lettere :

class SpedisciLettere implements Shippable<Lettera> {
  public void spedisci(Lettera lettera) {}
}

Possiamo però non specificare il tipo concreto, possiamo lasciare l’implementazione della classe concreta con un tipo generico :

class SpedisciQualcosa<T> implements Shippable<T> {
  public void spedisci(T t) {}
}

Il terzo ed ultimo modo, è di non utilizzare i generics, buono solo per codice vecchio :

class SpedisciVecchioStile implements Shippable {
  public void spedisci(Object o) {} 
}

Metodi generici

Oltre a classi e interfacce, anche i metodi possono specificare tipi generici, normalmente sono utilizzati in metodi statici ma non per forza, vediamo un esempio :

public static <T> Hangar<T> impacchetta(T t) {
  System.out.println("preparo : "+t);
  return new Hangar<T>();

Qui specifichiamo che il nostro metodo restituirà un Hangar<T> dobbiamo specificare il tipo di ritorno subito prima dell’oggetto ritornato dal metodo, e si specifica tramite <T>

public static <T> T metodo1(T t) {} // OK
public static <T> void metodo2(T t) {} //OK
public static T noGood (T t) {} //ERRORE, manca <T>

Per invocare questo metodo, bisogna utilizzare la seguente sintassi :

Box.<String>impacchetta("boeing");
Box.<String[]>impacchetta({"boeing","md80"});

Interagire con codice vecchio

Per codice vecchio si intende quel codice scritto con versione Java 1.4 o precedente, dove le collection non hanno i generics, ricordiamo che l’utilizzo dei generics di da abbastanza sicurezza di scrivere codice congruente in fase di compilazione, ma l’utilizzo di routine scritte senza generics, potrebbe farci incappare in errori inaspettati, come questo esempio :

class Boeing {}
class McDonnelDouglas {}
public class LegacyHangar {
  public static void main(String[] args) {
    List aerei = new ArrayList();
    aerei.add(new McDonnelDouglas());
    stampaAerei(aerei);
  }
  static void stampaAerei(List<Boeing> aerei) {
    for (Boeing b : aerei) //ClassCastException
      System.out.println(b);
  }
}

Questo codice compila correttamente, ma in fase di run avremmo un class cast exception, il fatto è che tutto il codice non utilizza i generics, il compilatore non potrà sapere se la collection passata al metodo conterrà o no gli oggetti corretti.

Vediamo un altro esempio :

public class LegacyHangar {
  public static void main(String[] args) {
    List<Boeing> aerei = new ArrayList<>();
    aggiungiAereo(aerei);
    Boeing b = aerei.get(0); //ClassCastException
  }
  static void aggiungiAereo(List aerei) {
    aerei.add(new McDonnelDouglas());
  }
}

Vediamo che il metodo main utilizza correttamente i generics, ma il metodo aggiungiAereo no, di fatto potremo passare una collection con generics ma il codice vecchio potrebbe inserire oggetti non congruenti poichè non utilizza i generics.

Limiti

fino ad ora abbiamo visto come i generics possano accettare qualsiasi tipo di classe, vediamo come possiamo fare se vogliamo restringere quali tipi di dato possano essere passati come tipo generico.

Un bounded parameter type specifica i limiti per un tipo generico,

Un wildcard parameter type è un tipo generico sconosciuto, rappresentato da ?

Unbunded wildcart ? List<?> l = new ArrayList<String>()
Wildcard con limite superiore ? extends type List<? extends Exception> l = new ArrayList<RuntimeException>()
Wildcard con limite inferiore ? super type List<? super Exception> l = new ArrayList<Object>()

 

Unbunded wildcard

Rappresenta un qualunque tipo di dato, puoi specificare con ? che per te qualsiasi tipo di dati è corretto per te, vediamo un esempio di un metodo che accetta una lista di qualsiasi oggetto :

public static void printList(List<Object> list) {
  for (Object x : list) System.out.println(x);
}
public static void main(String... a) {
  List<String> k = new ArrayList<>();
  k.add("java");
  printList(k); // NON COMPILA
}

Sembrava ok vero ? il metodo printList vuole in input una lista di Object, noi gli abbiamo passato una List<String> perchè il compilatore si arrabbia ? semplice, List<String> non può essere assegnato a List<Object> per Java ! Immagina cosa succederebbe se potessi fare questa cosa :

List<Integer> numeri = new ArrayList<>();
numeri.add(new Integer(20));
List<Object> o = numeri; //NON COMPILA
o.add("una bella stringa");
System.out.println(o.get(1));

Fortunatamente non compila ! se fosse permesso, avremmo infranto la promessa di avere una lista di Integer, aggiungendovi di fatto una String !!

Tornando al nostro esempio, come possiamo rendere il metodo printList per accettare una qualsiasi list ?

public static void printList(List<?> list) {
  for (Object x : list) System.out.println(x);

}

Ora compila correttamente !

Limite Superiore

Immaginiamo di volere un metodo che accetti in ingresso una lista di qualsiasi numero, sappiamo che i generics non possono essere usati con le sottoclassi, e se scriviamo una cosa del genere non compilerebbe :

List<Number> a = new ArrayList<Integer>();

Possiamo risolvere dicendo a Java che vogliamo una lista che può contenere tutti i tipi che sono sottoclassi di Number :

List<? extends Number> a = new ArrayList<Integer>();

Questo è ok perchè Integer è sottoclasse di Number, vediamo di utilizzarlo in un metodo :

public static long totale(List<? extends Number> lista) {
  long t = 0;
  for (Number n : lista) 
    t += n.longValue();
  return t;
}

Ricordiamo che mediante gli bound le liste sono immutabili ! una cosa del genere andrebbe in errore : 

static class Boeing extends Aereo {}
static class Aereo {}
public static void main(String... a) {
  List<? extends Aereo> aerei = new ArrayList<Aereo>();
  aerei.add(new Boeing());  //NON COMPILA
  aerei.add(new Aereo());   //NON COMPILA
}

Non compila perchè Java non sà qual’è il vero dipo di List<? extends Aereo> potrebbe essere List<Aereo> o List<Boeing>

Limiti inferiori

Proviamo a scrivere un metodo che aggiunga una stringa a due liste :

List<String> stringhe = new ArrayList<>();
stringhe.add("ciao");
List<Object> oggetti = new ArrayList<Object>(stringhe);
addStringa(stringhe);
addStringa(oggetti);

Dobbiamo scrivere un metodo che accetti in input List<String> e List<Object>, potremmo essere portati a specificare il parametro come List<?> ma non possiamo, perchè noi vogliamo aggiungere delle stringhe e le liste con wildcard sono immutabili

per farlo, possiamo utilizzare il limite inferiore :

public static void addStringa(List<? super String> lista { }

Stiamo dicendo a Java che questo metodo accettera una lista di stringhe o una lista di oggetti che sono superclasse di String

Object è una superclasse di String quindi ammessa.

ATTENZIONE !

Quando si ha a che fare con limite upper e lower, la cosa può farsi complessa, guardiamo questo esempio :

List<? super IOException> exception = new ArrayList<Exception>();
exception.add(new Exception()); //NON COMPILA
exception.add(new IOException());
exception.add(new FileNotFoundException());

Strano vero ? la dichiarazione permette di avere List<IOException> o List<Exception> o List<Object>, ma la prima add non compila perchè potremmo avere una List<IOException> e Exception non può starci !

Ricapitoliamo

Ora abbiamo visto tutti i concetti base sui generics, vediamo come si può scrivere un codice molto confuso, che tanto piace a chi scrive i test di certificazione :

Vediamo un esempio bastardo :

class A {}
class B extends A {}
class C extends B {}

Sai riconoscere quali di queste istruzioni compila e quale no ?

List<?> list1 = new ArrayList<A>();
List<? extends A> list2 = new ArrayList<A>();
List<? super A> list3 = new ArrayList<A>();
List<? extends B> list4 = new ArrayList<A>();
List<? super B> list5 = new ArrayList<A>();
list<?> list6 = new ArrayList<? extends A>();
  • list1 può contenere istanze di A, è un unbounded wildcard, quindi è OK
  • list2 è un upper-bound, può contenere list di classi che estendono A, quindi List<A>, List<B> e List<C>
  • list3 è corretta, accetta classi che sono superclasse di A
  • list4 non è corretta, ? extends B vuole ArrayList<B> o ArrayList<C>, noi abbiamo specificato ArrayList<A>
  • list5 è corretta, ArrayList<A> , A è superclasse di B, quindi OK
  • list6 non è corretta, non possiamo specificare wildcard nella dichiarazione del tipo quando crei l’istanza di ArrayList

Ora vediamo un metodo :

<T> T method1(List<? extends T> list) {
  return list.get(0);
}

Questo metodo è corretto, prende in input una list di T o una lista di oggetti che sono sottoclassi di T e ritorna un singolo oggetto T, per esempio lo puoi invocare passando un List<String> e ti torna un oggetto String, oppure, passandogli un List<Number> e ti restituisce un Number

e se scrivessimo :

<T> <? extends T> method2(List<? extends T> list) {
  return list.get(0);
}

questo non compila ! perchè il tipo di ritorno non è un tipo di ritorno valido ! e non si possono specificare wildcard nei parametri di ritorno !

Vediamone un altro :

<B extends A> B method3(List<B> list) {
  return new B();
}

non compila, con <B extends A>  stai dicendo che vuoi usare B come parametro solo per questo metodo e che B deve estendere A, ma attenzione, B è anche il nome della classe.