Menu

Les génériques en Java : classes et méthodes typées

Ce que sont les génériques de Java, comment écrire des classes et des méthodes génériques, les paramètres de type bornés, les jokers et pourquoi l'effacement de type compte.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Pourquoi les génériques existent

Un type générique permet d'écrire le code une seule fois et de le réutiliser pour de nombreux types sans renoncer à la sécurité de typage. Au lieu d'écrire séparément une IntBox, une StringBox et une UserBox, vous écrivez une seule Box<T>T est un emplacement que l'appelant remplit.

Vous avez déjà utilisé des génériques chaque fois que vous avez écrit ArrayList<String> ou HashMap<String, Integer>. La partie <...> est un argument de type. Cette page montre comment écrire les vôtres.

L'alternative - tout stocker en tant qu'Object - jette l'information de type et impose des conversions laides et sujettes aux erreurs :

Cette conversion sur la dernière ligne lève une ClassCastException à l'exécution - exactement le type de bug que les génériques sont conçus pour rendre impossible.

Une classe générique

Déclarez un paramètre de type entre chevrons après le nom de la classe. Par convention, c'est une seule lettre majuscule : T pour « type », E pour « element » (élément), K/V pour clé/valeur.

À l'intérieur de Box, chaque T devient ce que l'appelant a fourni. Box<String> est une boîte qui ne contient et ne renvoie que des String. Le compilateur rejette name.set(99) avant même que le programme ne s'exécute.

Les <> vides à droite (l'opérateur diamant) permettent au compilateur d'inférer l'argument de type à partir du côté gauche, ce qui vous évite de répéter <String> deux fois.

Les méthodes génériques

Une méthode unique peut avoir son propre paramètre de type, indépendant de la classe. Placez le paramètre <T> avant le type de retour :

Vous ne passez jamais T explicitement - le compilateur l'infère à partir de l'argument. Les méthodes génériques sont la façon dont des utilitaires comme Collections.sort ou List.of restent typés en sécurité quel que soit le type d'élément.

Paramètres de type bornés

Parfois, un type générique n'a de sens que pour certains types. extends contraint le paramètre afin que vous puissiez appeler les méthodes de la borne. Ici T extends Number signifie que T est Number ou n'importe quelle sous-classe (Integer, Double, ...), donc doubleValue() est disponible :

Notez qu'ici extends signifie « est un sous-type de », et cela fonctionne aussi bien pour les classes que pour les interfaces - <T extends Comparable<T>> est extrêmement courant lorsque vous devez comparer des éléments.

Les jokers : ? extends et ? super

Un piège subtil : List<Integer> n'est pas une List<Number>, même si Integer est un Number. Les génériques sont invariants. Les jokers assouplissent cette règle lorsque vous avez seulement besoin de lire ou seulement besoin d'écrire.

Utilisez ? extends T pour un producteur dont vous lisez, et ? super T pour un consommateur dans lequel vous écrivez (la règle « PECS » - Producer Extends, Consumer Super) :

Une liste ? extends Number vous permet de lire les éléments comme des Number mais pas d'y ajouter (le compilateur ne peut pas connaître le type exact de l'élément). Une liste ? super Integer vous permet d'ajouter des Integer, mais les lectures reviennent en tant qu'Object. Choisissez le joker qui correspond à la façon dont les données circulent.

L'effacement de type et ses limites

Les génériques sont une fonctionnalité de compilation. Après la compilation, le paramètre de type est effacé - à l'exécution, Box<String> et Box<Integer> ne sont toutes deux qu'une Box. Cela maintient la compatibilité des génériques avec l'ancien code, mais impose de vraies limites.

// Aucun de ces exemples ne compile - le paramètre de type n'existe pas à l'exécution :
T value = new T();          // impossible d'instancier un paramètre de type
T[] array = new T[10];      // impossible de créer un tableau générique
if (list instanceof List<String>) { } // impossible de tester l'argument de type

Comme le type a disparu à l'exécution, vous ne pouvez pas demander « quel était T ? » par réflexion, et vous ne pouvez pas surcharger des méthodes qui ne diffèrent que par leur argument générique (foo(List<String>) et foo(List<Integer>) s'effacent vers la même signature). Lorsque vous avez réellement besoin du type à l'exécution, passez un jeton Class<T> en paramètre du constructeur ou de la méthode.

Et ensuite : les expressions lambda

Vous avez vu qu'une méthode générique prend un type en paramètre. L'étape suivante consiste à traiter le comportement comme un paramètre. Les expressions lambda permettent de passer un morceau de code - une fonction - à une méthode, ce qui est exactement la façon dont vous allez trier, filtrer et transformer les collections génériques que vous venez d'apprendre à typer en toute sécurité.

Questions fréquentes

Que sont les génériques en Java ?

Les génériques permettent d'écrire une classe ou une méthode qui fonctionne avec un type que vous précisez plus tard, au lieu de la figer sur un type concret unique. Vous déclarez un paramètre de type entre chevrons - class Box<T> - et l'appelant fournit le type réel - Box<String>. Le compilateur applique alors ce type partout : vous repérez les incompatibilités à la compilation et vous évitez les conversions manuelles.

Pourquoi utiliser les génériques plutôt que Object ?

Utiliser Object fait perdre toute l'information de type : le compilateur ne peut pas vous empêcher d'y mettre la mauvaise chose, et vous devez convertir chaque valeur qui ressort (au risque d'une ClassCastException à l'exécution). Les génériques déplacent cette vérification vers la compilation. Une List<String> n'acceptera tout simplement pas un Integer, et get() renvoie déjà un String - pas de conversion, pas de surprise à l'exécution.

Qu'est-ce que l'effacement de type dans les génériques de Java ?

L'effacement de type signifie que l'information de type générique n'existe qu'à la compilation. Une fois compilées, List<String> et List<Integer> ne sont toutes deux qu'une List à l'exécution - le paramètre de type est effacé. C'est pourquoi vous ne pouvez pas écrire new T[10], appeler list instanceof List<String> ni lire un paramètre de type par réflexion. Les génériques offrent une sécurité à la compilation, pas des données de type à l'exécution.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER