Pourquoi les tableaux sont-ils covariants alors que les génériques sont invariants?

160

Tiré de Effective Java par Joshua Bloch,

  1. Les tableaux diffèrent du type générique de deux manières importantes. Les premiers tableaux sont covariants. Les génériques sont invariants.
  2. Covariant signifie simplement que si X est un sous-type de Y, alors X [] sera également un sous-type de Y []. Les tableaux sont covariants Car string est le sous-type de Object So

    String[] is subtype of Object[]

    Invariant signifie simplement que X soit le sous-type de Y ou non,

     List<X> will not be subType of List<Y>.

Ma question est pourquoi la décision de rendre les tableaux covariants en Java? Il existe d'autres articles SO tels que Pourquoi les tableaux sont invariants, mais les listes covariantes? , mais ils semblent se concentrer sur Scala et je ne suis pas en mesure de suivre.

eagertoLearn
la source
1
N'est-ce pas parce que les génériques ont été ajoutés plus tard?
Sotirios Delimanolis
1
Je pense que la comparaison entre les tableaux et les collections est injuste, les collections utilisent des tableaux en arrière-plan !!
Ahmed Adel Ismail
4
@ EL-conteDe-monteTereBentikh Pas toutes les collections, par exemple LinkedList.
Paul Bellora
@PaulBellora je sais que les cartes sont différentes des implémenteurs de collections, mais j'ai lu dans le SCPJ6 que les collections reposaient généralement sur des tableaux !!
Ahmed Adel Ismail
Parce qu'il n'y a aucune ArrayStoreException; lors de l'insertion d'un élément incorrect dans Collection où le tableau l'a. Ainsi, Collection ne peut trouver cela qu'au moment de la récupération et cela aussi à cause du casting. Les génériques garantiront donc la résolution de ce problème.
Kanagavelu Sugumar

Réponses:

151

Via wikipedia :

Les premières versions de Java et C # n'incluaient pas de génériques (aka polymorphisme paramétrique).

Dans un tel contexte, rendre les tableaux invariants exclut les programmes polymorphes utiles. Par exemple, envisagez d'écrire une fonction pour mélanger un tableau ou une fonction qui teste l'égalité de deux tableaux à l'aide de la Object.equalsméthode sur les éléments. L'implémentation ne dépend pas du type exact d'élément stocké dans le tableau, il devrait donc être possible d'écrire une seule fonction qui fonctionne sur tous les types de tableaux. Il est facile d'implémenter des fonctions de type

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

Cependant, si les types de tableaux étaient traités comme invariants, il ne serait possible d'appeler ces fonctions que sur un tableau du type exact Object[]. On ne pouvait pas, par exemple, mélanger un tableau de chaînes.

Par conséquent, Java et C # traitent les types de tableaux de manière covariante. Par exemple, en C # string[]est un sous-type de object[], et en Java String[]est un sous-type de Object[].

Cela répond à la question "Pourquoi les tableaux sont-ils covariants?", Ou plus précisément, "Pourquoi les tableaux ont- ils été rendus covariants à l'époque ?"

Lorsque les génériques ont été introduits, ils n'ont pas été délibérément rendus covariants pour les raisons exposées dans cette réponse de Jon Skeet :

Non, a List<Dog>n'est pas a List<Animal>. Considérez ce que vous pouvez faire avec un List<Animal>- vous pouvez y ajouter n'importe quel animal ... y compris un chat. Maintenant, pouvez-vous logiquement ajouter un chat à une portée de chiots? Absolument pas.

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

Soudain, vous avez un chat très confus.

La motivation originale pour rendre les tableaux covariants décrite dans l'article de wikipedia ne s'appliquait pas aux génériques car les jokers rendaient possible l'expression de la covariance (et de la contravariance), par exemple:

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
Paul Bellora
la source
3
oui, Arrays permet un comportement polymorphe, cependant, il introduit des excpetions d'exécution (contrairement aux exceptions à la compilation avec des génériques). Par exemple:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagertoLearn le
21
C'est correct. Les tableaux ont des types réifiables et lancent des ArrayStoreExceptions si nécessaire. À l’époque, cela était clairement considéré comme un bon compromis. Comparez cela avec aujourd'hui: beaucoup considèrent la covariance de tableau comme une erreur, rétrospectivement.
Paul Bellora
1
Pourquoi «beaucoup» considèrent-ils cela comme une erreur? C'est beaucoup plus utile que de ne pas avoir de covariance de tableau. Combien de fois avez-vous vu une ArrayStoreException; ils sont assez rares. L'ironie ici est imo impardonnable ... l'une des pires erreurs jamais commises en Java est la variance du site d'utilisation, c'est-à-dire les jokers.
Scott
3
@ScottMcKinney: "Pourquoi" beaucoup "considèrent-ils cela comme une erreur?" AIUI, c'est parce que la covariance de tableau nécessite des tests de type dynamique sur toutes les opérations d'affectation de tableau (bien que les optimisations du compilateur puissent peut-être aider?), Ce qui peut entraîner une surcharge d'exécution importante.
Dominique Devriese
Merci, Dominique, mais d'après mon observation, il semble que la raison pour laquelle "beaucoup" considèrent que c'est une erreur est plus proche de ce que quelques autres ont dit. Encore une fois, en jetant un regard neuf sur la covariance de tableau, c'est beaucoup plus utile que dommageable. Encore une fois, la GRANDE erreur que Java a commise était la variance générique du site d'utilisation via des caractères génériques. Cela a causé plus de problèmes que je ne pense que "beaucoup" veulent l'admettre.
Scott
30

La raison en est que chaque tableau connaît son type d'élément pendant l'exécution, contrairement à la collection générique à cause de l'effacement du type.

Par exemple:

String[] strings = new String[2];
Object[] objects = strings;  // valid, String[] is Object[]
objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime

Si cela était autorisé avec les collections génériques:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;  // let's say it is valid
objects.add(12);  // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this

Mais cela poserait des problèmes plus tard lorsque quelqu'un essaierait d'accéder à la liste:

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
Katona
la source
Je pense que la réponse de Paul Bellora est plus appropriée, car il commente pourquoi les tableaux sont covariants. Si les tableaux ont été rendus invariants, alors c'est très bien. vous auriez un effacement de type avec. La principale raison de la propriété de type Erasure est la compatibilité descendante correcte?
eagertoLearn le
@ user2708477, oui, l'effacement de type a été introduit en raison de la compatibilité descendante. Et oui, ma réponse tente de répondre à la question du titre, pourquoi les génériques sont invariants.
Katona
Le fait que les tableaux connaissent leur type signifie que si la covariance permet au code de demander à stocker quelque chose dans un tableau où il ne rentre pas, cela ne signifie pas qu'un tel stockage sera autorisé à avoir lieu. Par conséquent, le niveau de danger introduit par le fait que les tableaux soient covariants est bien inférieur à ce qu'il serait s'ils ne connaissaient pas leurs types.
supercat du
@supercat, correct, ce que je voulais souligner, c'est que pour les génériques avec effacement de type en place, la covariance n'aurait pas pu être mise en œuvre avec la sécurité minimale des vérifications d'exécution
Katona
1
Je pense personnellement que cette réponse fournit la bonne explication quant à la raison pour laquelle les tableaux sont covariants alors que les collections ne peuvent pas l'être. Merci!
asgs
23

Peut-être cette aide: -

Les génériques ne sont pas covariants

Les tableaux dans le langage Java sont covariants - ce qui signifie que si Integer étend Number (ce qu'il fait), non seulement un Integer est également un Number, mais un Integer [] est également un Number[], et vous êtes libre de passer ou d'attribuer un Integer[]où a Number[]est appelé. (Plus formellement, si Number est un supertype de Integer, alors Number[]est un supertype of Integer[].) Vous pourriez penser qu'il en va de même pour les types génériques - c'est List<Number>un supertype de List<Integer>, et que vous pouvez passer a List<Integer>où a List<Number>est attendu. Malheureusement, cela ne fonctionne pas de cette façon.

Il s'avère qu'il y a une bonne raison pour laquelle cela ne fonctionne pas de cette façon: cela briserait le type de sécurité que les génériques étaient censés fournir. Imaginez que vous puissiez attribuer un List<Integer>à un fichier List<Number>. Ensuite, le code suivant vous permettrait de mettre quelque chose qui n'était pas un entier dans un List<Integer>:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

Parce que ln est un List<Number>, y ajouter un Float semble parfaitement légal. Mais si ln avait un alias avec li, alors cela briserait la promesse de sécurité de type implicite dans la définition de li - que c'est une liste d'entiers, c'est pourquoi les types génériques ne peuvent pas être covariants.

Rahul Tripathi
la source
3
Pour les tableaux, vous obtenez un ArrayStoreExceptionà l'exécution.
Sotirios Delimanolis
4
ma question est de savoir si les WHYtableaux sont rendus covariants. comme Sotirios l'a mentionné, avec les tableaux, on obtiendrait ArrayStoreException au moment de l'exécution, si les tableaux étaient rendus invariants, alors nous pourrions détecter cette erreur au moment de la compilation elle-même correcte?
eagertoLearn le
@eagertoLearn: Une faiblesse sémantique majeure de Java est que rien dans son système de types ne peut distinguer "Array qui ne contient que des dérivés de Animal, qui n'a pas à accepter les éléments reçus d'ailleurs" de "Array qui ne doit contenir que Animal, et doit être prêt à accepter des références externes à Animal. Le code qui a besoin du premier devrait accepter un tableau de Cat, mais le code qui a besoin du second ne devrait pas. Si le compilateur pouvait distinguer les deux types, il pourrait fournir une vérification à la compilation. Malheureusement, la seule chose qui les distingue ...
supercat
... est de savoir si le code essaie réellement d'y stocker quoi que ce soit, et il n'y a aucun moyen de le savoir avant l'exécution.
supercat le
3

Les tableaux sont covariants pour au moins deux raisons:

  • Il est utile pour les collections contenant des informations qui ne changeront jamais en covariantes. Pour qu'une collection de T soit covariante, son magasin de support doit également être covariant. Bien que l'on puisse concevoir une Tcollection immuable qui n'utilise pas a T[]comme magasin de stockage (par exemple en utilisant une arborescence ou une liste chaînée), une telle collection aurait peu de chances de fonctionner aussi bien qu'une collection soutenue par un tableau. On pourrait argumenter qu'une meilleure façon de fournir des collections immuables covariantes aurait été de définir un type de "tableau immuable covariant" qu'ils pourraient utiliser un magasin de sauvegarde, mais autoriser simplement la covariance de tableau était probablement plus facile.

  • Les tableaux seront fréquemment mutés par du code qui ne sait pas quel type de chose va être dedans, mais ne mettra pas dans le tableau tout ce qui n'a pas été lu dans ce même tableau. Un exemple typique de ceci est le code de tri. Conceptuellement, il aurait pu être possible pour les types de tableaux d'inclure des méthodes pour permuter ou permuter des éléments (ces méthodes pourraient être également applicables à n'importe quel type de tableau), ou de définir un objet "manipulateur de tableau" contenant une référence à un tableau et une ou plusieurs choses qui avaient été lus à partir de celui-ci, et pourraient inclure des méthodes pour stocker les éléments précédemment lus dans le tableau d'où ils provenaient. Si les tableaux n'étaient pas covariants, le code utilisateur ne pourrait pas définir un tel type, mais le runtime aurait pu inclure des méthodes spécialisées.

Le fait que les tableaux soient covariants peut être considéré comme un horrible hack, mais dans la plupart des cas, il facilite la création de code fonctionnel.

supercat
la source
1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.- bon point
eagertoApprendre le
3

Une caractéristique importante des types paramétriques est la capacité d'écrire des algorithmes polymorphes, c'est-à-dire des algorithmes qui fonctionnent sur une structure de données indépendamment de sa valeur de paramètre, comme Arrays.sort().

Avec les génériques, c'est fait avec des types génériques:

<E extends Comparable<E>> void sort(E[]);

Pour être vraiment utiles, les types génériques nécessitent une capture générique, ce qui nécessite la notion de paramètre de type. Rien de tout cela n'était disponible au moment où les tableaux ont été ajoutés à Java, et la création de tableaux de type covariant de référence permettait un moyen beaucoup plus simple d'autoriser des algorithmes polymorphes:

void sort(Comparable[]);

Cependant, cette simplicité a ouvert une faille dans le système de type statique:

String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException

nécessitant une vérification à l'exécution de chaque accès en écriture à un tableau de type référence.

En un mot, la nouvelle approche incarnée par les génériques rend le système de type plus complexe, mais aussi plus sûr de type statique, tandis que l'ancienne approche était plus simple et moins sûre de type statique. Les concepteurs du langage ont opté pour une approche plus simple, ayant des choses plus importantes à faire que de fermer une petite faille dans le système de typage qui pose rarement des problèmes. Plus tard, lorsque Java a été établi et que les besoins urgents ont été satisfaits, ils ont eu les ressources pour le faire correctement pour les génériques (mais le changer pour des tableaux aurait cassé les programmes Java existants).

meriton
la source
2

Les génériques sont invariants : à partir de JSL 4.10 :

... Le sous-typage ne s’étend pas aux types génériques: T <: U n’implique pas que C<T><: C<U>...

et quelques lignes plus loin, JLS explique également que les
tableaux sont covariants (première puce):

4.10.3 Sous-typage parmi les types de tableaux

entrez la description de l'image ici

Alfasin
la source
2

Je pense qu'ils ont pris une mauvaise décision au départ, ce qui a rendu le tableau covariant. Cela brise la sécurité de type telle qu'elle est décrite ici et ils sont restés bloqués avec cela à cause de la compatibilité descendante et après cela, ils ont essayé de ne pas faire la même erreur pour le générique. Et c'est l'une des raisons pour lesquelles Joshua Bloch préfère les listes aux tableaux du point 25 du livre "Effective Java (deuxième édition)"

Arnold
la source
Josh Block était l'auteur du framework de collections Java (1.2), et l'auteur des génériques de Java (1.5). Donc, le gars qui a construit les génériques dont tout le monde se plaint est aussi par coïncidence celui qui a écrit le livre en disant qu'ils sont la meilleure voie à suivre? Pas une énorme surprise!
cpurdy
1

Ma prise: lorsque le code attend un tableau A [] et que vous lui donnez B [] où B est une sous-classe de A, il n'y a que deux choses à s'inquiéter: que se passe-t-il lorsque vous lisez un élément de tableau, et que se passe-t-il si vous écrivez il. Il n'est donc pas difficile d'écrire des règles de langage pour s'assurer que la sécurité des types est préservée dans tous les cas (la règle principale étant qu'un an ArrayStoreExceptionpeut être jeté si vous essayez de coller un A dans un B []). Pour un générique, cependant, lorsque vous déclarez une classe SomeClass<T>, il peut y avoir un certain nombre de façons d' Tutiliser dans le corps de la classe, et je suppose que c'est trop compliqué de travailler sur toutes les combinaisons possibles pour écrire des règles sur quand les choses sont autorisées et quand elles ne le sont pas.

ajb
la source