web-dev-qa-db-fra.com

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

From Effective Java de 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 Comme la chaîne est le sous-type de Object So

    String[] is subtype of Object[]

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

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

Ma question est pourquoi la décision de faire des tableaux covariant en Java? Il y a d'autres SO posts tels que Pourquoi les tableaux sont-ils invariants, mais les listes sont-ils covariants? , mais ils semblent se focaliser sur Scala et je ne suis pas capable de suivre.

151
eagertoLearn

Via wikipedia :

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

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 commande Object.equals mé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 serait possible d'appeler ces fonctions uniquement sur un tableau de type Object[]. On ne pourrait pas, par exemple, mélanger un tableau de chaînes.

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

Ceci répond à la question "Pourquoi les tableaux sont-ils covariants?", Ou plus précisément, "Pourquoi des tableaux sont-ils devenus covariants à le temps ? "

Lorsque les génériques ont été introduits, ils n'ont pas été volontairement transformés en covariante pour les raisons indiquées dans cette réponse de Jon Skeet :

Non, un List<Dog> n'est pas un 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 initiale de la mise en covariance des tableaux décrite dans l'article de Wikipédia ne s'appliquait pas aux génériques, car caractères génériques rendait possible l'expression de covariance (et de contravariance), par exemple:

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
144
Paul Bellora

La raison en est que chaque tableau connaît son type d'élément pendant l'exécution, contrairement à la collection générique en raison 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 des 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
27
Katona

Peut être this help: -

Les génériques ne sont pas covariants

Les tableaux du Java langage sont des covariants - ce qui signifie que si Integer étend Number (ce qui est le cas), non seulement un Integer est également un nombre, mais un Integer [] est également un Number[], et vous êtes libre de passer ou d’attribuer un Integer[] où un Number[] est appelé. (Plus formellement, si Number est un supertype d'Integer, alors Number[] est un supertype de Integer[].) Vous pourriez penser qu'il en va de même pour les types génériques - que List<Number> est un supertype de List<Integer>, et que vous pouvez passer un List<Integer> où un List<Number> devrait. Malheureusement, ça ne marche pas comme ça.

Il s’avère que c’est une bonne raison pour que cela ne fonctionne pas ainsi: cela casserait le type que les génériques de sécurité étaient censés fournir. Imaginez que vous puissiez assigner un List<Integer> à un List<Number>. Alors 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 flotteur semble parfaitement légal. Mais si ln avait un alias avec li, alors la promesse de sécurité du type implicite dans la définition de li serait rompue - il s’agit d’une liste d’entiers, ce qui explique pourquoi les types génériques ne peuvent pas être covariants.

20
Rahul Tripathi

Une caractéristique importante des types paramétriques est la possibilité d'écrire des algorithmes polymorphes, c'est-à-dire des algorithmes qui agissent sur une structure de données, quelle que soit la valeur de son paramètre, telle que Arrays.sort().

Avec les génériques, cela se fait avec des types génériques:

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

Pour être vraiment utiles, les types de caractères génériques nécessitent une capture de caractère générique, ce qui nécessite la notion de paramètre de type. Aucune de ces informations n'était disponible au moment où les tableaux ont été ajoutés à Java, et les tableaux contenant un covariant de type référence ont permis de créer un moyen beaucoup plus simple d'autoriser les algorithmes polymorphes:

void sort(Comparable[]);

Cependant, cette simplicité a ouvert une faille dans le système de types 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 types de référence.

En résumé, la nouvelle approche incarnée par les génériques rend le système de types plus complexe, mais également plus sûr en ce qui concerne les types statiques, alors que l’ancienne approche était plus simple et moins sûre en termes de types. Les concepteurs du langage ont opté pour une approche plus simple, ayant des tâches plus importantes à accomplir que la fermeture d’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é pris en charge, ils ont eu les ressources nécessaires pour le faire correctement pour les génériques (mais le changer pour des tableaux aurait brisé l'existant Java programmes).

3
meriton

Les tableaux sont covariants pour au moins deux raisons:

  • Il est utile pour les collections qui contiennent des informations qui ne changeront jamais d'être covariantes. Pour qu'une collection de T soit covariante, son magasin de support doit également être covariant. Bien que l’on puisse concevoir une collection immuable de T qui n’utilise pas de T[] en tant que magasin de sauvegarde (par exemple, en utilisant une arborescence ou une liste chaînée), une telle collection aurait peu de chance de fonctionner aussi bien que celle sauvegardée par un tableau. On pourrait faire valoir qu'un meilleur moyen de fournir des collections covariantes immuables aurait été de définir un type de "tableau immuable covariant" pouvant être utilisé par un magasin d'arrière-plan, mais il était probablement plus simple d'autoriser une covariance de tableau.

  • Les tableaux sont souvent mutés par du code qui ne sait pas quel type de chose va être dans eux, mais ne met pas dans le tableau tout ce qui n'a pas été lu à partir de ce même tableau. Un excellent exemple de ceci est le code de tri. Conceptuellement, il aurait été possible pour les types de tableau d'inclure des méthodes pour permuter ou permuter des éléments (de telles méthodes pourraient s'appliquer de la même manière à tout type de tableau), ou pour définir un objet "manipulateur de tableau" contenant une référence à un tableau et une ou plusieurs choses. cela en avait été lu et pouvait inclure des méthodes pour stocker des éléments lus précédemment dans le tableau d'où ils venaient. Si les tableaux n'étaient pas covariants, le code utilisateur ne pourrait pas définir un tel type, mais l'exécution aurait pu inclure des méthodes spécialisées.

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

3
supercat

Les génériques sont invariants : from JSL 4.10 :

... Les sous-types ne couvrent pas les 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 entre types de tableaux

enter image description here

2
alfasin

Je pense qu'ils ont pris une mauvaise décision à la première place qui a rendu covariant de tableau. Cela casse le type de sécurité tel qu'il est décrit ici et ils sont restés coincés dans cette erreur à cause de la compatibilité avec les versions antérieures et ils ont ensuite essayé de ne pas commettre la même erreur avec générique. Et c’est l’une des raisons pour lesquelles Joshua Bloch préfère les listes aux tableaux de la rubrique 25 du livre "Effective Java (deuxième édition)"

1
Reza

Ma prise en compte: quand 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 à considérer: ce qui se passe lorsque vous lisez un élément de tableau et ce qui se passe si vous écrivez il. Il n’est donc pas difficile d’écrire des règles de langage pour s’assurer que la sécurité du type est préservée dans tous les cas (la règle principale étant qu'un ArrayStoreException puisse être lancé 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 y a plusieurs façons d'utiliser T dans le corps de la classe, et je suppose que c'est bien trop compliqué pour trouver toutes les combinaisons possibles pour écrire des règles indiquant quand les choses sont autorisées et quand ils ne sont pas.

1
ajb