Introduction aux génériques Java
Pourquoi les génériques Java existent — sûreté des types, réutilisation du code et suppression des casts dans les collections et les API.
Introduction aux génériques Java
Les génériques sont la fonctionnalité qui permet à une classe, une interface ou une méthode de travailler sur un type non spécifié, puis de laisser le compilateur fixer 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 mettre une Date est rejetée avant même que le programme ne s’exécute. Avant l’arrivée des génériques en Java 5, la même liste était une List d’Object, et chaque lecture nécessitait un cast écrit à la main qui pouvait ou non réussir à l’exécution. Les génériques ont transformé ce pari à l’exécution en une vérification à la compilation, et presque toute API Java moderne est façonnée par eux.
Le problème que résolvent les génériques
Pour voir pourquoi les génériques existent, imaginez un Java sans eux. Un conteneur qui contient des choses 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. D’abord, le cast est du bruit — chaque lecture du conteneur en exige un. Ensuite, et pire, rien n’empêche quelqu’un de mettre 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 un cadre de pile utile près de la vraie faute.
Les génériques corrigent les deux :
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 dit au compilateur « cette liste contient des String », et à partir de ce moment chaque add et get est vérifié par rapport à cette promesse.
Trois choses que vous obtenez gratuitement
Les génériques vous apportent trois bénéfices concrets, et ils sont la raison pour laquelle chaque collection, stream et optional du JDK moderne est générique :
- Des vérifications plus fortes à la compilation. L’insertion du mauvais type ci-dessus est attrapée au build, pas en production. Toute une classe de
ClassCastExceptioncesse simplement de se produire. - Plus de casts. Lire dans une
Map<String, User>vous donne unUser, pas unObjectà caster. Moins de bruit syntaxique, moins à lire, moins à maintenir. - Réutilisation du code sans copier-coller. Une seule classe
List<E>fonctionne pour chaque type d’élément. Avant les génériques, la bibliothèque standard acceptait soitObjectpartout, soit livrait uneStringList,IntList,DateList, etc. Maintenant vous écrivez une classe et laissez l’appelant la paramétrer.
Ce dernier point est le plus grand gain architectural. Les génériques, c’est ainsi que vous écrivez un conteneur, un algorithme ou une forme de callback une seule fois et que vous l’appliquez à chaque type que l’appelant pourrait passer.
Une première classe générique
La convention est de nommer un paramètre de type par une seule lettre majuscule — T pour un « type » générique, E pour « element » d’une collection, K/V pour « key » et « value » d’une map, R pour « return ». Voici la classe générique la plus simple possible — une paire de deux choses 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 dans la classe partout où un type normal pourrait aller. L’appelant choisit T lorsqu’il crée 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+) dit au compilateur d’inférer le type depuis la déclaration de gauche — vous n’avez presque jamais à répéter l’argument de type.
Ce qui est paramétré, et ce qui ne l’est pas
Un paramètre de type peut tenir lieu de :
- Le type d’un champ (
private T value;) - Un type de paramètre ou de retour d’une méthode (
public T get() { ... },void put(T value)) - Le type d’élément d’un tableau de ce type (
T[] items— avec quelques réserves)
Un paramètre de type ne peut pas tenir lieu de :
- 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 génériques à l’exécution, donc le programme n’a pas deTà construire ni à tester
La liste complète des « choses que vous ne pouvez pas faire » a son propre chapitre à la fin de cette partie — Restrictions des génériques Java — une fois que nous aurons couvert assez de mécanismes pour que les règles aient du sens.
Un exemple détaillé : sûreté des types vs types bruts, côte à côte
Le programme ci-dessous construit le même conteneur deux fois — une fois comme une List brute (la forme d’avant les génériques) et une fois comme une List<String>. Les deux compilent ; seule la paramétrée est sûre.
La version brute plante en pleine itération parce que la boucle a fait confiance à un cast qu’elle n’avait aucune raison d’honorer. La version générique a rendu la même erreur impossible à représenter — le mauvais add(42) ne compile tout simplement pas. Ce glissement de l’exécution vers la compilation est toute la raison d’être des génériques.
Ce que couvre cette partie du livre
Les chapitres restants de cette partie démontent les génériques pièce par pièce :
- 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. - Jokers (wildcards) —
? extends T,? super Tet la règle PECS qui décide quand utiliser chacun. - Effacement de type — comment la JVM implémente les génériques en coulisses, et pourquoi certaines choses auxquelles vous vous attendez 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 les précédents.
La suite
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 Classes génériques Java.
Practice
A method declares `public static List getNames() { ... }` (no type parameter on the list). The caller writes `String first = getNames().get(0);`. Why does the compiler warn — and what's the danger if you ignore the warning?