Was sind die Gründe für die Entscheidung, keine vollständig generische get-Methode in der Schnittstelle von Java.util.Map<K, V>
zu haben.
Zur Klärung der Frage lautet die Signatur der Methode
V get(Object key)
anstatt
V get(K key)
und ich frage mich warum (dasselbe für remove, containsKey, containsValue
).
Wie von anderen erwähnt, ist der Grund, warum get()
usw. nicht generisch ist, da der Schlüssel des abgerufenen Eintrags nicht mit dem Typ des Objekts übereinstimmen muss, das Sie an get()
; Die Spezifikation der Methode erfordert nur, dass sie gleich sind. Dies ergibt sich daraus, wie die equals()
-Methode ein Objekt als Parameter verwendet und nicht nur den gleichen Typ wie das Objekt.
Obwohl es allgemein wahr sein kann, dass für viele Klassen equals()
definiert ist, so dass ihre Objekte nur Objekten ihrer eigenen Klasse entsprechen können, gibt es in Java) viele Stellen, an denen dies der Fall ist Dies ist jedoch nicht der Fall. Beispielsweise besagt die Spezifikation für List.equals()
, dass zwei List-Objekte gleich sind, wenn sie beide Listen sind und denselben Inhalt haben, auch wenn es sich um unterschiedliche Implementierungen von List
handelt. Um auf das Beispiel in dieser Frage zurückzukommen, ist es gemäß der Spezifikation der Methode möglich, ein Map<ArrayList, Something>
Zu haben und get()
mit einem LinkedList
als Argument aufzurufen , und es sollte den Schlüssel abrufen, der eine Liste mit demselben Inhalt ist. Dies wäre nicht möglich, wenn get()
generisch wäre und seinen Argumenttyp einschränken würde.
Ein großartiger Java Codierer bei Google, Kevin Bourrillion, schrieb vor einer Weile über genau dieses Problem in einem Blog-Beitrag (zugegebenermaßen im Zusammenhang mit Set
anstelle von Map
). Der relevanteste Satz:
Gleichermaßen beschränken Methoden des Java Collections Framework (und der Google Collections Library)) niemals die Typen ihrer Parameter, es sei denn, dies ist erforderlich, um zu verhindern, dass die Sammlung beschädigt wird.
Ich bin nicht ganz sicher, ob ich dem grundsätzlich zustimme - .NET scheint zum Beispiel in Ordnung zu sein, wenn man den richtigen Schlüsseltyp benötigt -, aber es lohnt sich, den Überlegungen im Blog-Beitrag zu folgen. (Nachdem wir .NET erwähnt haben, ist es wert zu erklären, dass ein Teil des Grundes, warum es in .NET kein Problem ist, darin besteht, dass es das größere Problem in .NET gibt mehr begrenzte Varianz ...)
Der Vertrag kommt folgendermaßen zum Ausdruck:
Genauer gesagt, wenn diese Zuordnung eine Zuordnung von einem Schlüssel k zu einem Wert v enthält, sodass (key == null? K == null: key.equals (k)), gibt diese Methode v zurück ; Andernfalls wird null zurückgegeben. (Es kann höchstens eine solche Zuordnung geben.)
(meine Betonung)
daher hängt eine erfolgreiche Schlüsselsuche von der Implementierung der Gleichheitsmethode durch den Eingabeschlüssel ab. Das ist nicht unbedingt abhängig von der Klasse von k.
Es ist eine Anwendung von Postels Gesetz "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 Methode equals
ist in der Klasse Object
definiert und akzeptiert alle Object
als Parameter. Daher ist es sinnvoll, für die Schlüsseläquivalenz und für Operationen, die auf der Schlüsseläquivalenz basieren, jeden Object
-Typ zu akzeptieren.
Wenn eine Map Schlüsselwerte zurückgibt, werden mithilfe des type-Parameters so viele Typinformationen wie möglich gespeichert.
Ich denke, dieser Abschnitt des Generics 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:
interface Collection {
public boolean containsAll(Collection c);
...
}
Ein naiver Versuch, es zu generieren, ist:
interface Collection<E> {
public boolean containsAll(Collection<E> c);
...
}
Dies ist zwar sicher typsicher, entspricht aber nicht dem ursprünglichen Vertrag der API. Die containAll () -Methode funktioniert mit jeder Art von eingehender Sammlung. Es wird nur erfolgreich sein, wenn die eingehende Auflistung wirklich nur Instanzen von E enthält, aber:
Der Grund dafür ist, dass die Eindämmung durch equals
und hashCode
bestimmt wird, die Methoden für Object
sind und beide einen Object
- Parameter verwenden. Dies war ein früher Konstruktionsfehler in Javas Standardbibliotheken. Verbunden mit Einschränkungen in Javas Typsystem erzwingt es, dass alles, was auf equals und hashCode beruht, Object
annimmt.
Die einzige Möglichkeit, typsichere Hash-Tabellen und Gleichheit in Java) zu haben, besteht darin, Object.equals
Und Object.hashCode
Zu vermeiden und einen generischen Ersatz zu verwenden. Functional Java enthält zu diesem Zweck Typklassen: Hash<A>
und Equal<A>
. Ein Wrapper für HashMap<K, V>
wird bereitgestellt, die im Konstruktor Hash<K>
und Equal<K>
verwendet. Die Methoden get
und contains
dieser Klasse verwenden daher ein generisches Argument vom Typ K
.
Beispiel:
HashMap<String, Integer> h =
new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash);
h.add("one", 1);
h.get("one"); // All good
h.get(Integer.valueOf(1)); // Compiler error
Es gibt noch einen gewichtigen Grund, es kann technisch nicht getan werden, weil es die Map kaputt macht.
Java hat eine polymorphe generische Konstruktion wie <? extends SomeClass>
. Ein solcher Verweis kann auf einen mit <AnySubclassOfSomeClass>
Signierten Typ verweisen. Aber polymorphes Generikum macht diese Referenz readonly. Der Compiler erlaubt es Ihnen, generische Typen nur als Rückgabetyp von Methoden zu verwenden (wie einfache Getter), blockiert jedoch die Verwendung von Methoden, bei denen generischer Typ ein Argument ist (wie gewöhnliche Setter). Wenn Sie also Map<? extends KeyType, ValueType>
Schreiben, können Sie vom Compiler nicht die Methode get(<? extends KeyType>)
aufrufen, und die Map ist unbrauchbar. Die einzige Lösung besteht darin, diese Methode nicht generisch zu machen: get(Object)
.
Kompatibilität.
Bevor Generika verfügbar waren, gab es nur get (Object o).
Wenn sie diese Methode geändert hätten, um (<K> o) zu erhalten, hätte dies möglicherweise eine massive Codewartung für Java Benutzer erzwungen, nur um den Arbeitscode erneut kompilieren zu lassen.
Sie könnten haben eine zusätzliche Methode eingeführt, sagen get_checked (<K> o) und verwerfen die alte get () Methode, so dass es einen sanfteren Übergangspfad gab. Aber aus irgendeinem Grund wurde dies nicht getan. (Wir befinden uns derzeit in der Situation, 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, die sich auf die Semantik von .equals () beziehen, sind meiner Meinung nach falsch. (Technisch gesehen sind sie korrekt, aber ich denke immer noch, dass sie falsch sind. Kein Designer, der bei Verstand ist, wird jemals o1.equals (o2) true machen, wenn o1 und o2 keine gemeinsame Superklasse haben.)
Ich habe mir das angeschaut und überlegt, warum sie es so gemacht haben. Ich glaube nicht, dass eine der vorhandenen Antworten erklärt, warum sie die neue generische Schnittstelle nicht dazu bringen konnten, nur den richtigen Typ für den Schlüssel zu akzeptieren. Der eigentliche Grund ist, dass sie, obwohl sie Generika eingeführt haben, KEINE neue Schnittstelle erstellt haben. Die Map-Oberfläche ist dieselbe alte, nicht generische Map, die nur als generische und nicht generische Version dient. Auf diese Weise können Sie einer Methode, die eine nicht generische Map akzeptiert, einen Map<String, Customer>
Übergeben, und es würde weiterhin funktionieren. 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 der vorhandenen 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 Auflistungen selbst mit vorhandenen Methoden kompatibel wären, die Schnittstellen jedoch nicht.
Abwärtskompatibilität, denke ich. Map
(oder HashMap
) muss noch get(Object)
unterstützen.
Wir führen gerade große Umgestaltungen durch und haben dieses stark typisierte get () verpasst, um zu überprüfen, ob wir ein get () mit altem Typ nicht verpasst haben.
Aber ich habe einen Workaround/hässlichen Trick für die Überprüfung der Kompilierungszeit gefunden: Erstelle ein Map-Interface mit stark typisiertem get, enthältKey, entferne ... und füge es in das Java.util-Paket deines Projekts ein.
Sie erhalten Kompilierungsfehler, wenn Sie nur get () aufrufen. Bei falschen Typen scheint alles andere für den Compiler in Ordnung zu sein (zumindest in Eclipse kepler).
Vergessen Sie nicht, diese Schnittstelle nach der Überprüfung Ihres Builds zu löschen, da dies zur Laufzeit nicht gewünscht ist.