Définitions #
Nous allons rentrer dans les détails de la covariance et la contravariance. Commençons par donner leur définition et leur contexte associé.
Définition générale #
Terme | Définition |
---|---|
covariance | permet de substituer un type par son sous-type |
contravariance | permet de substituer un type par son super-type |
invariance | n’autorise aucune substitution |
Rappel
Si un typeT'
dérive/hérite/est unT
,T
peut référencer unT'
.
Integer
dérive de Number
et cette syntaxe est valide:
|
|
Nous allons nous intéresser, dans le cadre des génériques, aux référencements pour déterminer le type de variance.
Covariance et contravariance avec les types génériques #
Soient un générique G
et un type T'
dérivé de T
, on dit que G
est
Terme | Définition |
---|---|
covariant | si G<T'> est un sous-type de G<T> |
contravariant | si G<T> est un sous-type de G<T'> |
invariant | si aucun des deux n’est vrai |
Que l’on peut illustrer ainsi pour la covariance ou contravariance:
Et pour l’invariance, que l’on peut illustrer à l’aide des deux schémas ci-dessous. Rappelez-vous qu’une List
peut référencer une ArrayList
. Seul le paramètre est pris en considération.
Problématique #
Prenons l’exemple de l’interface Serializable
en Java. Un objet déclaré (qui l’implémente), indique qu’il est possible de le sérialiser et le désérialiser pour l’échanger à travers le réseau. Imaginons que nous souhaitons envoyer une liste d’objets sur un serveur à l’aide du code ci-dessous.
|
|
La méthode sendAll
récupère les éléments pour les envoyer. A priori, il n’y a pas de danger et nous aimerions pouvoir lui passer par exemple une List<String>
ou une List<Integer>
librement. Mais pour l’instant, l’argument sers
est invariant et nous devons transformer nos listes (List<String>
ou List<Integer>
) en une List<Serializable>
.
Une analogie toute simple #
Prenons un instant pour comprendre la problématique avec un exemple très académique. Imaginons que votre voisin vous demande un panier de fruits sans donner plus de précisions. Pouvez-vous lui donner un panier de bananes ? autrement dit, un panier de bananes est-il (hérite-t-il d’) un panier de fruits ? Intuitivement, nous avons envie de dire oui et le panier de fruits serait covariant. Nous pouvons l’exprimer ainsi:
|
|
Cela ne semble pas poser de problème, et ce, du moment où il récupère un fruit, celui-ci peut être une banane.
Mais que se passe-t-il si, pour nous remercier, il nous offre une pomme ?
|
|
Eh bien, il a réussi à rajouter une pomme dans un panier de bananes! Ce qui est problématique: un panier de bananes ne doit contenir que des bananes…
Choix du type de variance #
Cette analogie est utile pour comprendre que le choix de la variance va nous permettre soit de récupérer le paramètre en question (E get()
), soit de le donner (void add(E elem)
).
Dans le jargon, nous nous plaçons toujours du point de vue de la classe générique pour préciser si l’action consomme ou si elle produit.
E get()
signifie que la liste produit un élémentE
void add(E elem)
signifie que la liste consomme un élémentE
.
Mnémonique
PECS: Producer Extends and Consumer Super
Les différents cas de figure pour une classe générique
Foo<T>
quelconque:
Foo<? extends T>
(covariance)
- peut produire un
T
- exemple:
T get();
- peut référencer
Foo<T'>
siT'
est un sous-type deT
Foo<? super T>
(contravariance)
- peut consommer un
T
- exemple:
void add(T e);
- peut référencer
Foo<T'>
siT'
est un super-type deT
Foo<T>
(invariance)
- peut produire et consommer un
T
- ne peut référencer que
Foo<T>
Foo<?>
(covariance générale)
- alias pour
Foo<? extends Object>
- super-type de n’importe quel
Foo
- peut donc référencer n’importe quel
Foo<...>
- ne peut rien consommer
- ne peut rien produire (ou alors que des
Object
s)- exemple :
int size()
La covariance #
La covariance signifie que si une banane est un fruit, alors un panier de bananes est un panier de fruits également, et donc, un panier de fruits peut référencer un panier de bananes tout comme un fruit peut référencer une banane.
Ceci est vrai, uniquement si l’on récupère un fruit de notre référence.
|
|
Conclusion: nous autorisons notre référence covariante (fruits
) à produire un Fruit
, et la consommation devient interdite.
La contravariance #
Pour la contravariance, il s’agit du raisonnement inverse. Si une banane est un fruit alors cette fois-ci c’est le panier de fruits qui est un panier de bananes, et donc, le panier de bananes peut référencer le panier de fruits.
Dans l’exemple ci-dessous, le panier de bananes, représenté par la référence bananas
, ne manipule que des bananes. Nous comprenons cette fois-ci que si nous rajoutons une banane dedans, cela ne va pas corrompre pas l’objet référencé (fruits
) qui peut contenir n’importe quels fruits. Par contre, le danger vient de la récupération d’une banane. Nous ne sommes pas sûrs que ce soit le cas !
|
|
Si la contravariance est moins intuitive, cela signifie qu’un panier de fruits hérite de tous les paniers existants. Autrement dit, un panier de fruits est un panier de bananes, est un panier de pommes, est un panier de cerises…
Conclusion: nous autorisons notre référence contravariante (bananas
) à consommer une Banana
, et la production devient interdite pour notre référence bananas
.
use-site variance vs declaration-site variance #
Java prévoit une syntaxe spéciale pour autoriser la variance lors du référencement. C’est ce qui s’appelle le use-site variance. Ne confondez pas avec la syntaxe des paramètres bornés.
|
|
D’autres langages comme Scala, Kotlin ou C# utilisent le concept de declaration-site variance : celle-ci est précisée lors de la déclaration d’un type générique en indiquant pour chaque paramètre son type de variance.
Extrait d’une déclaration de type en C# #
|
|
Extrait d’une déclaration de type en Scala #
|
|
Tableaux statiques #
Les tableaux statiques sont particuliers. Il ne s’agit pas d’un type générique conventionnel. Nous avons vu qu’ils offrent peu de fonctionnalités. Ils ont un très grand autre désavantage : ils sont covariants par nature et aucune vérification n’est faite à la compilation quant à leurs usages abusifs.
Quiz #
Ecrivez un exemple de code problématique qui démontre le danger de ce choix de conception.