W3docs

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 compiler

Deux 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 runtime

Le 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 String

Le <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 ClassCastException cesse simplement de se produire.
  • Plus de casts. Lire dans une Map<String, User> vous donne un User, pas un Object à 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 soit Object partout, soit livrait une StringList, 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 Integer

Le <> 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 — utilisez Pair<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() ou instanceof T — Java efface les génériques à l’exécution, donc le programme n’a pas de T à 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.

java— editable, runs on the server

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 sur T.
  • Jokers (wildcards)? extends T, ? super T et 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

Pratique

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?