Génériques : lire et utiliser les <T>

Publié le · Mis à jour le

L'idée en une phrase

Les génériques, ce sont les <...> que tu écris tous les jours : List<Commande>, Optional<Client>, JpaRepository<Commande, Long>. Ils indiquent quel type d'objet une classe manipule, pour que le compilateur vérifie ton code avant l'exécution. Cette fiche t'explique comment ils fonctionnent, et les deux ou trois pièges qu'on rencontre réellement.

Une image pour comprendre : les bocaux étiquetés

Imagine une cuisine avec des bocaux.

  • Un bocal étiqueté « Pâtes » : on ne peut y mettre que des pâtes, et quand on en sort quelque chose, on sait que ce sont des pâtes. C'est une List<Pates>.
  • Un bocal sans étiquette : on peut y mettre n'importe quoi, et on ne sait jamais ce qu'on va en sortir. C'est un type brut (List tout court).

Le compilateur joue le rôle du chef de cuisine : il vérifie, avant le service, que personne ne verse de la soupe dans le bocal de pâtes.

Retiens aussi ce détail, il servira plus loin : les étiquettes sont retirées avant l'ouverture du restaurant. Une fois le programme lancé, les bocaux n'ont plus d'étiquette.

À quoi servent les génériques ?

Sans génériques (c'était Java avant 2004), une liste contenait des Object, et il fallait convertir à la main :

List commandes = new ArrayList();      // ❌ bocal sans étiquette
commandes.add(new Commande(...));
commandes.add("oups");                 // accepté sans rien dire
Commande c = (Commande) commandes.get(1); // 💥 ClassCastException à l'exécution

Avec les génériques :

List<Commande> commandes = new ArrayList<>(); // ✅ bocal étiqueté
commandes.add(new Commande(...));
commandes.add("oups");                        // ❌ refusé à la compilation
Commande c = commandes.get(0);                // ✅ pas de conversion à écrire

L'erreur est détectée par le compilateur, dans ton IDE, au lieu d'apparaître en production.

Note : le <> vide à droite (new ArrayList<>()) s'appelle le diamant : Java devine le type à partir de la gauche. Pas besoin de répéter <Commande>.

Les génériques que tu utilises déjà

Tu en lis tous les jours dans un projet Spring Boot. Savoir les lire, c'est savoir ce que la classe contient :

Écriture Se lit
List<Commande> une liste de commandes
Map<String, Integer> une table qui associe un texte (la clé) à un nombre (la valeur)
Optional<Client> peut-être un client, peut-être rien
JpaRepository<Commande, Long> un repository de commandes, dont l'identifiant est un Long
ResponseEntity<CommandeDto> une réponse HTTP dont le corps est une CommandeDto
Page<Produit> une page de résultats contenant des produits
CompletableFuture<Void> un traitement asynchrone qui ne renvoie rien
public interface CommandeRepository extends JpaRepository<Commande, Long> {
    // Spring sait que findById(Long) renvoie un Optional<Commande>
}

C'est grâce aux génériques que commandeRepository.findById(42L) renvoie un Optional<Commande> et non un Optional<Object> : le repository « sait » ce qu'il contient.

Écrire ta propre classe générique

Le cas le plus courant en Spring Boot : une enveloppe de réponse qui peut contenir n'importe quel type de données.

public record PageDto<T>(List<T> contenu, int page, long total) {}

Le T est un paramètre de type : un emplacement qui sera remplacé par un vrai type à l'utilisation.

PageDto<CommandeDto> commandes = new PageDto<>(listeCommandes, 0, 154);
PageDto<ClientDto> clients = new PageDto<>(listeClients, 2, 37);

Une seule classe, réutilisable pour tous les types, et toujours vérifiée par le compilateur.

Une méthode peut aussi être générique. Le <T> se place avant le type de retour :

public static <T> T premierOuErreur(List<T> liste) {
    if (liste.isEmpty()) {
        throw new IllegalStateException("La liste est vide");
    }
    return liste.get(0);
}

Commande c = premierOuErreur(commandes); // T = Commande, deviné par Java

Astuce : par convention, les paramètres de type sont une seule lettre majuscule : T (type), E (élément), K et V (clé et valeur d'une Map). Tu les retrouveras partout dans la documentation de Java.

Piège n°1 : List<String> n'est pas une List<Object>

Tu veux écrire une méthode qui affiche n'importe quelle liste. Le premier réflexe :

void afficher(List<Object> elements) { ... }

List<String> noms = List.of("Alice", "Bruno");
afficher(noms); // ❌ erreur de compilation

Pourtant, une String est bien un Object. Pourquoi est-ce refusé ? Reprenons les bocaux : si un bocal de pâtes pouvait être traité comme un bocal « nourriture en général », quelqu'un pourrait y verser de la soupe, puisque la soupe est aussi de la nourriture.

List<String> noms = new ArrayList<>();
List<Object> objets = noms;  // si c'était autorisé…
objets.add(42);              // …on mettrait un nombre dans une liste de textes

La solution est le joker ?, qui veut dire « une liste de n'importe quoi » :

void afficher(List<?> elements) {      // ✅ accepte toute liste
    elements.forEach(System.out::println);
}

En échange, tu ne peux rien ajouter dans une List<?> : le compilateur ne sait pas quel type elle accepte. Tu peux seulement lire.

Note : tu croiseras aussi ? extends T et ? super T dans les signatures de Java (par exemple List<? extends Number>). Ils servent surtout à écrire des bibliothèques. Pour lire : ? extends Number signifie « une liste de Number ou d'un de ses sous-types ». Tu n'auras que rarement besoin d'en écrire.

Piège n°2 : les types bruts

IntelliJ affiche un avertissement sur ce code :

List commandes = repository.findAll(); // ⚠️ "Raw use of parameterized class 'List'"

Ce n'est pas un détail : un type brut désactive toutes les vérifications du compilateur. C'est le bocal sans étiquette.

List<Commande> commandes = repository.findAll(); // ✅

Ne laisse jamais un avertissement de type brut dans ton code. Et n'essaie pas de le faire taire avec @SuppressWarnings("unchecked") : corrige-le.

Piège n°3 : les types sont effacés à l'exécution

Voici le détail des bocaux : le compilateur vérifie les types, puis les retire. Une fois le programme lancé, une List<Commande> et une List<Client> sont exactement la même chose : une List.

C'est invisible la plupart du temps. Mais ça pose un problème bien réel quand une bibliothèque doit créer des objets d'un type précis, par exemple pour transformer du JSON en objets Java.

Le cas typique : lire une liste en JSON

Tu appelles une API externe qui renvoie une liste de produits :

List<ProduitDto> produits = restClient.get()
        .uri("/produits")
        .retrieve()
        .body(List.class); // ❌ une List<LinkedHashMap>, pas une List<ProduitDto>

List.class ne contient pas l'information « de ProduitDto » : elle a été effacée. La bibliothèque ne sait pas quoi créer, et remplit la liste avec des Map. Le plantage arrive plus loin, avec une ClassCastException incompréhensible.

La solution : un ParameterizedTypeReference, qui conserve le type complet :

List<ProduitDto> produits = restClient.get()
        .uri("/produits")
        .retrieve()
        .body(new ParameterizedTypeReference<List<ProduitDto>>() {}); // ✅

Les {} à la fin ne sont pas une erreur : ils créent une petite classe anonyme, et c'est ce qui permet de garder l'information de type malgré l'effacement.

Astuce : même problème, même solution avec Jackson directement : objectMapper.readValue(json, new TypeReference<List<ProduitDto>>() {}). Retiens le réflexe : une liste ou une Map à lire depuis du JSON → une ...TypeReference. Pour un objet seul, ProduitDto.class suffit.

Autres conséquences de l'effacement

Ces lignes ne compilent pas, pour la même raison (le type n'existe plus à l'exécution) :

if (objet instanceof List<String>) { } // ❌ impossible de vérifier le <String>
T element = new T();                    // ❌ Java ne sait pas quel T créer

Si tu tombes sur ces erreurs, ce n'est pas une faute de syntaxe : c'est une limite de Java. Il faut changer d'approche (en général, passer le type en paramètre, comme ProduitDto.class).

Les bonnes habitudes

  1. Toujours préciser le type : List<Commande>, jamais List seul.
  2. Utilise le diamant <> à droite : new ArrayList<>().
  3. Pas de List<Object> pour dire « n'importe quelle liste » : utilise List<?>.
  4. Corrige les avertissements de type brut, ne les masque pas avec @SuppressWarnings.
  5. Pour lire une liste en JSON, utilise ParameterizedTypeReference (Spring) ou TypeReference (Jackson).
  6. Une classe générique quand elle a un vrai intérêt (une enveloppe de réponse, un résultat paginé), pas par principe.

À retenir

  • Les génériques (<...>) indiquent ce que contient une classe, pour que le compilateur vérifie ton code.
  • Un type brut (List sans <...>) désactive ces vérifications : à éviter.
  • List<String> n'est pas une List<Object> ; pour accepter n'importe quelle liste, utilise List<?>.
  • Les types sont effacés à l'exécution : pour lire une liste en JSON, il faut un ParameterizedTypeReference ou un TypeReference.
  • Tu écris rarement des génériques compliqués, mais tu en lis tous les jours : savoir les lire suffit pour commencer.

Avis sur l'article

Pas encore de note : soyez le premier à donner votre avis.

Les avis sont publiés après validation par le modérateur.

Votre note