W3docs

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 compile

Anche se Integer extends Number, List<Integer> non estende List<Number>. I tipi generici sono invariantiList<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 Double

Il 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>  — OK

All'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>    — OK

List<? 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 too

Qui è 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? 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 nome T — utile se si vuole restituire T, accettare un'altra List<T> come secondo parametro, o scrivere T 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 come Number. È 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.

java— editable, runs on the server

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.

Pratica

Pratica
Stai scrivendo `addAll(Collection<? extends T> src, Collection<? super T> dest)`. Perché esattamente questa combinazione di wildcard?
Stai scrivendo `addAll(Collection<? extends T> src, Collection<? super T> dest)`. Perché esattamente questa combinazione di wildcard?
Was this page helpful?