Perché esistono i generics
Un tipo generico ti permette di scrivere il codice una volta e riutilizzarlo per molti tipi senza rinunciare alla sicurezza sui tipi. Invece di scrivere un IntBox, uno StringBox e uno UserBox separati, scrivi un solo Box<T>, dove T è un segnaposto che chi usa la classe riempie.
Hai già usato i generics ogni volta che hai scritto ArrayList<String> o una HashMap come HashMap<String, Integer>. La parte <...> è un argomento di tipo. Questa pagina mostra come scrivere i tuoi.
L'alternativa, memorizzare tutto come Object, butta via le informazioni sul tipo e ti costringe a cast brutti e soggetti a errori:
Quel cast nell'ultima riga lancia una ClassCastException a runtime: proprio il tipo di eccezione che i generics sono pensati per rendere impossibile.
Una classe generica
Dichiara un parametro di tipo tra parentesi angolari dopo il nome della classe. Per convenzione è una sola lettera maiuscola: T per "type", E per "element", K/V per chiave/valore.
Dentro Box, ogni T diventa il tipo fornito da chi usa la classe. Box<String> è una scatola che contiene e restituisce solo String. Il compilatore rifiuta name.set(99) prima ancora che il programma venga eseguito.
Il <> vuoto a destra (il diamond operator) permette al compilatore di dedurre l'argomento di tipo dal lato sinistro, così non ripeti <String> due volte.
Metodi generici
Un singolo metodo può avere un proprio parametro di tipo, indipendente dalla classe. Metti il parametro <T> prima del tipo di ritorno:
Non passi mai T in modo esplicito: il compilatore lo deduce dall'argomento. I metodi generici sono il motivo per cui utility come Collections.sort o List.of restano type-safe con qualsiasi tipo di elemento.
Parametri di tipo vincolati
A volte un tipo generico ha senso solo per alcuni tipi. extends vincola il parametro, così puoi chiamare i metodi del limite. Qui T extends Number significa che T è Number o una sua sottoclasse (Integer, Double, ...), quindi doubleValue() è disponibile:
Nota che qui extends significa "è un sottotipo di", e funziona sia con le classi sia con le interfacce: <T extends Comparable<T>> è molto comune quando devi confrontare degli elementi.
Wildcard: ? extends e ? super
Una trappola sottile: List<Integer> non è una List<Number>, anche se Integer è un Number. I generics sono invarianti. Le wildcard allentano questo vincolo quando devi solo leggere o solo scrivere.
Usa ? extends T per un produttore da cui leggi, e ? super T per un consumatore in cui scrivi (la regola "PECS": Producer Extends, Consumer Super):
Una lista ? extends Number ti permette di leggere gli elementi come Number ma non di aggiungerne (il compilatore non può sapere il tipo esatto degli elementi). Una lista ? super Integer ti permette di aggiungere Integer, ma le letture restituiscono Object. Scegli la wildcard in base a come scorrono i dati.
La type erasure e i suoi limiti
I generics sono una funzionalità della fase di compilazione. Dopo la compilazione il parametro di tipo viene cancellato: a runtime Box<String> e Box<Integer> sono entrambi solo Box. Questo mantiene i generics compatibili con il codice vecchio, ma impone limiti concreti.
// Nessuno di questi compila: il parametro di tipo non esiste a runtime:
T value = new T(); // non si può istanziare un parametro di tipo
T[] array = new T[10]; // non si può creare un array generico
if (list instanceof List<String>) { } // non si può verificare l'argomento di tipo
Dato che il tipo sparisce a runtime, non puoi chiedere "cos'era T?" tramite reflection, e non puoi fare l'overloading di metodi che differiscono solo per l'argomento generico (foo(List<String>) e foo(List<Integer>) diventano la stessa firma dopo la cancellazione). Quando ti serve davvero il tipo a runtime, passa un token Class<T> come parametro del costruttore o del metodo.
Prossimo passo: le lambda expression
Hai visto che un metodo generico riceve un tipo come parametro. Il passo successivo è trattare un comportamento come parametro. Le lambda expression ti permettono di passare un pezzo di codice, una funzione, a un metodo: è proprio così che ordinerai, filtrerai e trasformerai in modo type-safe le collezioni generiche che hai appena imparato a usare.
Domande frequenti
Cosa sono i generics in Java?
I generics ti permettono di scrivere una classe o un metodo che funziona con un tipo specificato in seguito, invece di legarlo a un solo tipo concreto. Dichiari un parametro di tipo tra parentesi angolari, class Box<T>, e chi usa il codice indica il tipo reale, Box<String>. Il compilatore poi impone quel tipo ovunque, così scopri le incongruenze in fase di compilazione ed eviti i cast manuali.
Perché usare i generics invece di Object?
Usare Object fa perdere tutte le informazioni sul tipo: il compilatore non può impedirti di inserire la cosa sbagliata, e devi fare il cast di ogni valore che estrai (rischiando una ClassCastException a runtime). I generics spostano quel controllo in fase di compilazione. List<String> semplicemente non accetta un Integer, e get() restituisce già una String: niente cast, niente sorprese a runtime.
Cos'è la type erasure nei generics di Java?
La type erasure significa che le informazioni sui tipi generici esistono solo in fase di compilazione. Dopo la compilazione, List<String> e List<Integer> sono entrambe solo List a runtime: il parametro di tipo viene cancellato. Ecco perché non puoi scrivere new T[10], usare list instanceof List<String> o leggere un parametro di tipo tramite reflection. I generics ti danno sicurezza in fase di compilazione, non informazioni sui tipi a runtime.