Certains disent qu'il s'agit de la relation entre les types et les sous-types, d'autres disent qu'il s'agit de la conversion de type et d'autres encore disent qu'il est utilisé pour décider si une méthode est surchargée ou non.
Toutes ces réponses.
Au fond, ces termes décrivent comment la relation de sous-type est affectée par les transformations de type. Autrement dit, si A
et B
sont des types, f
est une transformation de type, et ≤ la relation de sous-type (c'est-à-dire. A ≤ B
signifie que A
est un sous-type de B
), nous avons
-
f
est covariant si A ≤ B
implique que f(A) ≤ f(B)
-
f
est contravariante si A ≤ B
implique que f(B) ≤ f(A)
-
f
est invariant si aucune des deux conditions ci-dessus n'est remplie.
Prenons un exemple. Soit f(A) = List<A>
où List
est déclaré par
class List<T> { ... }
Est f
covariant, contravariant, ou invariant ? Covariant signifie qu'un List<String>
est un sous-type de List<Object>
, contravariant que a List<Object>
est un sous-type de List<String>
et invariant qu'aucun des deux n'est un sous-type de l'autre, à savoir List<String>
et List<Object>
sont des types inconvertibles. En Java, ce dernier point est vrai, nous disons (de manière quelque peu informelle) que génériques sont invariants.
Un autre exemple. Soit f(A) = A[]
. Est f
covariant, contravariant, ou invariant ? En d'autres termes, String[] est-il un sous-type d'Object[], Object[] un sous-type de String[], ou aucun des deux n'est-il un sous-type de l'autre ? (Réponse : en Java, les tableaux sont covariants).
C'était encore assez abstrait. Pour rendre les choses plus concrètes, voyons quelles opérations en Java sont définies en termes de relation de sous-type. L'exemple le plus simple est l'affectation. L'instruction
x = y;
ne compilera que si typeof(y) ≤ typeof(x)
. C'est-à-dire que nous venons d'apprendre que les déclarations
ArrayList<String> strings = new ArrayList<Object>();
ArrayList<Object> objects = new ArrayList<String>();
ne compilera pas en Java, mais
Object[] objects = new String[1];
volonté.
Un autre exemple où la relation de sous-type est importante est l'expression d'invocation de méthode :
result = method(a);
De manière informelle, cette déclaration est évaluée en assignant la valeur de a
au premier paramètre de la méthode, puis à l'exécution du corps de la méthode, et enfin à l'affectation de la valeur de retour de la méthode à la variable result
. Comme pour l'affectation simple dans le dernier exemple, le "côté droit" doit être un sous-type du "côté gauche", c'est-à-dire que cette déclaration ne peut être valide que si typeof(a) ≤ typeof(parameter(method))
et returntype(method) ≤ typeof(result)
. C'est-à-dire que si la méthode est déclarée par :
Number[] method(ArrayList<Number> list) { ... }
aucune des expressions suivantes ne sera compilée :
Integer[] result = method(new ArrayList<Integer>());
Number[] result = method(new ArrayList<Integer>());
Object[] result = method(new ArrayList<Object>());
mais
Number[] result = method(new ArrayList<Number>());
Object[] result = method(new ArrayList<Number>());
volonté.
Un autre exemple où le sous-typage est important est la surcharge. Pensez-y :
Super sup = new Sub();
Number n = sup.method(1);
où
class Super {
Number method(Number n) { ... }
}
class Sub extends Super {
@Override
Number method(Number n);
}
De manière informelle, le runtime réécrira ceci en :
class Super {
Number method(Number n) {
if (this instanceof Sub) {
return ((Sub) this).method(n); // *
} else {
...
}
}
}
Pour que la ligne marquée compile, le paramètre de méthode de la méthode surchargée doit être un supertype du paramètre de méthode de la méthode surchargée, et le type de retour un sous-type de celui de la méthode surchargée. Formellement parlant, f(A) = parametertype(method asdeclaredin(A))
doit au moins être contravariant, et si f(A) = returntype(method asdeclaredin(A))
doit au moins être covariante.
Notez le "au moins" ci-dessus. Il s'agit d'exigences minimales que tout langage de programmation orienté objet à sécurité statique raisonnable appliquera, mais un langage de programmation peut choisir d'être plus strict. Dans le cas de Java 1.4, les types de paramètres et les types de retour des méthodes doivent être identiques (sauf en ce qui concerne l'effacement des types) lorsque l'on surcharge des méthodes, à savoir parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))
lorsque l'on passe outre. Depuis Java 1.5, les types de retour covariants sont autorisés en cas de surcharge, c'est-à-dire que l'exemple suivant compilera en Java 1.5, mais pas en Java 1.4 :
class Collection {
Iterator iterator() { ... }
}
class List extends Collection {
@Override
ListIterator iterator() { ... }
}
J'espère avoir tout couvert - ou plutôt, avoir gratté la surface. J'espère néanmoins que cela aidera à comprendre le concept abstrait, mais important, de la variance de type.