Restrictions des Generics Java
Ce que vous ne pouvez pas faire avec les generics Java : pas de primitifs, pas de paramètres de type statiques, pas de tableaux génériques, et plus.
Ce chapitre est le catalogue des choses que vous ne pouvez pas faire avec les generics Java, avec une brève explication des raisons pour lesquelles chacune est interdite. Presque toutes les restrictions découlent d'un seul fait que vous avez vu dans le chapitre précédent : le paramètre de type est effacé avant l'émission du bytecode, donc tout ce qui nécessite que le paramètre existe à l'exécution ne fonctionnera pas. Lisez ce chapitre comme la fiche de référence finale pour cette partie — c'est la liste des moments "j'ai essayé, le compilateur s'est plaint" regroupés sur une seule page.
1. Pas de primitifs comme arguments de type
List<int> ints = new ArrayList<>(); // ❌
List<Integer> ints = new ArrayList<>(); // ✓La forme effacée de List<E> au niveau du bytecode stocke ses éléments en tant qu'Object, et les primitifs ne sont pas des Object. La solution est la classe wrapper correspondante — Integer, Long, Double, Boolean, Character, etc. L'autoboxing comble ensuite l'écart : ints.add(5) et int x = ints.get(0) fonctionnent tous les deux, au prix que chaque élément paie pour un objet Integer sur le tas.
Project Valhalla est l'effort de longue haleine pour faire fonctionner réellement List<int>, via les types valeur et les generics spécialisés. Depuis Java 25, ce n'est pas encore là.
2. Pas de new T()
public class Box<T> {
public T newInstance() { return new T(); } // ❌
}À l'exécution, il n'y a pas de T — la JVM n'a que Object (ou la borne). Elle n'a pas d'objet de classe pour appeler un constructeur, aucun moyen de savoir quel constructeur appeler. La solution standard est de prendre une factory en paramètre :
public class Box<T> {
public T newInstance(Supplier<T> factory) { return factory.get(); }
}
Box<String> b = new Box<>();
String fresh = b.newInstance(String::new);Le Supplier<T> transporte la factory réelle à l'exécution, d'une manière que le paramètre de type n'aurait jamais pu faire.
3. Pas de T.class ni de instanceof T
public <T> boolean isIt(Object o) {
return o instanceof T; // ❌
}
public <T> Class<T> klass() {
return T.class; // ❌
}Encore une fois, pas de T à l'exécution. La solution dans les deux cas est de passer le jeton Class<T> en argument :
public <T> boolean isIt(Object o, Class<T> type) {
return type.isInstance(o);
}Class.isInstance(Object) est la forme réflexive de instanceof, et elle fonctionne avec l'objet Class à l'exécution que vous avez fourni. La bibliothèque standard fait cela partout — Collections.checkedList(List<E>, Class<E>), EnumSet.noneOf(Class<E>), les désérialiseurs JSON, et ainsi de suite.
4. Pas de tableaux d'un type générique
T[] arr = new T[10]; // ❌ — generic array creation
List<String>[] lists = new List<String>[10]; // ❌ — sameCelui-ci est plus subtil. Les tableaux en Java sont réifiés — un Integer[] sait à l'exécution qu'il est un Integer[], et les stockages seront vérifiés. Mais les generics sont effacés — la JVM ne peut pas distinguer List<String>[] de List<Integer>[]. Si ces deux restrictions n'étaient pas appliquées, vous pourriez corrompre le tas avec quelques lignes :
List<String>[] strs = new List<String>[1]; // pretend this is legal
Object[] objs = strs; // arrays are covariant
objs[0] = List.of(42); // stores an Integer list
String s = strs[0].get(0); // KABOOMLe compilateur refuse la création de tableaux génériques plutôt que de laisser cela se produire.
Solutions de contournement :
- Utiliser
(T[]) new Object[n]avec un@SuppressWarnings("unchecked")(vous avez vu cela dans la Stack générique précédemment). Sûr si le tableau est interne et que vous ne le laissez jamais fuir en tant queT[]. - Ou utiliser simplement une
List<T>au lieu d'un tableau. Dans neuf cas sur dix, c'est la bonne réponse.
5. Pas de champs statiques d'un paramètre de type
public class Box<T> {
private static T defaultValue; // ❌
public static T empty() { ... } // ❌
}Le paramètre de type appartient à une instance — chaque Box<...> porte son propre T. Les membres statiques appartiennent à la classe elle-même, qui n'a pas de T. Les deux portées ne se connectent pas.
Si vous avez besoin d'une méthode statique polymorphe sur un type, déclarez son propre paramètre de type (nous avons couvert cela dans les méthodes génériques) :
public class Box<T> {
public static <U> Box<U> empty() { return new Box<>(null); }
}<U> est local à la méthode — indépendant de tout T au niveau de la classe.
6. Pas de types d'exception génériques
public class MyException<T> extends Exception { ... } // ❌Les tables de gestion des exceptions de la JVM recherchent les blocs catch par classe effacée. Si deux types d'exception génériques différents s'effaçaient tous les deux vers la même classe, un catch (MyException<String> e) attraperait également un MyException<Integer> — ce qui corromprait silencieusement le système de types. Plutôt que d'essayer de faire fonctionner cela, Java interdit la déclaration de façon catégorique. Vous ne pouvez pas non plus avoir un paramètre de type générique dans une clause catch :
try { ... } catch (T e) { ... } // ❌Si votre exception doit vraiment transporter un contenu typé, stockez le contenu en tant que champ générique sur une exception non générique :
public class TaggedException extends Exception {
public final Object payload;
public TaggedException(String message, Object payload) {
super(message);
this.payload = payload;
}
}Ou déclarez le site de lancement de manière étroite et réservez les contenus typés aux chemins de retour normaux.
7. Pas de surcharges qui diffèrent uniquement par des paramètres génériques
public void process(List<String> list) { ... }
public void process(List<Integer> list) { ... } // ❌ — both erase to process(List)Après effacement, les deux méthodes ont la signature process(List). Java traite la résolution des surcharges par les signatures effacées, donc il ne peut pas distinguer les deux. La solution est de leur donner des noms différents — processStrings et processInts — ou d'accepter une List<Object> et de vérifier à l'exécution.
8. Les types génériques sont invariants
Ce n'est pas une règle "le compilateur rejette cela", c'est une règle "le compilateur rejette ce que vous vous attendiez à être légal", et nous l'avons couverte en détail dans les Wildcards :
List<Integer> ints = ...;
List<Number> nums = ints; // ❌ — generic types are invariant
List<? extends Number> nums = ints; // ✓ — wildcard restores the flexibilityVaut la peine de connaître car c'est la restriction que vous rencontrerez le plus souvent. Les wildcards sont la soupape de décompression.
9. Pas de types enum génériques
public enum Box<T> { // ❌
EMPTY, FULL;
T value;
}Les enums sont traduits en une seule classe avec un ensemble fixe de constantes — il n'y a aucun moyen pour les constantes de partager un T unique et sensé. La solution est généralement de rendre les méthodes génériques, pas l'enum lui-même :
public enum Box {
EMPTY, FULL;
public <T> T orDefault(T fallback) { return this == FULL ? null : fallback; }
}Ou, si chaque constante veut vraiment son propre type, utilisez une hiérarchie de classes non-enum et une Map<Name, Box>.
10. Appeler une méthode générique via un type brut
List rawList = new ArrayList();
rawList.add("hi"); // unchecked-warning, but allowed
List<String> typed = rawList; // unchecked-warning, dangerousMélanger des types bruts et des types génériques désactive toutes les vérifications à la compilation des generics pour cette variable. Le compilateur avertira (Unchecked call to add(E) as a member of raw type java.util.List), et ignorer l'avertissement vous redonne le problème d'avant Java 5 — des valeurs du mauvais type introduites silencieusement, qui explosent à la prochaine lecture.
Les types bruts existent pour la compatibilité ascendante, pas comme une fonctionnalité. Traitez l'avertissement comme une erreur dans tout nouveau code.
Un exemple concret : chaque restriction, côte à côte
Le programme ci-dessous essaie de faire chacune des choses que les règles interdisent (commentées pour que le fichier compile), puis montre la solution de contournement canonique pour chacune. Lisez les commentaires — ils correspondent un à un aux restrictions numérotées ci-dessus.
Chaque restriction numérotée correspond à une solution de contournement en une ligne dans le programme. Le schéma commun à toutes est le même : tout ce qui veut le paramètre de type à l'exécution reçoit les informations explicitement — un jeton Class<T>, un Supplier<T>, un paramètre de type au niveau de la méthode, un wildcard. L'effacement a supprimé la forme implicite ; vous la restituez à la frontière de l'API.
Cela clôt la Partie 10
Les generics constituent la fonctionnalité unique la plus profonde du langage en dehors de la JVM elle-même. Vous disposez maintenant du vocabulaire fonctionnel : paramètres de type sur les classes, méthodes et interfaces ; bornes ; wildcards et PECS ; effacement ; et le catalogue des restrictions que l'effacement impose sur la conception. Chaque API Java moderne est façonnée par ces règles, et lire du code de bibliothèque (ou concevoir le vôtre) est beaucoup plus facile avec ce modèle en tête.
Et après
Les generics ne sont pas une fin en soi — ils existent parce que Java avait besoin d'un moyen d'exprimer "conteneur de T" sans copier-coller une classe par type d'élément. La prochaine partie du livre est l'endroit où toute la machinerie générique de cette partie vous préparait secrètement : le Collections Framework. List, Set, Map, Queue, et les dizaines d'implémentations derrière elles — toutes paramétrées, toutes conçues autour des règles que vous venez d'apprendre. Continuez vers Introduction aux Collections Java.