Introduction aux Generics en Java
Pourquoi les Generics Java existent — sécurité des types, réutilisation du code et suppression des casts dans les collections et les API.
Les Generics sont la fonctionnalité qui permet à une classe, une interface ou une méthode de travailler sur un type non spécifié, puis de faire en sorte que le compilateur fixe ce type à l'endroit où vous l'utilisez. Une List<String> est une liste de chaînes, le compilateur le sait, et toute tentative d'y insérer une Date est rejetée avant même que le programme ne s'exécute. Avant l'arrivée des Generics dans Java 5, la même liste était une List d'Object, et chaque lecture nécessitait un cast écrit à la main qui pouvait réussir ou échouer à l'exécution. Les Generics ont transformé ce risque à l'exécution en vérification à la compilation, et presque toutes les API Java modernes en sont le fruit.
Le problème que les Generics résolvent
Pour comprendre pourquoi les Generics existent, imaginez un Java sans eux. Un conteneur qui stocke des éléments arbitraires doit déclarer son contenu comme Object :
// Pre-Java-5 style — what the standard library actually looked like.
List names = new ArrayList();
names.add("Ada");
names.add("Linus");
String first = (String) names.get(0); // cast required, never checked by the compilerDeux problèmes. Premièrement, le cast est du bruit — chaque lecture depuis le conteneur en nécessite un. Deuxièmement, et c'est pire, rien n'empêche quelqu'un d'insérer une Date dans cette même liste :
names.add(new java.util.Date()); // compiler is fine with this
String oops = (String) names.get(2); // ClassCastException at runtimeLe bug apparaît à la lecture, loin de l'écriture. Le cast ment — il dit « ceci est une String », et la JVM ne le découvre que lorsqu'il est trop tard pour vous donner une trace d'appel utile proche de la vraie faute.
Les Generics règlent les deux problèmes :
List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Linus");
names.add(new Date()); // ❌ compile error — won't even build
String first = names.get(0); // no cast — the compiler already knows it's a StringLe <String> entre chevrons est le paramètre de type. Il indique au compilateur « cette liste contient des Strings », et dès lors chaque appel à add et get est vérifié par rapport à cette promesse.
Trois avantages offerts gratuitement
Les Generics vous offrent trois bénéfices concrets, et c'est pourquoi chaque collection, stream et optional du JDK moderne est générique :
- Des vérifications à la compilation plus strictes. L'insertion d'un mauvais type ci-dessus est détectée à la construction, pas en production. Une catégorie entière de
ClassCastExceptioncesse tout simplement de se produire. - Plus de casts. Lire depuis une
Map<String, User>vous donne unUser, pas unObjectqu'il faut caster. Moins de bruit syntaxique, moins de code à lire, moins à maintenir. - Réutilisation du code sans copier-coller. Une seule classe
List<E>fonctionne pour chaque type d'élément. Avant les Generics, la bibliothèque standard acceptaitObjectpartout ou livrait uneStringList,IntList,DateList, etc. On écrit maintenant une seule classe et on laisse l'appelant la paramétrer.
Ce dernier point est le gain architectural le plus important. Les Generics permettent d'écrire un conteneur, un algorithme ou une forme de callback une seule fois et de l'appliquer à tous les types que l'appelant pourrait passer.
Une première classe générique
La convention est de nommer un paramètre de type avec une seule lettre majuscule — T pour un « type » générique, E pour « élément » d'une collection, K/V pour « clé » et « valeur » d'une map, R pour « retour ». Voici la classe générique la plus simple possible — une paire de deux éléments du même type :
public class Pair<T> {
private final T first;
private final T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T first() { return first; }
public T second() { return second; }
}Le <T> après le nom de la classe introduit le paramètre de type. À partir de là, T est utilisable à l'intérieur de la classe partout où un type normal pourrait être utilisé. L'appelant choisit T lors de la création de l'objet :
Pair<String> names = new Pair<>("Ada", "Grace");
Pair<Integer> scores = new Pair<>(100, 87);
String n1 = names.first(); // already a String, no cast
int s1 = scores.first(); // auto-unboxed from IntegerLe <> vide à droite (l'opérateur diamant, Java 7+) indique au compilateur d'inférer le type depuis la déclaration du côté gauche — vous n'avez presque jamais besoin de répéter l'argument de type.
Ce qui peut être paramétré, et ce qui ne peut pas l'être
Un paramètre de type peut remplacer :
- Le type d'un champ (
private T value;) - Un paramètre ou le type de retour d'une méthode (
public T get() { ... },void put(T value)) - Le type des éléments d'un tableau de ce type (
T[] items— avec quelques mises en garde)
Un paramètre de type ne peut pas remplacer :
- Un primitif (
Pair<int>est illégal — utilisezPair<Integer>et laissez l'autoboxing faire le travail) - Le paramètre de type d'un champ statique ou d'une méthode statique (le paramètre appartient à une instance, pas à la classe elle-même)
- La cible de
new T()ouinstanceof T— Java efface les Generics à l'exécution, donc le programme n'a pas deTà construire ou tester
La liste complète des « choses que vous ne pouvez pas faire » fait l'objet de son propre chapitre à la fin de cette partie — Java Generics Restrictions — une fois que nous aurons couvert suffisamment de mécanismes pour que les règles aient du sens.
Un exemple concret : sécurité des types vs. types bruts, côte à côte
Le programme ci-dessous construit le même conteneur deux fois — une fois en tant que List brut (la forme pré-Generics) et une fois en tant que List<String>. Les deux compilent ; seul le paramétré est sûr.
La version brute plante en milieu d'itération parce que la boucle a fait confiance à un cast qu'elle n'avait pas à faire. La version générique a rendu la même erreur impossible à représenter — le mauvais add(42) ne compilera pas du tout. Ce passage de l'exécution à la compilation est la raison d'être des Generics.
Ce que couvre cette partie du livre
Les chapitres restants de cette partie décortiquent les Generics un élément à la fois :
- Classes génériques — le paramètre de type au niveau de la classe que vous venez de voir, plus en profondeur.
- Méthodes génériques — des méthodes qui introduisent leur propre paramètre de type, indépendamment de la classe.
- Interfaces génériques — concevoir des contrats d'API paramétrés sur un type.
- Paramètres de type bornés — dire « T doit étendre
Number» pour pouvoir appeler des méthodes surT. - Wildcards —
? extends T,? super T, et la règle PECS qui décide lequel utiliser. - Type erasure — comment la JVM implémente les Generics sous le capot, et pourquoi certaines choses que vous pourriez attendre ne fonctionnent pas.
- Restrictions — le catalogue des choses que le langage refuse de vous laisser faire, avec les raisons derrière chacune.
Lisez-les dans l'ordre — chaque chapitre suppose que les précédents ont été lus.
Prochaine étape
Commencez par la forme la plus courante — une classe dont les champs et méthodes sont paramétrés sur un type choisi par l'appelant. Continuez avec Java Generic Classes.