Java Generic Wildcards
Impara a usare i wildcard illimitati, con limite superiore e inferiore nei generics Java e la regola PECS.
Un wildcard è il token ? che compare nei tipi generici al posto di un argomento di tipo concreto — List<?>, List<? extends Number>, List<? super Integer>. È la soluzione a un problema che si incontra quasi subito quando si inizia a scrivere codice generico: List<Integer> non è un sottotipo di List<Number>, anche se Integer è un sottotipo di Number. I wildcard sono il modo per descrivere "una lista di qualche Number" senza impegnarsi in un tipo di elemento specifico — e sono, di gran lunga, il singolo elemento più confuso del sistema di tipi di Java.
Il punto di partenza controintuitivo
Ecco il fatto che rende i wildcard necessari:
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = ints; // ❌ does not compileAnche se Integer extends Number, List<Integer> non estende List<Number>. I tipi generici sono invarianti — List<Sub> e List<Super> sono senza relazione, indipendentemente da cosa siano Sub e Super.
La ragione è valida, anche se sorprendente. Se List<Integer> fosse una List<Number>, si potrebbe fare questo:
List<Number> nums = ints; // pretend this is legal
nums.add(3.14); // legal — 3.14 is a Number
int x = ints.get(3); // KABOOM at runtime — it's a DoubleIl cast nell'ultima riga esploderebbe. Per evitarlo, il compilatore rifiuta il primissimo passo: List<Integer> non è una List<Number>. Fine.
I wildcard sono il modo per recuperare la flessibilità in modo sicuro.
Il wildcard illimitato: List<?>
Il wildcard più semplice è il solitario ? — "una lista di qualche tipo sconosciuto":
public static void printAll(List<?> list) {
for (Object o : list) System.out.println(o);
}
printAll(List.of(1, 2, 3)); // List<Integer> — OK
printAll(List.of("a", "b")); // List<String> — OK
printAll(new ArrayList<>()); // List<Object> — OKAll'interno del corpo, l'unica cosa che puoi fare con gli elementi di una List<?> è leggerli come Object — perché il compilatore non sa cosa sia ?. Non è possibile aggiungere nulla a una List<?> (con la sola eccezione di null):
public static void corrupt(List<?> list) {
list.add("hello"); // ❌ does not compile — ? is unknown
list.add(null); // ✓ — null is a value of every reference type
}List<?> è ciò che si scrive quando si vuole esprimere "accetto qualsiasi lista e leggo solo da essa come Object."
Wildcard con limite superiore: ? extends T
Quando è necessario leggere gli elementi come un tipo specifico — ad esempio, trattarli tutti come Number — si usa un wildcard con limite superiore:
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue(); // legal — every element IS-A Number
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> — OK, Integer extends Number
sum(List.of(1.5, 2.5)); // List<Double> — OK
sum(List.of(1L, 2L, 3L)); // List<Long> — OKList<? extends Number> si legge come "una lista di qualche tipo specifico che è Number o un sottotipo di Number." È possibile leggere da essa come Number. Non è possibile aggiungere nulla, anche qui con l'eccezione di null — perché il compilatore non sa quale sottotipo di Number la lista contenga effettivamente. Aggiungere un Integer a una List<? extends Number> che in realtà è una List<Double> la corromperebbe; piuttosto che cercare di capire quale sottotipo sia, il compilatore rifiuta semplicemente ogni add.
Wildcard con limite inferiore: ? super T
L'immagine speculare. ? super T significa "il tipo di elemento della lista è T o qualche supertipo di T":
public static void addOneTwoThree(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Integer> ints = new ArrayList<>(); addOneTwoThree(ints); // ✓
List<Number> nums = new ArrayList<>(); addOneTwoThree(nums); // ✓ — Number is a supertype of Integer
List<Object> objs = new ArrayList<>(); addOneTwoThree(objs); // ✓ — Object is tooQui è possibile aggiungere qualsiasi Integer (o sottotipo) in modo sicuro — il tipo di elemento della lista è garantito essere Integer o qualche suo antenato, quindi un Integer ci sta. Quello che non si può fare è leggere un tipo specifico — il massimo che si può dire di un elemento è che è un Object, perché la lista effettiva potrebbe essere List<Object>.
La regola PECS
Esiste un mnemonico che ogni sviluppatore Java memorizza alla fine:
PECS — Producer Extends, Consumer Super.
È la regola empirica per sapere quando usare quale wildcard:
- Se il parametro produce valori (si legge da esso): usare
? extends T. - Se il parametro consuma valori (si scrive su di esso): usare
? super T.
La firma canonica che produce è Collections.copy:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}src viene letto (produce T) — ? extends T. dest viene scritto (consuma T) — ? super T. Questa è l'intera ragione dell'asimmetria: la stessa firma funziona sia che src sia una List<Integer> e dest una List<Number>, sia viceversa, purché T sia un punto di incontro tra i due.
Se non si ricorda nient'altro di questo capitolo, si ricordi PECS.
Quando non usare un wildcard
Se un parametro viene sia letto che scritto nello stesso metodo, né ? extends T né ? super T funzionano — nessuno dei due permette entrambe le operazioni. In quel caso, si usa semplicemente un normale parametro di tipo:
public static <T> void swap(List<T> list, int i, int j) {
T tmp = list.get(i); // read
list.set(i, list.get(j)); // write
list.set(j, tmp); // write
}Un wildcard è lo strumento giusto quando un lato della relazione è "leggo soltanto" o "scrivo soltanto." Un parametro di tipo è lo strumento giusto quando è necessario parlare di un tipo di elemento specifico su entrambi i lati.
Wildcard vs. parametri di tipo con limite
A confronto:
public static <T extends Number> double sumNamed(List<T> list) { ... }
public static double sumWildcard(List<? extends Number> list) { ... }Dal punto di vista funzionale accettano lo stesso insieme di argomenti. La differenza sta in ciò che il corpo può dire:
- La forma con nome (
<T extends Number>) fornisce un nomeT— utile se si vuole restituireT, accettare un'altraList<T>come secondo parametro, o scrivereT tmp = list.get(0)per preservare il tipo di elemento preciso. - La forma con wildcard (
? extends Number) non fornisce un nome — è possibile fare riferimento agli elementi solo comeNumber. È più sintetica nell'API (nessun nome trapela nella firma) ma meno espressiva nel corpo.
Regola empirica: se si ha bisogno degli elementi solo come Number, il wildcard è la scelta più piccola e pulita. Se il corpo ha bisogno di parlare di uno specifico T, è meglio dargli un nome.
Un esempio pratico: PECS in azione
Il programma copia elementi da una lista all'altra e calcola la somma progressiva — entrambe le operazioni parametrizzate secondo PECS. Si osservino i call site: copyOf(intList, numberList) mescola i tipi di elementi perché i wildcard permettono a una destinazione Number di accettare valori Integer.
sum accetta sia una List<Integer> che una List<Double> perché il wildcard dice "qualche sottotipo di Number." fillWithSquares aggiunge valori Integer in una List<Number> perché il wildcard dice "qualsiasi lista che possa contenere Integer o uno dei suoi antenati." copyTo usa entrambi — la sorgente è un producer, la destinazione è un consumer, e T è il tipo di elemento condiviso che il compilatore inferisce dall'accordo dei due lati.
Cosa c'è dopo
Hai visto i quattro modi in cui i generics compaiono nel codice sorgente — classi, metodi, interfacce e wildcard. Ora scendiamo di un livello per vedere come la JVM implementa effettivamente tutto questo. La risposta — type erasure — spiega alcune restrizioni sorprendenti (no new T(), no instanceof T, no array generici) ed è l'unica intuizione che fa scattare il puzzle dei generics Java. Continua con Java Type Erasure.