Les raisons en sont basées sur la manière dont Java implémente les génériques.
Un exemple de tableaux
Avec les tableaux, vous pouvez le faire (les tableaux sont covariants)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Mais que se passerait-il si vous essayez de faire cela?
myNumber[0] = 3.14; //attempt of heap pollution
Cette dernière ligne compilerait très bien, mais si vous exécutez ce code, vous pourriez obtenir un fichier ArrayStoreException
. Parce que vous essayez de mettre un double dans un tableau d'entiers (indépendamment de l'accès via une référence numérique).
Cela signifie que vous pouvez tromper le compilateur, mais vous ne pouvez pas tromper le système de type d'exécution. Et c'est ainsi parce que les tableaux sont ce que nous appelons des types réifiables . Cela signifie qu'au moment de l'exécution, Java sait que ce tableau a en fait été instancié sous la forme d'un tableau d'entiers auquel il se trouve simplement accessible via une référence de type Number[]
.
Donc, comme vous pouvez le voir, une chose est le type réel de l'objet, et une autre chose est le type de référence que vous utilisez pour y accéder, non?
Le problème des génériques Java
Maintenant, le problème avec les types génériques Java est que les informations de type sont ignorées par le compilateur et ne sont pas disponibles au moment de l'exécution. Ce processus est appelé effacement de type . Il y a de bonnes raisons d'implémenter des génériques comme celui-ci en Java, mais c'est une longue histoire, et cela doit être lié, entre autres, à la compatibilité binaire avec du code préexistant (voir Comment nous avons obtenu les génériques que nous avons ).
Mais le point important ici est que puisque, au moment de l'exécution, il n'y a pas d'informations de type, il n'y a aucun moyen de garantir que nous ne commettons pas de pollution en tas.
Par exemple,
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap pollution
Si le compilateur Java ne vous empêche pas de faire cela, le système de type d'exécution ne peut pas non plus vous arrêter, car il n'y a aucun moyen, au moment de l'exécution, de déterminer que cette liste était censée être une liste d'entiers uniquement. Le runtime Java vous permettrait de mettre ce que vous voulez dans cette liste, alors qu'il ne devrait contenir que des entiers, car lors de sa création, il a été déclaré sous forme de liste d'entiers.
En tant que tel, les concepteurs de Java ont veillé à ce que vous ne puissiez pas tromper le compilateur. Si vous ne pouvez pas tromper le compilateur (comme nous pouvons le faire avec les tableaux), vous ne pouvez pas non plus tromper le système de type d'exécution.
En tant que tel, nous disons que les types génériques ne sont pas réifiables .
Évidemment, cela entraverait le polymorphisme. Prenons l'exemple suivant:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Maintenant, vous pouvez l'utiliser comme ceci:
Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};
System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));
Mais si vous essayez d'implémenter le même code avec des collections génériques, vous ne réussirez pas:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Vous obtiendrez des erreurs de compilation si vous essayez de ...
List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error
La solution consiste à apprendre à utiliser deux fonctionnalités puissantes des génériques Java appelées covariance et contravariance.
Covariance
Avec la covariance, vous pouvez lire les éléments d'une structure, mais vous ne pouvez rien y écrire. Ce sont toutes des déclarations valables.
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>();
List<? extends Number> myNums = new ArrayList<Double>();
Et vous pouvez lire à partir de myNums
:
Number n = myNums.get(0);
Parce que vous pouvez être sûr que quel que soit le contenu de la liste réelle, il peut être converti en un nombre (après tout, tout ce qui étend le nombre est un nombre, non?)
Cependant, vous n'êtes pas autorisé à mettre quoi que ce soit dans une structure covariante.
myNumst.add(45L); //compiler error
Cela ne serait pas autorisé, car Java ne peut pas garantir quel est le type réel de l'objet dans la structure générique. Il peut s'agir de tout ce qui étend Number, mais le compilateur ne peut pas en être sûr. Vous pouvez donc lire, mais pas écrire.
Contravariance
Avec la contravariance, vous pouvez faire le contraire. Vous pouvez mettre des choses dans une structure générique, mais vous ne pouvez pas en lire.
List<Object> myObjs = new List<Object>();
myObjs.add("Luke");
myObjs.add("Obi-wan");
List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);
Dans ce cas, la nature réelle de l'objet est une liste d'objets, et par contravariance, vous pouvez y mettre des nombres, essentiellement parce que tous les nombres ont Object comme ancêtre commun. En tant que tel, tous les nombres sont des objets, et donc cela est valide.
Cependant, vous ne pouvez rien lire en toute sécurité dans cette structure contravariante en supposant que vous obtiendrez un nombre.
Number myNum = myNums.get(0); //compiler-error
Comme vous pouvez le voir, si le compilateur vous permettait d'écrire cette ligne, vous obtiendrez une ClassCastException au moment de l'exécution.
Principe Get / Put
En tant que tel, utilisez la covariance lorsque vous avez uniquement l'intention de retirer des valeurs génériques d'une structure, utilisez la contravariance lorsque vous avez uniquement l'intention de placer des valeurs génériques dans une structure et utilisez le type générique exact lorsque vous avez l'intention de faire les deux.
Le meilleur exemple que j'ai est le suivant qui copie n'importe quel type de nombres d'une liste dans une autre liste. Cela ne fait que éléments de la source et ne place que les éléments dans la cible.
public static void copy(List<? extends Number> source, List<? super Number> target) {
for(Number number : source) {
target(number);
}
}
Grâce aux pouvoirs de covariance et de contravariance, cela fonctionne pour un cas comme celui-ci:
List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);