W3docs

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 compiler

Due 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 runtime

Il 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 String

Il <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 ClassCastException smette semplicemente di verificarsi.
  • Niente più cast. Leggere da una Map<String, User> ti restituisce un User, non un Object da 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 accettava Object ovunque oppure forniva StringList, IntList, DateList e 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 Integer

Il <> 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 — usa Pair<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() o instanceof T — Java cancella i generics a runtime, quindi il programma non ha alcun T da 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.

java— editable, runs on the server

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 su T.
  • 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.

Pratica

Pratica
Un metodo dichiara `public static List getNames() { ... }` (nessun parametro di tipo sulla lista). Il chiamante scrive `String first = getNames().get(0);`. Perché il compilatore avverte — e qual è il pericolo se si ignora l'avviso?
Un metodo dichiara `public static List getNames() { ... }` (nessun parametro di tipo sulla lista). Il chiamante scrive `String first = getNames().get(0);`. Perché il compilatore avverte — e qual è il pericolo se si ignora l'avviso?
Was this page helpful?