1. Il problema delle collezioni «raw» (raw types)
Un po’ di storia. Fino a Java 5 tutte le collezioni erano «onnivore». Memorizzavano oggetti di tipo Object e il compilatore non controllava cosa ci stavi mettendo dentro. Vuoi inserire una stringa? Prego. Un numero? Perché no. Un gatto? Si può anche quello.
// Esempio di collezioni "raw" (raw types), Java prima della versione 5
List list = new ArrayList();
list.add("Ciao");
list.add(42);
list.add(new Object());
Il problema emergeva quando «tiravi fuori» e usavi il valore:
String s = (String) list.get(0); // OK, è una stringa
String s2 = (String) list.get(1); // BOOM! ClassCastException
Il compilatore tace, ma a runtime ottieni una ClassCastException. È come una scatola con l’etichetta «mele» in cui ci sono una tazza, una banana e un riccio.
Perché è un problema?
- Gli errori emergono solo a runtime.
- I tipi si confondono: devi effettuare manualmente i cast agli oggetti.
- Il codice è meno leggibile e più rischioso.
La soluzione — generics (tipi generici)
I generics sono un meccanismo che consente di creare classi, interfacce e metodi con parametri di tipo. In pratica dici alla collezione: «Conserva solo stringhe», e il compilatore lo fa rispettare rigorosamente.
List<String> words = new ArrayList<>();
words.add("Ciao");
words.add("Mondo");
// words.add(42); // Errore di compilazione! Non si può aggiungere un int a List<String>
Ora il compilatore non permetterà di inserire nella lista nulla che non sia una stringa. L’errore viene intercettato prima dell’esecuzione del programma.
Idea chiave dei generics:
Garantire la sicurezza di tipo delle collezioni (e non solo), così da intercettare gli errori in fase di compilazione e non a runtime.
2. Sintassi dei generics: come appare nel codice
Specificare il tipo tra parentesi angolari
Quando crei una collezione, specifica il tipo degli elementi tra <>:
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(123); // Errore: non si può aggiungere un numero in una lista di stringhe
String first = names.get(0); // Nessun cast necessario!
Classici:
- List<String> — elenco di stringhe
- List<Integer> — elenco di interi
- Set<Double> — insieme di numeri in virgola mobile
- Map<String, Integer> — chiave String, valore Integer
Perché non scrivere semplicemente List?
Si può, ma perdi tutti i vantaggi dei generics, e il compilatore ti avviserà:
List list = new ArrayList(); // raw type — sconsigliato!
list.add("Hello");
list.add(7.5);
String s = (String) list.get(1); // Ciao, ClassCastException!
Il codice Java moderno usa sempre i generics.
Operatore diamond <>
Da Java 7 non è necessario indicare il tipo a destra, se è chiaro dal contesto:
List<String> list = new ArrayList<>(); // Il compilatore capirà da solo che qui è <String>
3. Generics per diverse collezioni
Esempi per List, Set, Map
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("Alice");
uniqueNames.add("Bob");
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 23);
ages.put("Bob", 31);
Esempio con una classe propria
class Student {
String name;
int age;
// ...
}
List<Student> students = new ArrayList<>();
students.add(new Student());
4. Sfumature utili
Vantaggi dei generics
Sicurezza di tipo. Il compilatore garantisce che nella collezione entrino solo elementi del tipo previsto.
Niente cast. Prima: String s = (String) list.get(0);. Ora: String s = list.get(0);.
Codice più leggibile e affidabile. Meno sorprese a runtime.
Limitazioni dei generics
Non puoi usare i tipi primitivi. I generics funzionano solo con oggetti, non con primitivi (int, double, boolean). Usa le classi wrapper: Integer, Double, Boolean.
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // int viene automaticamente convertito in Integer (autoboxing)
Brevemente sull’erasure dei tipi (type erasure)
In Java i generics sono implementati tramite il meccanismo di type erasure: dopo la compilazione l’informazione sui parametri di tipo viene eliminata, e a runtime la JVM non sa che si trattava di un List<String> e non semplicemente di un List. Questo è stato fatto per mantenere la retrocompatibilità.
Conseguenza: non si può verificare un parametro di tipo con instanceof usando un argomento di tipo concreto.
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { ... } // Errore di compilazione!
Tentare di aggiungere un elemento di altro tipo — errore di compilazione
List<String> words = new ArrayList<>();
words.add("Hello");
// words.add(123); // Errore di compilazione: incompatible types: int cannot be converted to String
Map<String, Integer> map = new HashMap<>();
map.put("Gatto", 5);
// map.put(3, "Elefante"); // Errore: la chiave deve essere String, il valore — Integer
Ed è fantastico: gli errori vengono intercettati in fase di compilazione.
Non solo collezioni
I generics possono essere usati in classi e metodi propri. Ad esempio, una «Scatola» universale:
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Ciao");
System.out.println(stringBox.get());
Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());
Nelle collezioni i generics sono lo standard, ma li incontrerai anche altrove, ad esempio nello Stream API e in Optional.
5. Errori tipici nell’uso dei generics
Errore n. 1: uso di collezioni «raw». Una dichiarazione come List list = new ArrayList(); ti fa perdere la sicurezza di tipo. Indica sempre i parametri di tipo, ad esempio List<String>.
Errore n. 2: tentare di usare i primitivi. Non puoi scrivere List<int>, usa List<Integer>.
Errore n. 3: cast manuale quando leggi dalla collezione. Se usi i generics, il cast del tipo (String) list.get(i) non serve. Se ti tocca farlo, hai violato i tipi da qualche parte.
Errore n. 4: pensare che i parametri di tipo siano disponibili a runtime. A causa del type erasure non puoi verificarli con instanceof alla maniera di List<String>.
Errore n. 5: mescolare tipi diversi nella stessa collezione. Se è dichiarato List<String>, non provare ad aggiungere un Integer — il compilatore non lo permetterà, ed è un bene.
GO TO FULL VERSION