Introduzione ai Generics di Java
Perché esistono i generics in Java: type safety, riuso del codice ed eliminazione dei cast nelle collezioni e nelle API.
I Generics sono la funzionalità che permette a una classe, un'interfaccia o un metodo di operare su un tipo non specificato, lasciando poi al compilatore il compito di determinare quel tipo nel punto in cui lo si utilizza. Una List<String> è una lista di stringhe: il compilatore lo sa, e qualsiasi tentativo di inserire una Date viene respinto prima ancora che il programma venga eseguito. Prima che i generics arrivassero in Java 5, la stessa lista era una List di Object, e ogni lettura richiedeva un cast scritto a mano che poteva o meno avere successo a runtime. I generics hanno trasformato quella scommessa a runtime in un controllo a tempo di compilazione, e quasi tutte le API Java moderne ne sono plasmate.
Il problema che i generics risolvono
Per capire perché esistono i generics, immagina un Java senza di essi. Un contenitore che può ospitare qualsiasi cosa deve dichiarare il proprio contenuto come Object:
// Pre-Java-5 style — what the standard library actually looked like.
List names = new ArrayList();
names.add("Ada");
names.add("Linus");
String first = (String) names.get(0); // cast required, never checked by the compilerDue problemi. Primo, il cast è rumore — ogni lettura dal contenitore ne richiede uno. Secondo, e peggio ancora, nulla impedisce a qualcuno di inserire una Date nella stessa lista:
names.add(new java.util.Date()); // compiler is fine with this
String oops = (String) names.get(2); // ClassCastException at runtimeIl bug si manifesta al momento della lettura, lontano dalla scrittura. Il cast mente — dice "questo è uno String," e la JVM lo scopre solo quando è troppo tardi per fornire uno stack frame utile vicino al punto del vero errore.
I generics risolvono entrambi i problemi:
List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Linus");
names.add(new Date()); // ❌ compile error — won't even build
String first = names.get(0); // no cast — the compiler already knows it's a StringIl <String> tra parentesi angolari è il parametro di tipo. Dice al compilatore "questa lista contiene String," e da quel momento in poi ogni add e get viene verificato rispetto a tale promessa.
Tre vantaggi gratuiti
I generics ti offrono tre benefici concreti, ed è per questo che ogni collezione, stream e optional nel moderno JDK è generico:
- Controlli più rigorosi a tempo di compilazione. L'inserimento del tipo sbagliato mostrato sopra viene rilevato durante la build, non in produzione. Una categoria di
ClassCastExceptionsmette semplicemente di verificarsi. - Niente più cast. Leggere da una
Map<String, User>ti restituisce unUser, non unObjectda castare. Meno rumore sintattico, meno codice da leggere, meno da mantenere. - Riuso del codice senza copia-incolla. Una sola classe
List<E>funziona per ogni tipo di elemento. Prima dei generics, la libreria standard accettavaObjectovunque oppure fornivaStringList,IntList,DateListe così via. Ora scrivi una sola classe e lasci che il chiamante la parametrizzi.
Quest'ultimo punto è il vantaggio architetturale più grande. I generics sono il modo in cui scrivi un contenitore, un algoritmo o una forma di callback una volta sola e lo applichi a ogni tipo che il chiamante potrebbe passare.
Una prima classe generica
La convenzione vuole che un parametro di tipo sia nominato con una singola lettera maiuscola — T per un "tipo" generico, E per "elemento" di una collezione, K/V per "chiave" e "valore" di una mappa, R per "ritorno". Ecco la classe generica più semplice possibile — una coppia di due elementi dello stesso tipo:
public class Pair<T> {
private final T first;
private final T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T first() { return first; }
public T second() { return second; }
}Il <T> dopo il nome della classe introduce il parametro di tipo. Da lì in poi, T può essere usato all'interno della classe ovunque possa andare un tipo normale. Il chiamante sceglie T quando crea l'oggetto:
Pair<String> names = new Pair<>("Ada", "Grace");
Pair<Integer> scores = new Pair<>(100, 87);
String n1 = names.first(); // already a String, no cast
int s1 = scores.first(); // auto-unboxed from IntegerIl <> vuoto a destra (l'operatore diamante, Java 7+) indica al compilatore di inferire il tipo dalla dichiarazione a sinistra — non è quasi mai necessario ripetere l'argomento di tipo.
Cosa viene parametrizzato e cosa no
Un parametro di tipo può sostituire:
- Il tipo di un campo (
private T value;) - Un parametro o il tipo di ritorno di un metodo (
public T get() { ... },void put(T value)) - Il tipo dell'elemento di un array di quel tipo (
T[] items— con alcune limitazioni)
Un parametro di tipo non può sostituire:
- Un primitivo (
Pair<int>è illegale — usaPair<Integer>e lascia fare l'autoboxing) - Il parametro di tipo di un campo o metodo statico (il parametro appartiene all'istanza, non alla classe stessa)
- Il target di
new T()oinstanceof T— Java cancella i generics a runtime, quindi il programma non ha alcunTda costruire o su cui fare un test
L'elenco completo delle "cose che non puoi fare" ha un capitolo dedicato alla fine di questa parte — Java Generics Restrictions — una volta che avremo trattato abbastanza meccanismi da rendere le regole comprensibili.
Un esempio pratico: type safety vs. tipi raw, a confronto
Il programma seguente costruisce lo stesso contenitore due volte — una come List raw (la forma pre-generics) e una come List<String>. Entrambi compilano; solo quello parametrizzato è sicuro.
La versione raw si interrompe a metà iterazione perché il ciclo si affidava a un cast che non aveva motivo di fare. La versione generica ha reso irrapresentabile lo stesso errore — la add(42) sbagliata non compila affatto. Questo spostamento dal runtime al tempo di compilazione è l'unica ragione per cui i generics esistono.
Cosa tratta questa parte del libro
I capitoli rimanenti in questa parte analizzano i generics un pezzo alla volta:
- Classi generiche — il parametro di tipo a livello di classe appena visto, in maggiore profondità.
- Metodi generici — metodi che introducono il proprio parametro di tipo, indipendentemente dalla classe.
- Interfacce generiche — progettare contratti API parametrizzati su un tipo.
- Parametri di tipo delimitati — dire "T deve estendere
Number" in modo da poter chiamare metodi suT. - Wildcard —
? extends T,? super T, e la regola PECS che decide quando usare ciascuna. - Type erasure — come la JVM implementa i generics internamente, e perché alcune cose che ci si aspetterebbe funzionino non funzionano.
- Restrizioni — il catalogo delle cose che il linguaggio non permette di fare, con le ragioni dietro a ciascuna.
Leggili in ordine — ogni capitolo assume quelli precedenti.
Cosa c'è dopo
Inizia con la forma più comune — una classe i cui campi e metodi sono parametrizzati su un tipo scelto dal chiamante. Continua con Java Generic Classes.