Was sind die Gründe für die Entscheidung, nicht eine völlig generische get - Methode in der Schnittstelle haben java.util.Map<K, V>
.
Zur Klärung der Frage lautet die Signatur der Methode
V get(Object key)
anstatt
V get(K key)
und ich frage mich warum (das gleiche für remove, containsKey, containsValue
).
java
generics
collections
map
WMR
quelle
quelle
Antworten:
Wie von anderen erwähnt, ist der Grund dafür
get()
usw. nicht generisch, da der Schlüssel des Eintrags, den Sie abrufen, nicht vom selben Typ sein muss wie das Objekt, an das Sie übergebenget()
. Die Spezifikation der Methode erfordert nur, dass sie gleich sind. Dies folgt daraus, wie dieequals()
Methode ein Objekt als Parameter aufnimmt, nicht nur denselben Typ wie das Objekt.Obwohl es allgemein zutreffend sein mag, dass viele Klassen so
equals()
definiert haben, dass ihre Objekte nur Objekten ihrer eigenen Klasse entsprechen können, gibt es in Java viele Stellen, an denen dies nicht der Fall ist. In der Spezifikation fürList.equals()
heißt es beispielsweise, dass zwei Listenobjekte gleich sind, wenn sie beide Listen sind und denselben Inhalt haben, auch wenn es sich um unterschiedliche Implementierungen von handeltList
. Wenn wir also auf das Beispiel in dieser Frage zurückkommen, ist es gemäß der Spezifikation der Methode möglich, einMap<ArrayList, Something>
und für michget()
mit einemLinkedList
as-Argument aufzurufen , und es sollte den Schlüssel abrufen, der eine Liste mit demselben Inhalt ist. Dies wäre nicht möglich, wennget()
der Argumenttyp generisch und eingeschränkt wäre.quelle
V Get(K k)
in C #?m.get(linkedList)
, warum haben Sie denm
Typ nicht als definiertMap<List,Something>
? Ich kann mir keinen Anwendungsfall vorstellen, in dem ein Aufrufm.get(HappensToBeEqual)
ohne Änderung desMap
Typs für eine Schnittstelle sinnvoll ist.TreeMap
kann fehlschlagen, wenn Sie Objekte des falschen Typs an dieget
Methode übergeben, aber gelegentlich übergeben, z. B. wenn die Karte zufällig leer ist. Und noch schlimmer, im Falle einer Lieferung kannComparator
diecompare
Methode (die eine generische Signatur hat!) Ohne ungeprüfte Warnung mit Argumenten des falschen Typs aufgerufen werden. Das ist kaputtes Verhalten.Ein großartiger Java-Codierer bei Google, Kevin Bourrillion, hat vor einiger Zeit in einem Blog-Beitrag über genau dieses Problem geschrieben (zugegebenermaßen im Zusammenhang mit
Set
stattMap
). Der relevanteste Satz:Ich bin mir nicht ganz sicher, ob ich dem grundsätzlich zustimme - .NET scheint in Ordnung zu sein, wenn beispielsweise der richtige Schlüsseltyp benötigt wird -, aber es lohnt sich, den Überlegungen im Blog-Beitrag zu folgen. (Nachdem wir .NET erwähnt haben, lohnt es sich zu erklären, dass ein Teil des Grundes, warum es in .NET kein Problem ist, darin besteht, dass es in .NET ein größeres Problem mit begrenzterer Varianz gibt ...)
quelle
Integer
und aDouble
niemals gleich sein können, ist es dennoch eine faire Frage, ob aSet<? extends Number>
den Wert enthältnew Integer(5)
.Set<? extends Foo>
. Ich habe sehr häufig den Schlüsseltyp einer Karte geändert und war dann frustriert, dass der Compiler nicht alle Stellen finden konnte, an denen der Code aktualisiert werden musste. Ich bin wirklich nicht davon überzeugt, dass dies der richtige Kompromiss ist.Der Vertrag wird folgendermaßen ausgedrückt:
(meine Betonung)
Daher hängt eine erfolgreiche Schlüsselsuche von der Implementierung der Gleichheitsmethode durch den Eingabeschlüssel ab. Das hängt nicht unbedingt von der Klasse von k ab.
quelle
hashCode()
. Ohne eine ordnungsgemäße Implementierung von hashCode () ist eine gut implementierteequals()
Version in diesem Fall eher nutzlos.get()
muss kein typisches Argument verwendet werdenObject
, um den Kontakt zu befriedigen. Stellen Sie sich vor, die get-Methode wäre auf den Schlüsseltyp beschränktK
- der Vertrag wäre weiterhin gültig. Natürlich können Verwendungen, bei denen der Kompilierungszeittyp keine Unterklasse von warK
, jetzt nicht kompiliert werden, aber dies macht den Vertrag nicht ungültig, da Verträge implizit diskutieren, was passiert, wenn der Code kompiliert wird.Es ist eine Anwendung des Postelschen Gesetzes: "Sei konservativ in dem, was du tust, sei liberal in dem, was du von anderen akzeptierst."
Gleichheitsprüfungen können unabhängig vom Typ durchgeführt werden. Die
equals
Methode ist auf der definiertObject
Klasse und akzeptiert alleObject
als Parameter. Daher ist es sinnvoll, dass Schlüsseläquivalenz und Operationen, die auf Schlüsseläquivalenz basieren, einen beliebigenObject
Typ akzeptieren .Wenn eine Karte Schlüsselwerte zurückgibt, werden mithilfe des Typparameters so viele Typinformationen wie möglich gespeichert.
quelle
V Get(K k)
in C #?V Get(K k)
in C #, weil es auch Sinn macht. Der Unterschied zwischen den Java- und .NET-Ansätzen besteht eigentlich nur darin, wer nicht übereinstimmende Inhalte blockiert. In C # ist es der Compiler, in Java die Sammlung. Ich Wut über .NET ist inkonsistent Collection - Klassen ab und zu, aberGet()
undRemove()
nur eine passende Art zu akzeptieren sicher verhindert , dass Sie versehentlich einen falschen Wert im Vorbeigehen.contains : K -> boolean
.Ich denke, dieser Abschnitt des Generika-Tutorials erklärt die Situation (mein Schwerpunkt):
"Sie müssen sicherstellen, dass die generische API nicht übermäßig restriktiv ist. Sie muss weiterhin den ursprünglichen Vertrag der API unterstützen. Betrachten Sie noch einmal einige Beispiele aus java.util.Collection. Die vorgenerische API sieht folgendermaßen aus:
Ein naiver Versuch, es zu erzeugen, ist:
Dies ist zwar sicherlich typsicher, entspricht jedoch nicht dem ursprünglichen Vertrag der API. Die Methode includesAll () funktioniert mit jeder Art von eingehender Sammlung. Es wird nur erfolgreich sein, wenn die eingehende Sammlung wirklich nur Instanzen von E enthält, aber:
quelle
containsAll( Collection< ? extends E > c )
?containsAll
mit einemCollection<S>
whereS
einen Supertyp von zuzulassenE
. Dies wäre nicht erlaubt, wenn es so wärecontainsAll( Collection< ? extends E > c )
. Darüber, wie sie ausdrücklich im Beispiel angegeben, ist es legitim , eine Sammlung eines anderen Typs zu übergeben (mit dem Rückgabewert dann seinfalse
).Der Grund dafür ist, dass die Eindämmung durch
equals
und fürhashCode
welche Methoden bestimmt wirdObject
und beide einenObject
Parameter annehmen . Dies war ein früher Konstruktionsfehler in Javas Standardbibliotheken. In Verbindung mit Einschränkungen im Java-Typsystem wird alles erzwungen, was auf equals und hashCode angewiesen istObject
.Der einzige Weg , typsichere Hash - Tabellen und Gleichheit in Java haben , ist zu vermeiden
Object.equals
undObject.hashCode
und einen generischen Ersatz zu verwenden. Funktionales Java wird zu diesem Zweck mit Typklassen geliefert:Hash<A>
undEqual<A>
. Es wird ein Wrapper fürHashMap<K, V>
bereitgestellt, derHash<K>
undEqual<K>
in seinem Konstruktor enthält. Diese Klassenget
undcontains
Methoden verwenden daher ein generisches Argument vom TypK
.Beispiel:
quelle
Kompatibilität.
Bevor Generika verfügbar waren, gab es nur get (Object o).
Hätten sie diese Methode geändert, um (<K> o) zu erhalten, hätte dies Java-Benutzern möglicherweise eine massive Code-Wartung aufgezwungen, nur um den Arbeitscode erneut kompilieren zu lassen.
Sie könnten eine eingeführte zusätzliche Methode, sagt get_checked (<K> o) und deprecate die alte Methode get () , so gibt es einen sanften Übergang Weg war. Aber aus irgendeinem Grund wurde dies nicht getan. (Die Situation, in der wir uns gerade befinden, besteht darin, dass Sie Tools wie findBugs installieren müssen, um die Typkompatibilität zwischen dem Argument get () und dem deklarierten Schlüsseltyp <K> der Map zu überprüfen.)
Die Argumente in Bezug auf die Semantik von .equals () sind meiner Meinung nach falsch. (Technisch gesehen sind sie korrekt, aber ich denke immer noch, dass sie falsch sind. Kein Designer, der bei klarem Verstand ist, wird jemals o1.equals (o2) wahr machen, wenn o1 und o2 keine gemeinsame Oberklasse haben.)
quelle
Es gibt noch einen weiteren wichtigen Grund, der technisch nicht möglich ist, da er die Karte bricht.
Java hat eine polymorphe generische Konstruktion wie
<? extends SomeClass>
. Eine solche Referenz kann auf einen mit signierten Typ verweisen<AnySubclassOfSomeClass>
. Aber polymorphes Generikum macht diese Referenz schreibgeschützt . Mit dem Compiler können Sie generische Typen nur als Rückgabetyp für Methoden verwenden (wie einfache Getter), aber die Verwendung von Methoden blockieren, bei denen generischer Typ ein Argument ist (wie gewöhnliche Setter). Wenn Sie schreibenMap<? extends KeyType, ValueType>
, können Sie vom Compiler keine Methode aufrufenget(<? extends KeyType>)
, und die Zuordnung ist unbrauchbar. Die einzige Lösung besteht darin, diese Methode nicht generisch zu machen :get(Object)
.quelle
Abwärtskompatibilität, denke ich.
Map
(oderHashMap
) muss noch unterstützenget(Object)
.quelle
put
(was die generischen Typen einschränkt). Durch die Verwendung von Rohtypen erhalten Sie Abwärtskompatibilität. Generika sind "Opt-In".Ich habe mir das angesehen und darüber nachgedacht, warum sie es so gemacht haben. Ich glaube nicht, dass eine der vorhandenen Antworten erklärt, warum die neue generische Schnittstelle nicht einfach nur den richtigen Typ für den Schlüssel akzeptieren konnte. Der eigentliche Grund ist, dass sie, obwohl sie Generika eingeführt haben, KEINE neue Schnittstelle erstellt haben. Die Kartenschnittstelle ist dieselbe alte nicht generische Karte, die nur als generische und nicht generische Version dient. Auf diese Weise können Sie eine Methode übergeben, die nicht generische Map akzeptiert,
Map<String, Customer>
und sie funktioniert weiterhin. Gleichzeitig akzeptiert der Vertrag für get Object, sodass die neue Schnittstelle auch diesen Vertrag unterstützen sollte.Meiner Meinung nach hätten sie eine neue Schnittstelle hinzufügen und beide in die vorhandene Sammlung implementieren sollen, aber sie haben sich für kompatible Schnittstellen entschieden, auch wenn dies ein schlechteres Design für die get-Methode bedeutet. Beachten Sie, dass die Sammlungen selbst mit vorhandenen Methoden kompatibel wären, nur die Schnittstellen nicht.
quelle
Wir führen gerade ein großes Refactoring durch und haben dieses stark typisierte get () verpasst, um zu überprüfen, ob wir get () mit altem Typ nicht verpasst haben.
Aber ich habe einen Workaround / hässlichen Trick für die Überprüfung der Kompilierungszeit gefunden: Erstellen Sie eine Map-Oberfläche mit stark typisiertem get, enthältKey, remove ... und fügen Sie sie in das Paket java.util Ihres Projekts ein.
Sie erhalten Kompilierungsfehler nur für den Aufruf von get (), ... mit falschen Typen scheint alles andere für den Compiler in Ordnung zu sein (zumindest innerhalb von Eclipse Kepler).
Vergessen Sie nicht, diese Schnittstelle nach Überprüfung Ihres Builds zu löschen, da dies zur Laufzeit nicht Ihren Wünschen entspricht.
quelle