Variance

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.

1
2
3
4
static void getMeOneResource(Resource oneResource) { ... }

Resource r = new Room("A402", 50);
getMeOneResource( new Room("A502", 34) );

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.

1
2
3
4
5
6
interface I {
    Serializable test();
}
class A implements I {
    String test() { ... }
}

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.












comments powered by Disqus