Ich bin etwas verwirrt darüber, wie Java-Generika mit Vererbung / Polymorphismus umgehen.
Nehmen Sie die folgende Hierarchie an -
Tier (Eltern)
Hund - Katze (Kinder)
Angenommen, ich habe eine Methode doSomething(List<Animal> animals)
. Nach allen Regeln der Vererbung und Polymorphismus, würde ich davon ausgehen , dass ein List<Dog>
ist ein List<Animal>
und List<Cat>
ist ein List<Animal>
- und so entweder konnte man an diese Methode übergeben werden. Nicht so. Wenn ich dieses Verhalten erreichen möchte, muss ich die Methode explizit anweisen, eine Liste aller Unterklassen von Tieren zu akzeptieren, indem ich sage doSomething(List<? extends Animal> animals)
.
Ich verstehe, dass dies Javas Verhalten ist. Meine Frage ist warum ? Warum ist Polymorphismus im Allgemeinen implizit, aber wenn es um Generika geht, muss er spezifiziert werden?
quelle
Antworten:
Nein, a
List<Dog>
ist nicht aList<Animal>
. Überlegen Sie, was Sie mit einem tun könnenList<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.Plötzlich hast du eine sehr verwirrte Katze.
Jetzt können Sie ein nicht
Cat
zu einem hinzufügen ,List<? extends Animal>
weil Sie nicht wissen, dass es ein istList<Cat>
. Sie können einen Wert abrufen und wissen, dass es sich um einen Wert handeltAnimal
, aber Sie können keine beliebigen Tiere hinzufügen. Das Gegenteil gilt fürList<? super Animal>
- in diesem Fall können Sie einAnimal
sicher hinzufügen , aber Sie wissen nichts darüber, was daraus abgerufen werden könnte, da es ein sein könnteList<Object>
.quelle
Was Sie suchen, nennt man kovariante Typparameter . Dies bedeutet, dass, wenn ein Objekttyp in einer Methode durch einen anderen ersetzt werden kann (z. B.
Animal
durch ersetzt werden kannDog
), dies auch für Ausdrücke gilt, die diese Objekte verwenden (alsoList<Animal>
durch ersetzt werden könntenList<Dog>
). Das Problem ist, dass Kovarianz für veränderbare Listen im Allgemeinen nicht sicher ist. Angenommen, Sie haben eineList<Dog>
, und sie wird als verwendetList<Animal>
. Was passiert, wenn Sie versuchen, eine Katze hinzuzufügen,List<Animal>
die wirklich eine istList<Dog>
? Das automatische Zulassen, dass Typparameter kovariant sind, unterbricht das Typensystem.Es wäre nützlich, eine Syntax hinzuzufügen, damit Typparameter als Kovariante angegeben werden können. Dadurch werden die
? extends Foo
Deklarationen in Methoden vermieden , dies erhöht jedoch die Komplexität.quelle
Der Grund, warum a
List<Dog>
kein a istList<Animal>
, ist, dass Sie beispielsweise einCat
in ein einfügen könnenList<Animal>
, aber nicht in einList<Dog>
... Sie können Platzhalter verwenden, um Generika nach Möglichkeit erweiterbarer zu machen. Zum Beispiel ist das Lesen von aList<Dog>
dem Lesen von a ähnlichList<Animal>
- aber nicht das Schreiben.Die Generika in der Java-Sprache und der Abschnitt über Generika in den Java-Tutorials enthalten eine sehr gute und ausführliche Erklärung, warum einige Dinge polymorph sind oder nicht oder mit Generika zulässig sind.
quelle
Ein Punkt, den ich denke, sollte zu dem hinzugefügt werden, was andere Antworten erwähnen, ist das während
es ist auch wahr, dass
Die Art und Weise, wie die Intuition des OP funktioniert - was natürlich völlig gültig ist - ist der letztere Satz. Wenn wir diese Intuition anwenden, erhalten wir jedoch eine Sprache, die in ihrem Typensystem nicht Java-artig ist: Angenommen, unsere Sprache erlaubt das Hinzufügen einer Katze zu unserer Liste von Hunden. Was würde das bedeuten? Dies würde bedeuten, dass die Liste keine Liste von Hunden mehr ist und lediglich eine Liste von Tieren bleibt. Und eine Liste von Säugetieren und eine Liste von Quadrapeds.
Anders ausgedrückt: A bedeutet
List<Dog>
auf Java nicht "eine Liste von Hunden" auf Englisch, sondern "eine Liste, die Hunde haben kann, und sonst nichts".Im Allgemeinen eignet sich die Intuition von OP für eine Sprache, in der Operationen an Objekten ihren Typ ändern können , oder vielmehr, dass die Typen eines Objekts eine (dynamische) Funktion seines Werts sind.
quelle
Ich würde sagen, der springende Punkt bei Generics ist, dass es das nicht zulässt. Betrachten Sie die Situation mit Arrays, die diese Art von Kovarianz zulassen:
Dieser Code wird gut kompiliert, löst jedoch einen Laufzeitfehler aus (
java.lang.ArrayStoreException: java.lang.Boolean
in der zweiten Zeile). Es ist nicht typsicher. Bei Generics geht es darum, die Sicherheit für den Kompilierungszeittyp hinzuzufügen. Andernfalls können Sie sich einfach an eine einfache Klasse ohne Generics halten.Jetzt gibt es Zeiten, in denen Sie flexibler sein müssen,
? super Class
und dafür? extends Class
sind die und da . Ersteres ist, wenn Sie in einen Typ einfügen müssenCollection
(zum Beispiel), und letzteres ist, wenn Sie typsicher daraus lesen müssen. Die einzige Möglichkeit, beides gleichzeitig zu tun, besteht darin, einen bestimmten Typ zu haben.quelle
Um das Problem zu verstehen, ist es nützlich, einen Vergleich mit Arrays durchzuführen.
List<Dog>
ist keine Unterklasse vonList<Animal>
.Ist
Dog[]
aber Unterklasse vonAnimal[]
.Arrays sind reifizierbar und kovariant .
Reifizierbar bedeutet, dass ihre Typinformationen zur Laufzeit vollständig verfügbar sind.
Daher bieten Arrays Sicherheit vom Typ Laufzeit, jedoch keine Sicherheit vom Typ Kompilierungszeit.
Bei
Generika ist es umgekehrt: Generika werden gelöscht und sind unveränderlich .
Daher können Generika keine Sicherheit vom Typ Laufzeit bieten, sie bieten jedoch Sicherheit vom Typ Kompilierungszeit.
Wenn im folgenden Code Generika kovariant wären, könnte in Zeile 3 eine Haufenverschmutzung auftreten .
quelle
Die hier gegebenen Antworten haben mich nicht ganz überzeugt. Also mache ich stattdessen ein anderes Beispiel.
klingt gut, nicht wahr? Sie können aber nur
Consumer
s undSupplier
s fürAnimal
s übergeben. Wenn Sie einenMammal
Verbraucher, aber einenDuck
Lieferanten haben, sollten diese nicht passen, obwohl beide Tiere sind. Um dies zu verbieten, wurden zusätzliche Einschränkungen hinzugefügt.Anstelle der oben genannten müssen wir Beziehungen zwischen den von uns verwendeten Typen definieren.
Z.B.,
stellt sicher, dass wir nur einen Lieferanten verwenden können, der uns den richtigen Objekttyp für den Verbraucher bietet.
OTOH, das könnten wir auch
wo wir den anderen Weg gehen: Wir definieren den Typ des
Supplier
und beschränken, dass es in das gesetzt werden kannConsumer
.Wir können es sogar tun
wo die intuitiven Beziehungen mit
Life
->Animal
->Mammal
->Dog
,Cat
usw., könnten wir sogar ein setzenMammal
in einenLife
Verbraucher, aber nichtString
in einenLife
Verbraucher.quelle
(Consumer<Runnable>, Supplier<Dog>)
whileDog
als Subtyp vonAnimal & Runnable
Die Basislogik für ein solches Verhalten besteht darin
Generics
, einem Mechanismus der Typlöschung zu folgen. Also während der Laufzeit haben Sie keine Möglichkeit , wenn die Identifizierung der Art der imcollection
Gegensatz zuarrays
denen es keine solchen Löschprozess. Kommen wir also auf Ihre Frage zurück ...Angenommen, es gibt eine Methode wie folgt:
Wenn Java dem Aufrufer erlaubt, dieser Methode eine Liste vom Typ Animal hinzuzufügen, können Sie der Sammlung möglicherweise etwas Falsches hinzufügen, und auch zur Laufzeit wird es aufgrund des Löschens des Typs ausgeführt. Während bei Arrays eine Laufzeitausnahme für solche Szenarien angezeigt wird ...
Somit wird dieses Verhalten im Wesentlichen so implementiert, dass man der Sammlung nichts Falsches hinzufügen kann. Jetzt glaube ich, dass es eine Typlöschung gibt, um die Kompatibilität mit Legacy-Java ohne Generika zu gewährleisten.
quelle
Die Subtypisierung ist für parametrisierte Typen unveränderlich . Selbst wenn die Klasse
Dog
ein Subtyp von istAnimal
, ist der parametrisierte TypList<Dog>
kein Subtyp vonList<Animal>
. Im Gegensatz dazu wird die kovariante Subtypisierung von Arrays verwendet, sodass der Array-TypDog[]
ein Subtyp von istAnimal[]
.Durch die invariante Untertypisierung wird sichergestellt, dass die von Java erzwungenen Typeinschränkungen nicht verletzt werden. Betrachten Sie den folgenden Code von @Jon Skeet:
Wie von @Jon Skeet angegeben, ist dieser Code illegal, da er sonst die Typbeschränkungen verletzen würde, indem eine Katze zurückgegeben wird, wenn ein Hund dies erwartet.
Es ist lehrreich, das Obige mit analogem Code für Arrays zu vergleichen.
Der Code ist legal. Löst jedoch eine Array-Speicherausnahme aus . Ein Array trägt seinen Typ zur Laufzeit auf diese Weise, sodass JVM die Typensicherheit der kovarianten Subtypisierung erzwingen kann.
Um dies weiter zu verstehen, schauen wir uns den Bytecode an, der
javap
von der folgenden Klasse generiert wurde :Mit dem Befehl
javap -c Demonstration
wird der folgende Java-Bytecode angezeigt:Beachten Sie, dass der übersetzte Code der Methodenkörper identisch ist. Der Compiler ersetzte jeden parametrisierten Typ durch seine Löschung . Diese Eigenschaft ist von entscheidender Bedeutung, da sie die Abwärtskompatibilität nicht beeinträchtigt.
Zusammenfassend ist die Laufzeitsicherheit für parametrisierte Typen nicht möglich, da der Compiler jeden parametrisierten Typ durch seine Löschung ersetzt. Dies macht parametrisierte Typen nichts anderes als syntaktischen Zucker.
quelle
Tatsächlich können Sie eine Schnittstelle verwenden, um das zu erreichen, was Sie wollen.
}}
Sie können dann die Sammlungen mit verwenden
quelle
Wenn Sie sicher sind, dass die Listenelemente Unterklassen dieses bestimmten Supertyps sind, können Sie die Liste mit diesem Ansatz umwandeln:
Dies ist nützlich, wenn Sie die Liste in einem Konstruktor übergeben oder darüber iterieren möchten
quelle
Die Antwort sowie andere Antworten sind korrekt. Ich werde diese Antworten mit einer Lösung ergänzen, die ich für hilfreich halte. Ich denke, das kommt beim Programmieren oft vor. Zu beachten ist, dass bei Sammlungen (Listen, Sets usw.) das Hauptproblem darin besteht, die Sammlung zu erweitern. Dort brechen die Dinge zusammen. Auch das Entfernen ist in Ordnung.
In den meisten Fällen können wir dann
Collection<? extends T>
eher verwendenCollection<T>
und das sollte die erste Wahl sein. Ich finde jedoch Fälle, in denen dies nicht einfach ist. Es steht zur Debatte, ob dies immer das Beste ist. Ich präsentiere hier eine Klasse DownCastCollection, die die Konvertierung von aCollection<? extends T>
in aCollection<T>
(wir können ähnliche Klassen für List, Set, NavigableSet, .. definieren) verwenden kann, um verwendet zu werden, wenn der Standardansatz verwendet wird, ist sehr unpraktisch. Im Folgenden finden Sie ein Beispiel für die Verwendung (wir könnten es auchCollection<? extends Object>
in diesem Fall verwenden, aber ich halte es einfach, die Verwendung von DownCastCollection zu veranschaulichen.Nun die Klasse:
}}
quelle
Collections.unmodifiableCollection
Collection<? extends E>
Behandelt dieses Verhalten jedoch bereits korrekt, es sei denn, Sie verwenden es auf eine Weise, die nicht typsicher ist (z. B. wenn Sie es auf etwas anderes übertragen). Der einzige Vorteil, den ich dort sehe, ist, dass beim Aufrufen deradd
Operation eine Ausnahme ausgelöst wird, selbst wenn Sie sie gewirkt haben.Nehmen wir das Beispiel aus dem JavaSE- Tutorial
Warum eine Liste von Hunden (Kreisen) nicht implizit als Liste von Tieren (Formen) betrachtet werden sollte, liegt an dieser Situation:
Java "Architekten" hatten also zwei Möglichkeiten, um dieses Problem anzugehen:
Denken Sie nicht daran, dass ein Subtyp implizit ein Supertyp ist, und geben Sie einen Kompilierungsfehler aus, wie es jetzt geschieht
Betrachten Sie den Subtyp als Supertyp und beschränken Sie beim Kompilieren die "add" -Methode (wenn also in der drawAll-Methode eine Liste von Kreisen, Subtyp der Form, übergeben würde, sollte der Compiler dies erkennen und Sie mit Kompilierungsfehlern einschränken Das).
Aus offensichtlichen Gründen entschied sich dies für den ersten Weg.
quelle
Wir sollten auch berücksichtigen, wie der Compiler die generischen Klassen bedroht: In "instanziiert" ein anderer Typ, wenn wir die generischen Argumente ausfüllen.
So haben wir
ListOfAnimal
,ListOfDog
,ListOfCat
, usw., die verschiedenen Klassen sind , dass am Ende wird durch den Compiler „erzeugt“ wird , wenn wir die allgemeinen Argumente angeben. Und dies ist eine flache Hierarchie (eigentlichList
ist dies überhaupt keine Hierarchie).Ein weiteres Argument, warum Kovarianz bei generischen Klassen keinen Sinn ergibt, ist die Tatsache, dass im Grunde alle Klassen gleich sind -
List
Instanzen sind. Das Spezialisieren von aList
durch Ausfüllen des generischen Arguments erweitert die Klasse nicht, sondern funktioniert nur für dieses bestimmte generische Argument.quelle
Das Problem wurde gut identifiziert. Aber es gibt eine Lösung; mach doSomething generisch:
Jetzt können Sie doSomething entweder mit List <Dog> oder List <Cat> oder List <Animal> aufrufen.
quelle
Eine andere Lösung besteht darin, eine neue Liste zu erstellen
quelle
Weiter zur Antwort von Jon Skeet, der diesen Beispielcode verwendet:
Auf der tiefsten Ebene ist das Problem hier das
dogs
undanimals
teilen Sie eine Referenz. Das bedeutet, dass eine Möglichkeit, diese Arbeit zu machen, darin besteht, die gesamte Liste zu kopieren, was die Referenzgleichheit brechen würde:Nach dem Anruf
List<Animal> animals = new ArrayList<>(dogs);
können Sieanimals
entwederdogs
oder nicht direkt zuweisencats
:Daher können Sie den falschen Subtyp von nicht
Animal
in die Liste aufnehmen, da es keinen falschen Subtyp gibt - jedes Objekt des Subtyps? extends Animal
kann hinzugefügt werdenanimals
.Offensichtlich ändert sich die Semantik, da die Listen
animals
unddogs
sind nicht mehr freigegeben, so zu einer Liste hinzuzufügen hinzufügen nicht auf die anderen (das ist genau das , was Sie wollen, um das Problem zu vermeiden , dass einCat
zu einer Liste hinzugefügt werden könnte , die nur sollDog
Objekte enthalten ). Das Kopieren der gesamten Liste kann auch ineffizient sein. Dies löst jedoch das Typäquivalenzproblem, indem die Referenzgleichheit gebrochen wird.quelle