W3docs

Les Wildcards Génériques en Java

Utilisez les wildcards bornés supérieurs, bornés inférieurs et non bornés dans les génériques Java, ainsi que la règle PECS.

Un wildcard est le jeton ? qui apparaît dans les types génériques à la place d'un argument de type concret — List<?>, List<? extends Number>, List<? super Integer>. C'est la réponse à un problème qu'on rencontre presque immédiatement en commençant à écrire du code générique : List<Integer> n'est pas un sous-type de List<Number>, même si Integer est un sous-type de Number. Les wildcards permettent de décrire "une liste de quelque Number" sans s'engager sur un type d'élément spécifique — et ils constituent, de loin, la partie la plus déroutante du système de types de Java.

Le point de départ contre-intuitif

Voici le fait qui rend les wildcards nécessaires :

List<Integer> ints  = List.of(1, 2, 3);
List<Number>  nums  = ints;            // ❌ does not compile

Même si Integer extends Number, List<Integer> n'étend pas List<Number>. Les types génériques sont invariantsList<Sub> et List<Super> ne sont pas liés, peu importe ce que sont Sub et Super.

La raison est fondée, même si surprenante. Si List<Integer> était une List<Number>, on pourrait faire ceci :

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

Le cast sur la dernière ligne exploserait. Pour éviter cela, le compilateur refuse dès la première étape : List<Integer> n'est pas une List<Number>. Point final.

Les wildcards permettent de retrouver cette flexibilité en toute sécurité.

Le wildcard non borné : List<?>

Le wildcard le plus simple est le solitaire ? — "une liste d'un type inconnu" :

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

Dans le corps, la seule chose qu'on peut faire avec les éléments d'une List<?> est de les lire en tant qu'Object — car le compilateur ne sait pas ce qu'est ?. On ne peut pas ajouter quoi que ce soit à une List<?> (à l'unique exception de 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<?> s'utilise lorsqu'on veut exprimer "j'accepte n'importe quelle liste et je vais seulement la lire en tant qu'Object."

Le wildcard borné supérieur : ? extends T

Quand on a besoin de lire les éléments en tant que type spécifique — par exemple, les traiter tous comme des Number — on utilise un wildcard borné supérieur :

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> se lit comme "une liste d'un type spécifique qui est Number ou un sous-type de Number." On peut lire depuis elle en tant que Number. On ne peut pas ajouter quoi que ce soit, à l'exception de null — car le compilateur ne sait pas quel sous-type de Number la liste contient réellement. Ajouter un Integer à une List<? extends Number> qui est secrètement une List<Double> la corromprait ; plutôt que d'essayer de déterminer quel sous-type c'est, le compilateur refuse tout simplement chaque add.

Le wildcard borné inférieur : ? super T

L'image miroir. ? super T signifie "le type d'élément de la liste est T ou un supertype de 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

Ici, on peut ajouter n'importe quel Integer (ou sous-type) en toute sécurité — le type d'élément de la liste est garanti d'être Integer ou l'un de ses ancêtres, donc un Integer convient. Ce qu'on ne peut pas faire, c'est lire un type spécifique — le plus qu'on puisse dire d'un élément, c'est qu'il s'agit d'un Object, car la liste réelle pourrait être une List<Object>.

La règle PECS

Il existe un mnémotechnique que tout développeur Java finit par mémoriser :

PECS — Producer Extends, Consumer Super.

C'est la règle empirique pour savoir quel wildcard utiliser :

  • Si le paramètre produit des valeurs (on lit depuis lui) : utiliser ? extends T.
  • Si le paramètre consomme des valeurs (on écrit dedans) : utiliser ? super T.

La signature canonique qu'elle produit est 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 est lu (il produit des T) — ? extends T. dest est écrit (il consomme des T) — ? super T. Voilà toute la raison de l'asymétrie : la même signature fonctionne que src soit une List<Integer> et dest une List<Number>, ou l'inverse, à condition que T soit un point de rencontre entre eux.

Si vous ne retenez qu'une seule chose de ce chapitre, retenez PECS.

Quand ne pas utiliser un wildcard

Si un paramètre est à la fois lu et écrit dans la même méthode, ni ? extends T ni ? super T ne fonctionnent — aucun des deux ne permet de faire les deux. Dans ce cas, utilisez simplement un paramètre de type normal :

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 est le bon outil quand l'un des côtés de la relation dit "je ne lis que" ou "je n'écris que". Un paramètre de type est le bon outil quand on a besoin de parler d'un type d'élément spécifique des deux côtés.

Wildcards vs. paramètres de type bornés

Comparaison :

public static <T extends Number> double sumNamed(List<T> list)         { ... }
public static            double sumWildcard(List<? extends Number> list) { ... }

Fonctionnellement, ces deux formes acceptent le même ensemble d'arguments. La différence réside dans ce que le corps peut dire :

  • La forme nommée (<T extends Number>) vous donne un nom T — utile si vous voulez retourner T, accepter une autre List<T> comme second paramètre, ou écrire T tmp = list.get(0) pour préserver le type d'élément précis.
  • La forme wildcard (? extends Number) ne vous donne pas de nom — vous ne pouvez faire référence aux éléments qu'en tant que Number. Elle est plus concise dans l'API (aucun nom ne transparaît dans la signature) mais moins expressive dans le corps.

Règle empirique : si vous n'avez besoin des éléments qu'en tant que Number, le wildcard est le choix le plus simple et le plus propre. Si le corps doit parler d'un T spécifique, nommez-le.

Un exemple concret : PECS en pratique

Le programme copie des éléments d'une liste dans une autre et calcule la somme cumulée — les deux opérations paramétrées à la manière PECS. Observez les sites d'appel : copyOf(intList, numberList) mélange les types d'éléments car les wildcards permettent à une destination Number d'accepter des valeurs Integer.

java— editable, runs on the server

sum accepte une List<Integer> et une List<Double> car le wildcard dit "un sous-type de Number." fillWithSquares ajoute des valeurs Integer dans une List<Number> car le wildcard dit "toute liste pouvant contenir Integer ou l'un de ses ancêtres." copyTo utilise les deux — la source est un producteur, la destination est un consommateur, et T est le type d'élément partagé que le compilateur déduit à partir de l'accord entre les deux côtés.

La suite

Vous avez vu les quatre façons dont les génériques apparaissent dans le code source — classes, méthodes, interfaces et wildcards. Nous allons maintenant regarder une couche plus bas pour voir comment la JVM implémente réellement tout cela. La réponse — l'effacement de type — explique certaines restrictions surprenantes (new T() impossible, instanceof T impossible, tableaux génériques impossibles) et constitue l'information essentielle qui rend cohérente l'histoire des génériques en Java. Continuez vers Java Type Erasure.

Pratique

Pratique
Vous écrivez `addAll(Collection<? extends T> src, Collection<? super T> dest)`. Pourquoi exactement cette combinaison de wildcards ?
Vous écrivez `addAll(Collection<? extends T> src, Collection<? super T> dest)`. Pourquoi exactement cette combinaison de wildcards ?
Was this page helpful?