Génériques : lire et utiliser les <T>
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 (
Listtout 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),KetV(clé et valeur d'uneMap). 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 Tet? super Tdans les signatures de Java (par exempleList<? extends Number>). Ils servent surtout à écrire des bibliothèques. Pour lire :? extends Numbersignifie « une liste deNumberou 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 uneMapà lire depuis du JSON → une...TypeReference. Pour un objet seul,ProduitDto.classsuffit.
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
- Toujours préciser le type :
List<Commande>, jamaisListseul. - Utilise le diamant
<>à droite :new ArrayList<>(). - Pas de
List<Object>pour dire « n'importe quelle liste » : utiliseList<?>. - Corrige les avertissements de type brut, ne les masque pas avec
@SuppressWarnings. - Pour lire une liste en JSON, utilise
ParameterizedTypeReference(Spring) ouTypeReference(Jackson). - 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 (
Listsans<...>) désactive ces vérifications : à éviter. List<String>n'est pas uneList<Object>; pour accepter n'importe quelle liste, utiliseList<?>.- Les types sont effacés à l'exécution : pour lire une liste en JSON, il faut un
ParameterizedTypeReferenceou unTypeReference. - Tu écris rarement des génériques compliqués, mais tu en lis tous les jours : savoir les lire suffit pour commencer.