Différence entre <? super T> et <? extends T> en Java (PECS)
Les wildcards bornés <? extends T> et <? super T> sont parmi les concepts les plus confus des génériques Java. Ils permettent d'écrire des APIs flexibles qui acceptent une hiérarchie de types, mais la règle qui dicte leur choix (PECS) est contre-intuitive à première lecture.
Rappel rapide sur les génériques
En Java, List<Chien> n'est pas un sous-type de List<Animal>, même si Chien étend Animal. Les génériques sont invariants. Les wildcards sont la façon d'introduire de la variance contrôlée.
List<Chien> chiens = new ArrayList<>();
List<Animal> animaux = chiens; // ❌ Erreur de compilation
<? extends T> — le borne supérieure (covariance)
Signifie "un type inconnu qui est T ou une sous-classe de T". Vous pouvez lire des T depuis la collection, mais pas y écrire (sauf null).
List<? extends Animal> animaux = new ArrayList<Chien>();
Animal a = animaux.get(0); // ✅ on peut lire en Animal
// animaux.add(new Chien()); // ❌ le compilateur ignore quel sous-type exact est stocké
Logique : si animaux peut être une List<Chien>, List<Chat> ou List<Hamster>, y ajouter un Chien casserait la cohérence de la List<Chat>. Donc interdit.
<? super T> — le borne inférieure (contravariance)
Signifie "un type inconnu qui est T ou une super-classe de T". Vous pouvez écrire des T dans la collection, mais les lectures retournent Object.
List<? super Chien> collecteur = new ArrayList<Animal>();
collecteur.add(new Chien()); // ✅ on peut ajouter un Chien
collecteur.add(new Labrador()); // ✅ Labrador est aussi un Chien
// Chien c = collecteur.get(0); // ❌ le compilateur ne sait pas si c'est Animal, Object...
Object o = collecteur.get(0); // ✅ seul Object est garanti
Logique : n'importe quelle super-classe peut accueillir un Chien, donc l'écriture est sûre ; mais la lecture ne garantit que Object, le plus commun dénominateur.
La règle PECS — Producer Extends, Consumer Super
Formulée par Joshua Bloch dans Effective Java :
PECS : Producer Extends, Consumer Super.
Si un paramètre produit des T (la source de données), utilisez<? extends T>. S'il consomme des T (la destination), utilisez<? super T>.
Exemple classique : copier une liste
public static <T> void copier(
List<? extends T> source, // producer → extends
List<? super T> destination // consumer → super
) {
for (T item : source) {
destination.add(item);
}
}
List<Labrador> labradors = ...;
List<Animal> animaux = ...;
copier(labradors, animaux); // ✅ on copie des chiens dans une liste d'animaux
Cette API est beaucoup plus flexible que copier(List<T>, List<T>) qui exigerait des types exactement identiques.
Cas réels dans le JDK
Collections.copy
public static <T> void copy(List<? super T> dest, List<? extends T> src)
Stream.forEach
void forEach(Consumer<? super T> action);
// Consumer absorbe des T → super
Stream<Chien> chiens = ...;
Consumer<Animal> aboyeur = a -> System.out.println(a);
chiens.forEach(aboyeur); // ✅ Consumer<Animal> accepte des Chien
Stream.map
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// Function consomme T (super) et produit R (extends)
Récapitulatif visuel
| Usage | Wildcard | Lecture | Écriture |
|---|---|---|---|
| Producteur (lire depuis) | <? extends T> | ✅ T | ❌ rien |
| Consommateur (écrire dans) | <? super T> | ⚠ Object uniquement | ✅ T et sous-types |
| Producteur ET consommateur | <T> (pas de wildcard) | ✅ T | ✅ T |
Erreur fréquente
// Vouloir remplir une liste — mauvais choix
public static void remplir(List<? extends Number> liste) {
liste.add(42); // ❌ Erreur : extends interdit l'écriture
}
// Correct : super parce qu'on consomme
public static void remplir(List<? super Number> liste) {
liste.add(42); // ✅
}
Quand ne pas utiliser de wildcard ?
Si un paramètre est à la fois lu et écrit avec le même type, ou si vous devez référencer plusieurs paramètres du même type, utilisez un type générique nommé plutôt qu'un wildcard :
// Mauvais : impossible d'ajouter un élément lu depuis cette liste
public static void echangerPremier(List<?> l1, List<?> l2) { ... }
// Bon : T capture le type commun
public static <T> void echangerPremier(List<T> l1, List<T> l2) {
T temp = l1.get(0);
l1.set(0, l2.get(0));
l2.set(0, temp);
}
En résumé
<? extends T>: lecture seule, variance covariante, pour les sources.<? super T>: écriture seule, variance contravariante, pour les destinations.<T>sans wildcard : usage strict, sans variance.- PECS — Producer Extends, Consumer Super.
Maîtriser ces trois cas couvre 99 % des situations où vous avez besoin de variance dans une API Java générique.