CodeGym /Cours /JAVA 25 SELF /Jokers des génériques

Jokers des génériques

JAVA 25 SELF
Niveau 27, Leçon 4
Disponible

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 !
1
Mission
JAVA 25 SELF, niveau 27, leçon 4
Bloqué
Calcul de la valeur totale des actifs 💰
Calcul de la valeur totale des actifs 💰
1
Mission
JAVA 25 SELF, niveau 27, leçon 4
Bloqué
Système universel de journalisation des événements 📜
Système universel de journalisation des événements 📜
1
Étude/Quiz
Interfaces des collections, niveau 27, leçon 4
Indisponible
Interfaces des collections
Interfaces des collections
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION