Menu

Generics in Java: classi e metodi type-safe

Cosa sono i generics di Java, come scrivere classi e metodi generici, parametri di tipo vincolati, wildcard e perché la type erasure è importante.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA