Variance #
Nous avons vu que le sous-typage permet de substituer un type par un sous-type. Si Room
est un sous-type de Resource
, une expression qui retourne une Room
peut être employée partout où un type Resource
est attendu. C’est ce que l’on a appelé le polymorphisme de sous-typage.
|
|
La variance permet de déterminer le sous-typage lors de situations plus complexes comme l’exemple ci-dessous où une redéfinition peut retourner un sous-type du type de retour de la définition d’origine. Nous précisons que le type de retour d’une méthode est covariant.
|
|
Type générique #
La variance est rencontrée plus couramment sur les types génériques et c’est ce qui va nous intéresser dans cette section.
Voici les trois différents types de variances:
- la covariance qui permet de substituer un type par son sous-type
- la contravariance qui permet de substituer un type par son super-type
- l’invariance qui n’autorise aucune substitution
Si la covariance est relativement intuitive (si ce n’est pas le cas, revoyez le chapitre sur le poylmorphisme ), ça l’est généralement moins pour la contravariance.
Mais commençons par l’invariance. En Java, les génériques sont invariants. C’est-à-dire que le paramètre ne peut pas changer. Une ArrayList
peut substituer une List
mais le paramètre doit rester le même :
List<Number> numbers = new ArrayList<Number>();
Le code ci-dessous ne compile donc pas:
List<Number> numbers = new ArrayList<Integer>();
Et d’ailleurs, c’est grâce à cette propriété que le paramètre de ArrayList
est forcément le même que celui de List
et il devient superflu:
List<Number> numbers = new ArrayList<Number>();
// peut être écrit ainsi:
List<Number> numbers = new ArrayList<>();
Si les génériques sont invariants, à quoi sert de continuer le chapitre me direz-vous ? Et bien, il existe plein de situations où nous souhaiterions alléger cette contrainte. Il nous serait parfois utile de dire qu’une liste d’entier est une liste de nombres, ou qu’un ensemble d’imprimantes équivaut à un ensemble de périphériques.
Java offre un mécanisme pour permettre la variance. Si un type générique est invariant de par sa définition, il est possible d’autoriser la variance lors du référencement.
Prenons l’exemple de la méthode addAll(Collection<E> c)
sur List<E>
et supposons qu’elle soit invariante. Si nous avons une référence de type List<Number>
, la méthode deviendrait addAll(Collection<Number> c)
et il deviendrait impossible de lui passer une ArrayList<Integer>
par exemple, ce qui est ennuyeux. Cela obligerait l’utilisateur à transférer tous les éléments de sa liste dans une ArrayList<Number>
!
Ce que nous souhaitons faire:
List<Integer> ints = List.of(1,2,3);
...
List<Number> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(1.0);
...
numbers.addAll(ints);
Pourtant le code ci-dessus fonctionne. L’argument est de type Collection<Number>
, mais nous avons pu lui passer une ArrayList<Integer>
qui revient à faire ceci:
List<Integer> ints = List.of(1,2,3);
List<Number> arguments = ints; // ne compile pas !
...
List<Number> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(1.0);
...
numbers.addAll(arguments);
List
et ArrayList
sont invariants, mais il est possible d’autoriser la variance lors du référencement. C’est le cas de addAll
qui est en fait covariant:
boolean addAll(Collection<? extends E> c)
La syntaxe <? extends E>
autorise le référencement covariant, alors que <? super E>
autorise le référencement contravariant. Avant de rentrer dans les détails, voyez à quel point cette syntaxe est utilisée pour nous faciliter la vie. En voici un extrait tiré uniquement de ArrayList
:
ArrayList(Collection<? extends E> c)
boolean addAll(Collection<? extends E> c)
void forEach(Consumer<? super E> action)
boolean removeIf(Predicate<? super E> filter)
boolean removeAll(Collection<?> c)
En reprenant l’exemple plus haut, le code ci-dessous fonctionne:
List<Integer> ints = List.of(1,2,3);
List<? extends Number> arguments = ints; // OK !
...
List<Number> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(1.0);
...
numbers.addAll(arguments);
Nous indiquons ici que arguments
est une référence covariante. C’est-à-dire la référence est covariante et non la déclaration de List
ou ArrayList
.