Warum sind Arrays kovariant, Generika jedoch invariant?

164

Aus Effective Java von Joshua Bloch,

  1. Arrays unterscheiden sich vom generischen Typ in zwei wichtigen Punkten. Erste Arrays sind kovariant. Generika sind unveränderlich.
  2. Kovariante bedeutet einfach, wenn X der Subtyp von Y ist, dann ist X [] auch der Subtyp von Y []. Arrays sind kovariant, da der String der Subtyp von Object So ist

    String[] is subtype of Object[]

    Invariante bedeutet einfach, unabhängig davon, ob X ein Subtyp von Y ist oder nicht.

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

Meine Frage ist, warum die Entscheidung, Arrays in Java kovariant zu machen? Es gibt andere SO-Posts wie Warum sind Arrays invariant, aber Listen kovariant? , aber sie scheinen sich auf Scala zu konzentrieren und ich kann nicht folgen.

eagertoLearn
quelle
1
Liegt das nicht daran, dass Generika später hinzugefügt wurden?
Sotirios Delimanolis
1
Ich denke, der Vergleich zwischen Arrays und Sammlungen ist unfair. Sammlungen verwenden Arrays im Hintergrund !!
Ahmed Adel Ismail
4
@ EL-conteDe-monteTereBentikh Zum Beispiel nicht alle Sammlungen LinkedList.
Paul Bellora
@PaulBellora Ich weiß, dass Maps anders sind als Collection-Implementierer, aber ich habe im SCPJ6 gelesen, dass Collections im Allgemeinen auf Arrays basieren !!
Ahmed Adel Ismail
Weil es keine ArrayStoreException gibt; beim Einfügen eines falschen Elements in Collection, wo es als Array vorhanden ist. Collection kann dies also nur zum Zeitpunkt des Abrufs finden und das auch aufgrund von Casting. Generika sorgen also für die Lösung dieses Problems.
Kanagavelu Sugumar

Antworten:

154

Über Wikipedia :

Frühere Versionen von Java und C # enthielten keine Generika (auch bekannt als parametrischer Polymorphismus).

In einer solchen Einstellung schließt das Invarianten von Arrays nützliche polymorphe Programme aus. Sie können beispielsweise eine Funktion schreiben, um ein Array zu mischen, oder eine Funktion, die zwei Arrays mithilfe der Object.equalsMethode für die Elemente auf Gleichheit testet . Die Implementierung hängt nicht vom genauen Elementtyp ab, der im Array gespeichert ist. Daher sollte es möglich sein, eine einzelne Funktion zu schreiben, die für alle Arten von Arrays funktioniert. Es ist einfach, Funktionen vom Typ zu implementieren

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

Wenn Array-Typen jedoch als invariant behandelt würden, könnten diese Funktionen nur für ein Array mit genau dem Typ aufgerufen werden Object[]. Man konnte zum Beispiel eine Reihe von Strings nicht mischen.

Daher behandeln sowohl Java als auch C # Array-Typen kovariant. Zum Beispiel ist in C # string[]ein Subtyp von object[]und in Java String[]ein Subtyp von Object[].

Dies beantwortet die Frage "Warum sind Arrays kovariant?" Oder genauer: "Warum wurden Arrays zu dieser Zeit kovariant gemacht ?"

Als Generika eingeführt wurden, wurden sie aus den in dieser Antwort von Jon Skeet genannten Gründen absichtlich nicht kovariant gemacht :

Nein, a List<Dog>ist nicht a List<Animal>. Überlegen Sie, was Sie mit einem tun können List<Animal>- Sie können jedes Tier hinzufügen ... einschließlich einer Katze. Können Sie einem Wurf Welpen logisch eine Katze hinzufügen? Absolut nicht.

// 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?

Plötzlich hast du eine sehr verwirrte Katze.

Die im Wikipedia-Artikel beschriebene ursprüngliche Motivation, Arrays kovariant zu machen, galt nicht für Generika, da Platzhalter den Ausdruck von Kovarianz (und Kontravarianz) ermöglichten, zum Beispiel:

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
Paul Bellora
quelle
4
Ja, Arrays ermöglichen polymorphes Verhalten, führen jedoch Laufzeitausnahmen ein (im Gegensatz zu Ausnahmen zur Kompilierungszeit mit Generika). zB: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
24
Das ist richtig. Arrays haben reifizierbare Typen und werfen ArrayStoreExceptions nach Bedarf. Dies wurde damals eindeutig als würdiger Kompromiss angesehen. Vergleichen Sie das mit heute: Viele betrachten Array-Kovarianz im Nachhinein als Fehler.
Paul Bellora
2
Warum betrachten "viele" es als Fehler? Es ist weitaus nützlicher, als keine Array-Kovarianz zu haben. Wie oft haben Sie eine ArrayStoreException gesehen? Sie sind ziemlich selten. Die Ironie hier ist unverzeihlich imo ... Zu den schlimmsten Fehlern, die jemals in Java gemacht wurden, gehört die Varianz der Nutzungsseiten, auch bekannt als Platzhalter.
Scott
3
@ScottMcKinney: "Warum halten" viele "es für einen Fehler?" AIUI, dies liegt daran, dass für die Array-Kovarianz dynamische Typprüfungen für alle Array-Zuweisungsvorgänge erforderlich sind (obwohl Compiler-Optimierungen möglicherweise hilfreich sein können?), Die einen erheblichen Laufzeitaufwand verursachen können.
Dominique Devriese
Danke, Dominique, aber meiner Beobachtung nach scheint der Grund, warum "viele" es für einen Fehler halten, eher im Sinne von Papageien zu sein, was einige andere gesagt haben. Auch hier ist es weitaus nützlicher als schädlich, die Array-Kovarianz neu zu betrachten. Wiederum war der eigentliche große Fehler, den Java gemacht hat, die generische Varianz der Verwendungsstelle über Platzhalter. Das hat mehr Probleme verursacht, als ich denke, dass "viele" zugeben wollen.
Scott
30

Der Grund dafür ist, dass jedes Array zur Laufzeit seinen Elementtyp kennt, während die generische Auflistung dies aufgrund der Typlöschung nicht tut.

Zum Beispiel:

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

Wenn dies bei generischen Sammlungen zulässig war:

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

Dies würde jedoch später zu Problemen führen, wenn jemand versuchen würde, auf die Liste zuzugreifen:

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
Katona
quelle
Ich denke, die Antwort von Paul Bellora ist angemessener, da er kommentiert, warum Arrays kovariant sind. Wenn Arrays invariant gemacht wurden, ist es in Ordnung. Sie hätten damit eine Art Löschung. Der Hauptgrund für die Eigenschaft Typ Erasure ist die korrekte Abwärtskompatibilität.
eagertoLearn
@ user2708477, ja, die Löschung des Typs wurde aufgrund der Abwärtskompatibilität eingeführt. Und ja, meine Antwort versucht die Frage im Titel zu beantworten, warum Generika unveränderlich sind.
Katona
Die Tatsache , dass Arrays ihre Art Mittel wissen , dass während Kovarianz - Code ermöglicht zu fragen , zu speichern etwas in ein Array , wo es nicht passt - es bedeutet nicht , dass ein solches Geschäft wird stattfinden dürfen. Folglich ist das Ausmaß der Gefahr, die durch die Kovarianz von Arrays entsteht, viel geringer als wenn sie ihre Typen nicht kennen würden.
Supercat
2
Ich persönlich denke, diese Antwort liefert die richtige Erklärung dafür, warum Arrays kovariant sind, wenn Sammlungen nicht möglich sind. Vielen Dank!
Asgs
1
@mightyWOZ Ich denke, die Frage ist, warum sich Arrays und Generika in Bezug auf die Varianz unterscheiden. Meiner Meinung nach ist es überraschender, dass Generika nicht kovariant sind. Deshalb habe ich mich in meiner Antwort auf sie konzentriert.
Katona
23

Kann diese Hilfe sein: -

Generika sind nicht kovariant

Arrays in der Java-Sprache sind kovariant - was bedeutet, dass wenn Integer Number erweitert (was es tut), nicht nur eine Integer auch eine Zahl ist, sondern eine Integer [] auch eine Number[], und Sie können eine übergeben oder zuweisen Integer[]wo a Number[]verlangt wird. (Wenn Number ein Supertyp von Integer ist, dann Number[]ist dies ein Supertyp von Integer[].) Sie könnten denken, dass dies auch für generische Typen gilt - das List<Number>ist ein Supertyp von List<Integer>, und Sie können a übergeben, List<Integer>wo a List<Number>erwartet wird. Leider funktioniert das nicht so.

Es stellt sich heraus, dass es einen guten Grund gibt, warum es nicht so funktioniert: Es würde den Typ brechen, den Sicherheitsgenerika bieten sollten. Stellen Sie sich vor, Sie könnten a List<Integer>einem zuweisen List<Number>. Dann würde der folgende Code es Ihnen ermöglichen, etwas, das keine Ganzzahl war, in eine zu setzen List<Integer>:

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

Da ln a ist List<Number>, scheint das Hinzufügen eines Floats vollkommen legal zu sein. Wenn jedoch ein Aliasing angewendet liwürde, würde dies das in der Definition von li implizierte Versprechen der Typensicherheit brechen - dass es sich um eine Liste von Ganzzahlen handelt, weshalb generische Typen nicht kovariant sein können.

Rahul Tripathi
quelle
3
Für Arrays erhalten Sie eine ArrayStoreExceptionzur Laufzeit.
Sotirios Delimanolis
4
Meine Frage ist, WHYob Arrays kovariant gemacht wurden. Wie Sotirios erwähnte, würde man mit Arrays zur Laufzeit ArrayStoreException erhalten. Wenn Arrays invariant gemacht würden, könnten wir diesen Fehler zur Kompilierungszeit selbst richtig erkennen.
eagertoLearn
@eagertoLearn: Eine große semantische Schwäche von Java besteht darin, dass nichts in seinem Typsystem "Array, das nichts als Ableitungen von enthält Animal, das keine von einem anderen Ort empfangenen Elemente akzeptieren muss" von "Array, das nichts anderes als enthalten muss" unterscheiden Animalkann. und muss bereit sein, extern bereitgestellte Verweise auf zu akzeptieren Animal. Code, der das erstere benötigt, sollte ein Array von akzeptieren Cat, Code, der das letztere benötigt, sollte dies nicht tun. Wenn der Compiler die beiden Typen unterscheiden könnte, könnte er eine Überprüfung zur Kompilierungszeit bereitstellen. Leider ist das einzige, was sie auszeichnet ...
Supercat
... ist, ob Code tatsächlich versucht, etwas in ihnen zu speichern, und es gibt keine Möglichkeit, dies bis zur Laufzeit zu wissen.
Supercat
3

Arrays sind aus mindestens zwei Gründen kovariant:

  • Es ist nützlich für Sammlungen, die Informationen enthalten, die sich niemals ändern und kovariant sind. Damit eine Sammlung von T kovariant ist, muss auch ihr Hintergrundspeicher kovariant sein. Während man eine unveränderliche TSammlung entwerfen könnte, die a nicht T[]als Hintergrundspeicher verwendet (z. B. unter Verwendung eines Baums oder einer verknüpften Liste), ist es unwahrscheinlich, dass eine solche Sammlung so gut funktioniert wie eine Sammlung, die von einem Array unterstützt wird. Man könnte argumentieren, dass ein besserer Weg, kovariante unveränderliche Sammlungen bereitzustellen, darin bestanden hätte, einen "kovarianten unveränderlichen Array" -Typ zu definieren, für den sie einen Sicherungsspeicher verwenden könnten, aber das einfache Zulassen der Array-Kovarianz war wahrscheinlich einfacher.

  • Arrays werden häufig durch Code mutiert, der nicht weiß, welche Art von Dingen in ihnen enthalten sein wird, aber nichts in das Array einfügt, was nicht aus demselben Array ausgelesen wurde. Ein Paradebeispiel hierfür ist das Sortieren von Code. Konzeptionell war es für Array-Typen möglicherweise möglich, Methoden zum Austauschen oder Permutieren von Elementen einzuschließen (solche Methoden können gleichermaßen auf jeden Array-Typ angewendet werden) oder ein "Array-Manipulator" -Objekt zu definieren, das einen Verweis auf ein Array und ein oder mehrere Dinge enthält Das wurde daraus gelesen und könnte Methoden zum Speichern zuvor gelesener Elemente in dem Array enthalten, aus dem sie stammen. Wenn Arrays nicht kovariant wären, könnte der Benutzercode einen solchen Typ nicht definieren, aber die Laufzeit hätte einige spezielle Methoden enthalten können.

Die Tatsache, dass Arrays kovariant sind, kann als hässlicher Hack angesehen werden, erleichtert jedoch in den meisten Fällen die Erstellung von Arbeitscode.

Superkatze
quelle
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.- guter Punkt
eagertoLearn
3

Ein wichtiges Merkmal von Parametertypen ist die Fähigkeit, polymorphe Algorithmen zu schreiben, dh Algorithmen, die eine Datenstruktur unabhängig von ihrem Parameterwert bearbeiten, wie z Arrays.sort().

Bei Generika geschieht dies mit Platzhaltertypen:

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

Um wirklich nützlich zu sein, erfordern Platzhaltertypen die Erfassung von Platzhaltern, und dies erfordert die Vorstellung eines Typparameters. Nichts davon war verfügbar, als Arrays zu Java hinzugefügt wurden, und die Erstellung von Arrays mit Referenzkovariante ermöglichte eine weitaus einfachere Möglichkeit, polymorphe Algorithmen zuzulassen:

void sort(Comparable[]);

Diese Einfachheit öffnete jedoch eine Lücke im statischen Typsystem:

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

Erfordert eine Laufzeitprüfung jedes Schreibzugriffs auf ein Array vom Referenztyp.

Kurz gesagt, der neuere Ansatz von Generika macht das Typensystem komplexer, aber auch statisch typsicherer, während der ältere Ansatz einfacher und weniger statisch typsicher war. Die Designer der Sprache entschieden sich für den einfacheren Ansatz und hatten wichtigere Dinge zu tun, als eine kleine Lücke im Typensystem zu schließen, die selten Probleme verursacht. Später, als Java eingerichtet wurde und die dringenden Anforderungen erfüllt wurden, verfügten sie über die Ressourcen, um dies für Generika richtig zu machen (aber das Ändern für Arrays hätte vorhandene Java-Programme beschädigt).

Meriton
quelle
2

Generika sind unveränderlich : ab JSL 4.10 :

... Subtyping erstreckt sich nicht über generische Typen: T <: U bedeutet nicht, dass C<T><: C<U>...

und ein paar Zeilen weiter erklärt JLS auch, dass
Arrays kovariant sind (erste Kugel):

4.10.3 Subtypisierung zwischen Array-Typen

Geben Sie hier die Bildbeschreibung ein

Nir Alfasi
quelle
2

Ich denke, sie haben an erster Stelle eine falsche Entscheidung getroffen, die das Array kovariant gemacht hat. Es bricht die hier beschriebene Typensicherheit und sie haben sich aufgrund der Abwärtskompatibilität damit abgefunden und danach versucht, nicht den gleichen Fehler für Generika zu machen. Und das ist einer der Gründe, warum Joshua Bloch Listen in Punkt 25 des Buches "Effective Java (zweite Ausgabe)" vorzieht.

Reza
quelle
Josh Block war der Autor des Java-Sammlungsframeworks (1.2) und der Autor der Java-Generika (1.5). Der Typ, der die Generika gebaut hat, über die sich alle beschweren, ist zufällig auch der Typ, der das Buch geschrieben hat und sagt, dass sie der bessere Weg sind? Keine große Überraschung!
cpurdy
1

Mein Standpunkt: Wenn Code ein Array A [] erwartet und Sie ihm B [] geben, wobei B eine Unterklasse von A ist, müssen Sie sich nur um zwei Dinge kümmern: Was passiert, wenn Sie ein Array-Element lesen, und was passiert, wenn Sie schreiben es. Es ist also nicht schwer, Sprachregeln zu schreiben, um sicherzustellen, dass die Typensicherheit in allen Fällen erhalten bleibt (die Hauptregel ist, dass ein ArrayStoreExceptionausgelöst werden kann, wenn Sie versuchen, ein A in ein B zu stecken []). Für ein Generikum kann es jedoch beim Deklarieren einer Klasse SomeClass<T>eine beliebige Anzahl von Möglichkeiten geben, Tdie im Hauptteil der Klasse verwendet werden, und ich vermute, es ist einfach viel zu kompliziert, alle möglichen Kombinationen zu erarbeiten, um Regeln darüber zu schreiben, wann Dinge sind erlaubt und wenn nicht.

ajb
quelle
0

Wir können nicht schreiben, List<Object> l = new ArrayList<String>();weil Java versucht, uns vor einer Laufzeitausnahme zu schützen. Sie könnten denken, dies würde bedeuten, dass wir nicht schreiben können Object[] o = new String[0];. Das ist nicht der Fall. Dieser Code kompiliert:

Integer[] numbers = { new Integer(42)};
Object[] objects = numbers;
objects[0] = "forty two"; // throws ArrayStoreException

Obwohl der Code kompiliert wird, löst er zur Laufzeit eine Ausnahme aus. Bei Arrays kennt Java den Typ, der im Array zulässig ist. Nur weil wir ein Integer[]einem zugewiesen haben , Object[]ändert dies nichts an der Tatsache, dass Java weiß, dass es wirklich ein ist Integer[].

Aufgrund der Typlöschung haben wir keinen solchen Schutz für eine ArrayList. Zur Laufzeit weiß die ArrayList nicht, was darin zulässig ist. Daher verwendet Java den Compiler, um zu verhindern, dass diese Situation überhaupt auftritt. OK, warum fügt Java dieses Wissen nicht zu ArrayList hinzu? Der Grund ist die Abwärtskompatibilität. Das heißt, Java legt großen Wert darauf, vorhandenen Code nicht zu beschädigen.

OCP-Referenz.

Ali Yeganeh
quelle