Was sind die Gründe, warum Map.get (Objektschlüssel) nicht (vollständig) generisch ist?

405

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).

WMR
quelle
3
Ähnliche Frage bezüglich Sammlung: stackoverflow.com/questions/104799/…
AlikElzin-kilaka
1
Tolle. Ich benutze Java seit über 20 Jahren und heute erkenne ich dieses Problem.
GhostCat

Antworten:

260

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 übergeben get(). Die Spezifikation der Methode erfordert nur, dass sie gleich sind. Dies folgt daraus, wie die equals()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ür List.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 handelt List. Wenn wir also auf das Beispiel in dieser Frage zurückkommen, ist es gemäß der Spezifikation der Methode möglich, ein Map<ArrayList, Something>und für mich get()mit einem LinkedListas-Argument aufzurufen , und es sollte den Schlüssel abrufen, der eine Liste mit demselben Inhalt ist. Dies wäre nicht möglich, wenn get()der Argumenttyp generisch und eingeschränkt wäre.

newacct
quelle
28
Warum ist dann V Get(K k)in C #?
134
Die Frage ist, wenn Sie anrufen möchten m.get(linkedList), warum haben Sie den mTyp nicht als definiert Map<List,Something>? Ich kann mir keinen Anwendungsfall vorstellen, in dem ein Aufruf m.get(HappensToBeEqual)ohne Änderung des MapTyps für eine Schnittstelle sinnvoll ist.
Elazar Leibovich
58
Wow, schwerwiegender Designfehler. Sie erhalten auch keine Compiler-Warnung, vermasselt. Ich stimme Elazar zu. Wenn dies wirklich nützlich ist, was ich bezweifle, dass es häufig vorkommt, klingt ein getByEquals (Objektschlüssel) vernünftiger ...
mmm
37
Diese Entscheidung scheint eher auf der Grundlage der theoretischen Reinheit als der Praktikabilität getroffen worden zu sein. Für die meisten Verwendungszwecke würden Entwickler das Argument lieber durch den Vorlagentyp begrenzt sehen, als es unbegrenzt zu haben, um Randfälle wie den von newacct in seiner Antwort erwähnten zu unterstützen. Das Verlassen der nicht vorlagenbezogenen Signaturen verursacht mehr Probleme als es löst.
Sam Goldberg
14
@newacct: "Perfect Type Safe" ist eine starke Behauptung für ein Konstrukt, das zur Laufzeit unvorhersehbar ausfallen kann. Beschränken Sie Ihre Ansicht nicht auf Hash-Maps, die damit funktionieren. TreeMapkann fehlschlagen, wenn Sie Objekte des falschen Typs an die getMethode übergeben, aber gelegentlich übergeben, z. B. wenn die Karte zufällig leer ist. Und noch schlimmer, im Falle einer Lieferung kann Comparatordie compareMethode (die eine generische Signatur hat!) Ohne ungeprüfte Warnung mit Argumenten des falschen Typs aufgerufen werden. Das ist kaputtes Verhalten.
Holger
105

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 Setstatt Map). Der relevanteste Satz:

Einheitlich beschränken Methoden des Java Collections Framework (und auch der Google Collections Library) niemals die Typen ihrer Parameter, es sei denn, es ist erforderlich, um zu verhindern, dass die Sammlung beschädigt wird.

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 ...)

Jon Skeet
quelle
4
Apocalisp: Das stimmt nicht, die Situation ist immer noch dieselbe.
Kevin Bourrillion
9
@ user102008 Nein, der Beitrag ist nicht falsch. Auch wenn a Integerund a Doubleniemals gleich sein können, ist es dennoch eine faire Frage, ob a Set<? extends Number>den Wert enthält new Integer(5).
Kevin Bourrillion
33
Ich wollte noch nie die Mitgliedschaft in einem überprüfen 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.
Porculus
4
@EarthEngine: Es war schon immer kaputt. Das ist der springende Punkt - der Code ist kaputt, aber der Compiler kann ihn nicht abfangen.
Jon Skeet
1
Und es ist immer noch kaputt und hat uns nur einen Fehler verursacht ... eine großartige Antwort.
GhostCat
28

Der Vertrag wird folgendermaßen ausgedrückt:

Wenn diese Zuordnung eine Zuordnung von einem Schlüssel k zu einem Wert v enthält, so dass (key == null? K == null: key.equals (k) ), gibt diese Methode v; 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 hängt nicht unbedingt von der Klasse von k ab.

Brian Agnew
quelle
4
Es ist auch abhängig von hashCode(). Ohne eine ordnungsgemäße Implementierung von hashCode () ist eine gut implementierte equals()Version in diesem Fall eher nutzlos.
Rudolfson
5
Ich denke, im Prinzip könnten Sie damit einen einfachen Proxy für einen Schlüssel verwenden, wenn die Neuerstellung des gesamten Schlüssels unpraktisch wäre - solange equals () und hashCode () korrekt implementiert sind.
Bill Michell
5
@rudolfson: Soweit mir bekannt ist, ist nur eine HashMap auf den Hash-Code angewiesen, um den richtigen Bucket zu finden. Eine TreeMap verwendet beispielsweise einen binären Suchbaum und kümmert sich nicht um hashCode ().
Rob
4
Streng genommen get()muss kein typisches Argument verwendet werden Object, um den Kontakt zu befriedigen. Stellen Sie sich vor, die get-Methode wäre auf den Schlüsseltyp beschränkt K- der Vertrag wäre weiterhin gültig. Natürlich können Verwendungen, bei denen der Kompilierungszeittyp keine Unterklasse von war K, jetzt nicht kompiliert werden, aber dies macht den Vertrag nicht ungültig, da Verträge implizit diskutieren, was passiert, wenn der Code kompiliert wird.
BeeOnRope
16

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 equalsMethode ist auf der definiertObject Klasse und akzeptiert alle Objectals Parameter. Daher ist es sinnvoll, dass Schlüsseläquivalenz und Operationen, die auf Schlüsseläquivalenz basieren, einen beliebigen ObjectTyp akzeptieren .

Wenn eine Karte Schlüsselwerte zurückgibt, werden mithilfe des Typparameters so viele Typinformationen wie möglich gespeichert.

erickson
quelle
4
Warum ist dann V Get(K k)in C #?
1
Es ist 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, aber Get()und Remove()nur eine passende Art zu akzeptieren sicher verhindert , dass Sie versehentlich einen falschen Wert im Vorbeigehen.
Wormbo
26
Es ist eine falsche Anwendung des Postelschen Gesetzes. Sei liberal in dem, was du von anderen akzeptierst, aber nicht zu liberal. Diese idiotische API bedeutet, dass Sie den Unterschied zwischen "nicht in der Sammlung" und "Sie haben einen statischen Tippfehler gemacht" nicht erkennen können. Viele tausend verlorene Programmierstunden hätten mit get: K -> boolean verhindert werden können.
Judge Mental
1
Das hätte natürlich sein sollen contains : K -> boolean.
Richter Mental
4
Postel war falsch .
Alnitak
13

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:

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}

Ein naiver Versuch, es zu erzeugen, ist:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}

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:

  • Der statische Typ der eingehenden Sammlung kann unterschiedlich sein, möglicherweise weil der Anrufer den genauen Typ der übergebenen Sammlung nicht kennt oder weil es sich um eine Sammlung <S> handelt, wobei S ein Subtyp von E ist.
  • Es ist absolut legitim, includesAll () mit einer Sammlung eines anderen Typs aufzurufen. Die Routine sollte funktionieren und false zurückgeben. "
Yardena
quelle
2
warum dann nicht containsAll( Collection< ? extends E > c )?
Richter Mental
1
@JudgeMental, obwohl oben nicht als Beispiel angegeben, ist es auch notwendig, containsAllmit einem Collection<S>where Seinen Supertyp von zuzulassen E. Dies wäre nicht erlaubt, wenn es so wäre containsAll( 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 sein false).
Davmac
Es sollte nicht notwendig sein, includesAll mit einer Sammlung eines Supertyps von E zuzulassen. Ich behaupte, dass es notwendig ist, diesen Aufruf mit einer statischen Typprüfung zu verbieten, um einen Fehler zu verhindern. Es ist ein dummer Vertrag, von dem ich denke, dass er der Punkt der ursprünglichen Frage ist.
Richter Mental
6

Der Grund dafür ist, dass die Eindämmung durch equalsund für hashCodewelche Methoden bestimmt wird Objectund beide einen ObjectParameter 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 ist Object.

Der einzige Weg , typsichere Hash - Tabellen und Gleichheit in Java haben , ist zu vermeiden Object.equalsund Object.hashCodeund einen generischen Ersatz zu verwenden. Funktionales Java wird zu diesem Zweck mit Typklassen geliefert: Hash<A>und Equal<A>. Es wird ein Wrapper für HashMap<K, V>bereitgestellt, der Hash<K>und Equal<K>in seinem Konstruktor enthält. Diese Klassen getund containsMethoden 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
Apocalisp
quelle
4
Dies an sich verhindert nicht, dass der Typ von 'get' als "V get (K key)" deklariert wird, da 'Object' immer ein Vorfahr von K ist und "key.hashCode ()" also weiterhin gültig ist.
Finnw
1
Es verhindert es zwar nicht, aber ich denke, es erklärt es. Wenn sie die Methode equals umstellten, um die Klassengleichheit zu erzwingen, konnten sie den Leuten sicherlich nicht sagen, dass der zugrunde liegende Mechanismus zum Lokalisieren des Objekts in der Karte equals () und hashmap () verwendet, wenn die Methodenprototypen für diese Methoden nicht kompatibel sind.
CGP
5

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.)

Erwin Smout
quelle
4

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 schreiben Map<? extends KeyType, ValueType>, können Sie vom Compiler keine Methode aufrufen get(<? extends KeyType>), und die Zuordnung ist unbrauchbar. Die einzige Lösung besteht darin, diese Methode nicht generisch zu machen : get(Object).

Owheee
quelle
Warum ist die Set-Methode dann stark typisiert?
Sentenza
Wenn Sie 'put' meinen: Die put () -Methode ändert die Map und ist mit Generika wie <? nicht verfügbar. erweitert SomeClass>. Wenn Sie es aufrufen, haben Sie eine Kompilierungsausnahme. Eine solche Karte wird "schreibgeschützt" sein
Owheee
1

Abwärtskompatibilität, denke ich. Map(oder HashMap) muss noch unterstützen get(Object).

Anton Gogolev
quelle
13
Es könnte jedoch das gleiche Argument angeführt werden put(was die generischen Typen einschränkt). Durch die Verwendung von Rohtypen erhalten Sie Abwärtskompatibilität. Generika sind "Opt-In".
Thilo
Persönlich denke ich, dass der wahrscheinlichste Grund für diese Entwurfsentscheidung die Abwärtskompatibilität ist.
Geekdenz
1

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.

Stilgar
quelle
0

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.

Henva
quelle