Ch. 10 — Programmation générique
Types paramétrés : conventions E/K/V/T, effacement de type et ses interdictions, méthodes génériques et inférence, invariance des collections et jokers ? extends / ? super.
Le problème des couples
On veut des couples de Person et des couples de Book. Trois solutions, deux
mauvaises :
- des classes dédiées
CouplePerson,CoupleBook: invocation immédiate des méthodes propres aux éléments, mais redondance de code et aucun traitement générique possible sur les couples ; - une classe
CouplesurObject: générique et sans redondance, mais un downcast est nécessaire à chaque lecture pour retrouver les méthodes propres — dangereux ; - la bonne : ne pas fixer le type des éléments à l'écriture de la classe, mais à l'instanciation de l'objet — la programmation générique.
Les collections en sont l'exemple permanent : List<E>, HashMap<K,V>,
Comparable<T>… Fixer le type à la création (List<String>, Map<Book, Double>)
renforce le contrôle à la compilation.
Syntaxe et conventions
public class Value<T> {
private T v;
public Value(T v) { this.v = v; }
public T getValue() { return this.v; }
}
Value<Integer> v1 = new Value<Integer>(12);
v1.getValue().intValue(); // pas de downcast, les méthodes sont làLes lettres conventionnelles portent une sémantique qu'il faut respecter — surtout quand
des contraintes s'ajouteront : E (element) dans les collections, K (key), V
(value), N (number), T (type), puis S, U… pour les deuxième, troisième types. La
documentation distingue le type parameter (T dans Foo<T>) du type argument
(String dans Foo<String>). Une classe peut recevoir plusieurs paramètres
(Couple<T,U>, Map<K,V>). Les types primitifs sont exclus : on passe par les wrappers
et l'auto-boxing — Value<Integer>, pas Value<int>. En UML : Value<T>, ou un
compartiment supplémentaire portant le T.
Effacement de type
La compilation d'une classe générique produit une classe non générique où tout T
devient Object, avec des downcasts implicites insérés par le compilateur. À
l'exécution, seule Value existe : v1.getClass() == v2.getClass() est toujours
vrai, quel que soit le type argument — une limitation propre à Java.
D'où les interdictions du cours :
T tmp = new T(); // interdit
T[] tab = new T[100]; // interdit
Value<T>[] vals = new Value<T>[100]; // interdit
// et aucun champ static de type TUn champ static est partagé par toutes les instances — or il n'existe qu'une seule
classe Value après effacement : impossible de lui donner un type paramétré.
Méthodes génériques et inférence
On peut écrire des méthodes génériques dans des classes non génériques : le paramètre de type se déclare avant le type de retour.
public class Selection {
static <T> T select(boolean sel, T e1, T e2) {
return sel ? e1 : e2;
}
}Le type est inféré des arguments : select(true, n1, n2) avec deux Integer
renvoie un Integer, select(false, d1, d2) un Double — et select(true, n1, d2),
qui mélange Integer et Double, renvoie Number, leur plus petit ancêtre commun
(quand il existe). Le diamant <> (Java 7+) évite de répéter le type argument quand le
contexte l'infère : new Value<>(7) — à utiliser intelligemment.
Invariance et jokers
Piège classique : ArrayList<Book> est bien un sous-type de Collection<Book>, mais
ni de ArrayList<Object> ni de Collection<Object>. La généricité est
invariante : une méthode toTrash(Collection<Object> c) n'acceptera jamais une
Collection<Book> — et Collection<Object> ne signifie pas « n'importe quelle
collection pourvu qu'elle contienne des objets », mais exactement « une collection
d'éléments de type Object ».
Le joker ? désigne « n'importe quel type » : une variable Value<?> accepte
tour à tour une Value<Integer>, une Value<Double>… là où une Value<Integer> ne
satisfait à rien d'autre. Mais le joker verrouille la structure en modification :
al2.add(...) ne compile plus sur une ArrayList<?>. On borne le joker dans un sens ou
dans l'autre :
| Syntaxe | Interprétation |
|---|---|
? | n'importe quel type |
? extends T | tout sous-type de T — pour lire |
? super T | tout super-type de T — pour écrire |
static void toRecycle(Collection<? extends Recyclable> c) {
for (Recyclable r : c) { r.recycle(); }
}toRecycle accepte désormais une ArrayList<Bottle> — notez que extends exprime ici
le polymorphisme de type, pas l'héritage. On retrouve les deux bornes dans l'API :
addAll(Collection<? extends E>) en lecture,
sort(List<T>, Comparator<? super T>) en écriture, et même des combinaisons de bornes
dans Collections.max.
La règle producteur / consommateur
On lit dans une structure déclarée ? extends T (elle produit des T) et on
écrit dans une structure ? super T (elle consomme des T). Inverser les deux est
refusé à la compilation — c'est justement son rôle.
A retenir
- le type se fixe à l'instanciation : la sûreté à la compilation, sans duplication ;
- effacement de type :
TdevientObject+ casts implicites — d'oùnew T(),new T[...]et champsstaticde typeTinterdits ; - méthodes génériques : type inféré, plus petit ancêtre commun ; diamant
<>; - invariance : joker
? extendspour lire,? superpour écrire.
Parce qu'à l'exécution, après effacement, T n'existe plus : la classe est compilée
en new Object() pour tous les usages. Le compilateur ne peut donc ni vérifier quel
constructeur appeler, ni allouer le bon type — il refuse.
Pour garantir le typage : si c'était permis, on pourrait insérer un Person dans une
liste censée ne contenir que des Book. L'invariance protège la collection —
le joker ? extends exprime proprement « une collection de quelque chose qui est un
Book ou plus spécifique ».
Le joker dit « n'importe quel type » : le compilateur ne peut prouver que l'élément
ajouté a le bon type pour cette liste précise. Il bloque donc l'ajout (et la
modification) — seule la lecture en Object reste possible.
Toujours true en comparaison : l'effacement de type ne garde qu'une seule
classe Value à l'exécution, les paramètres étant remplacés par Object et les
casts insérés par le compilateur. Les génériques n'existent qu'à la compilation.
coursOO_10.pdf
R2.01 · Cours 10 — Programmation générique · 27 diapositives
Chargement du document…
Dernière mise à jour le
Ch. 9 — Entrées / sorties
Les flux de java.io : lire la nomenclature Reader/Writer et InputStream/OutputStream, tamponner, écrire avec PrintWriter, sérialiser des objets et analyser avec Scanner.
TD 8 à 10 — Révisions
Trois TD de révision de fin de semestre : un Student itérable et comparable, premières lectures/écritures de fichiers avec exceptions personnalisées, et les objets génériques jusqu'au jeu de la Mourre.