Cours BUT Info

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 Couple sur Object : 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 T

Un 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 :

SyntaxeInterprétation
?n'importe quel type
? extends Ttout sous-type de T — pour lire
? super Ttout 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 : T devient Object + casts implicites — d'où new T(), new T[...] et champs static de type T interdits ;
  • méthodes génériques : type inféré, plus petit ancêtre commun ; diamant <> ;
  • invariance : joker ? extends pour lire, ? super pour écrire.


coursOO_10.pdf

R2.01 · Cours 10 — Programmation générique · 27 diapositives

Consultation seule — téléchargement désactivé

Chargement du document…

Dernière mise à jour le

Sur cette page