1. Invariance des génériques : List<Number> ≠ List<Integer>
En Java, les génériques sont stricts : ils sont invariants. Cela signifie que même si un type est un sous-type d’un autre (par exemple, Integer est un sous-type de Number), les collections avec ces types ne sont pas liées entre elles.
Imaginez : vous avez une boîte de pommes (List<Integer>), et quelqu’un dit que c’est une « boîte de fruits » (List<Number>). Cela semble logique, car les pommes sont des fruits. Mais alors on pourrait y mettre une banane (Double), et tout casserait — la boîte est en réalité « aux pommes ».
Exemple de code :
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Erreur de compilation !
Le compilateur est volontairement strict ici : il n’autorise pas de transformer une liste de « pommes » en liste de « fruits ». Sinon, on pourrait y ajouter n’importe quoi, et le programme tomberait en panne à l’exécution.
Ainsi, List<Number> et List<Integer> sont deux types complètement différents, même si Integer est lui-même un sous-type de Number.
Contrairement aux tableaux :
Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // Autorisé (les tableaux sont covariants)
numArr[0] = 3.14; // Erreur à l'exécution (ArrayStoreException)
Conclusion : les génériques sont invariants afin d’assurer la sécurité de type à la compilation.
2. Bornes des paramètres de type
Parfois, il faut restreindre avec quels types une classe ou une méthode générique peut fonctionner. Pour cela, on utilise des bornes (bounds).
Borne supérieure (extends)
class Stats<T extends Number> {
private T[] nums;
// ...
}
Désormais, Stats ne peut fonctionner qu’avec des types qui sont des sous-classes de Number (Integer, Double, Float, etc.). Tenter de créer Stats<String> provoquera une erreur de compilation.
Borne avec interface
class Sorter<T extends Comparable<T>> {
void sort(List<T> list) { /* ... */ }
}
Désormais, Sorter ne peut fonctionner qu’avec des types qui implémentent l’interface Comparable.
Bornes multiples
On peut indiquer plusieurs bornes à l’aide de & :
class MyClass<T extends Number & Comparable<T>> { /* ... */ }
T doit être une sous-classe de Number et implémenter Comparable<T>.
L’ordre est important : d’abord la classe, puis les interfaces.
3. Découverte du joker : ?
Un joker est un type « sous-inconnu » qui dit : « Il peut y avoir un certain type ici, mais je ne sais pas lequel exactement. »
Exemples :
- List<?> — une liste de n’importe quoi.
- List<? extends Number> — une liste de tout type qui est une sous-classe de Number (par exemple, Integer, Double).
- List<? super Integer> — une liste de tout type qui est un supertype de Integer (par exemple, Integer, Number, Object).
À quoi servent les jokers ?
Ils permettent d’écrire des méthodes qui fonctionnent avec des collections de types différents mais apparentés.
Exemple : lecture seule (producteur)
void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
// list.add(123); // Erreur ! Il est interdit d'ajouter des éléments
}
- On peut lire les éléments en tant que Number.
- Impossible d’ajouter des éléments (sauf null).
Exemple : écriture seule (consommateur)
void addIntegers(List<? super Integer> list) {
list.add(42); // OK
// Integer x = list.get(0); // Erreur ! On ne sait pas quel type sera renvoyé
}
- On peut ajouter des éléments de type Integer (ou ses sous-types).
- Impossible de lire en toute sécurité les éléments en tant que Integer (seulement en tant que Object).
Règle PECS
PECS — « Producer Extends, Consumer Super » :
- Producer — Extends : si la collection ne fait que fournir des données (producteur), utilisez ? extends T.
- Consumer — Super : si la collection ne fait que consommer des données (consommateur), utilisez ? super T.
À retenir facilement : extends — pour la lecture (producteur), super — pour l’écriture (consommateur).
Comparaison : tableaux covariants, génériques invariants
- Les tableaux sont covariants : on peut affecter un Integer[] à une variable de type Number[].
- Les génériques sont invariants : on ne peut pas affecter un List<Integer> à une variable de type List<Number>.
Les jokers permettent d’atténuer partiellement l’invariance des génériques.
5. Méthodes génériques et inférence de types ; limitations (type erasure)
Méthodes génériques
On peut déclarer des méthodes génériques qui fonctionnent avec n’importe quels types :
public static <T> void printList(List<T> list) {
for (T elem : list) {
System.out.println(elem);
}
}
Inférence de type (type inference)
Java peut « deviner » le type du paramètre :
List<String> strings = List.of("a", "b");
printList(strings); // T = String
Limitations des génériques : effacement de types
- En Java, les génériques sont implémentés via l’effacement des types : l’information sur les paramètres de type est supprimée après la compilation.
- Dans le bytecode, aucune différence entre List<String> et List<Integer>.
- Impossible de créer des tableaux de types génériques : new List<String>[10] — erreur.
- Impossible d’utiliser instanceof avec des paramètres de type : obj instanceof List<String> — erreur.
6. Pratique sur les collections et l’API Stream
Copier des éléments entre listes
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
- src — producteur (? extends T)
- dest — consommateur (? super T)
Utilisation :
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK : Number est un supertype de Integer
Stream API et jokers
List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;
numbers.stream()
.map(Number::doubleValue)
.forEach(System.out::println);
Filtrage avec joker
public static void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
7. Erreurs fréquentes
Erreur n° 1 : Raw types (types bruts).
L’utilisation de « types bruts » désactive la vérification de type et mène à des erreurs à l’exécution.
List list = new ArrayList(); // raw type — mauvais !
list.add("chaîne");
list.add(123); // On peut ajouter n'importe quoi
String s = (String) list.get(1); // ClassCastException!
N’utilisez jamais les types bruts (raw types). Indiquez toujours les paramètres de type : List<String>, List<Integer>.
Erreur n° 2 : conversions non sûres avec extends.
Tenter d’ajouter des éléments dans une collection avec ? extends ... provoquera une erreur de compilation.
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // Erreur de compilation !
Erreur n° 3 : effacement de type lors de la surcharge.
On ne peut pas surcharger des méthodes uniquement par leurs paramètres génériques — après l’effacement des types, les signatures coïncident.
public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // Erreur de compilation !
Erreur n° 4 : tableaux de types génériques.
Impossible de créer des tableaux de types paramétrés à cause de l’effacement des types.
List<String>[] arr = new List<String>[10]; // Erreur de compilation !
GO TO FULL VERSION