2016-07-15 27 views
17

Ich bekomme anscheinend doppelte Schlüssel in der standardmäßigen Java HashMap. Mit "duplizieren" meine ich, dass die Schlüssel durch ihre equals() Methode gleich sind. Hier ist die problematische Code:Warum bekomme ich doppelte Schlüssel in Java HashMap?

import java.util.Map; 
import java.util.HashMap; 

public class User { 
    private String userId; 
    public User(String userId) { 
     this.userId = userId; 
    } 
    public boolean equals(User other) { 
     return userId.equals(other.getUserId()); 
    } 
    public int hashCode() { 
     return userId.hashCode(); 
    } 
    public String toString() { 
     return userId; 
    } 

    public static void main(String[] args) { 
     User arvo1 = new User("Arvo-Part"); 
     User arvo2 = new User("Arvo-Part"); 
     Map<User,Integer> map = new HashMap<User,Integer>(); 
     map.put(arvo1,1); 
     map.put(arvo2,2); 

     System.out.println("arvo1.equals(arvo2): " + arvo1.equals(arvo2)); 
     System.out.println("map: " + map.toString()); 
     System.out.println("arvo1 hash: " + arvo1.hashCode()); 
     System.out.println("arvo2 hash: " + arvo2.hashCode()); 
     System.out.println("map.get(arvo1): " + map.get(arvo1)); 
     System.out.println("map.get(arvo2): " + map.get(arvo2)); 
     System.out.println("map.get(arvo2): " + map.get(arvo2)); 
     System.out.println("map.get(arvo1): " + map.get(arvo1)); 
    } 
} 

Und hier ist die resultierende Ausgabe:

arvo1.equals(arvo2): true 
map: {Arvo-Part=1, Arvo-Part=2} 
arvo1 hash: 164585782 
arvo2 hash: 164585782 
map.get(arvo1): 1 
map.get(arvo2): 2 
map.get(arvo2): 2 
map.get(arvo1): 1 

Wie Sie sehen können, die equals() Methode auf den beiden User Objekte zurückkehrt true und deren Hash-Codes sind die gleichen aber sie bilden jeweils eine eindeutige key in map. Weiterhin unterscheidet map weiterhin zwischen den beiden User Schlüsseln der letzten vier get() Anrufe.

Dies widerspricht direkt die documentation:

Formaler wenn diese Karte eine Zuordnung von einem Schlüssel k auf einen Wert v enthält, so dass (Taste == null k == null: key.equals (k)), dann gibt diese Methode v zurück; Andernfalls wird null zurückgegeben. (Es kann höchstens ein solches Mapping geben.)

Ist das ein Fehler? Fehle ich hier etwas? Ich benutze Java Version 1.8.0_92, die ich über Homebrew installiert habe.

EDIT: Diese Frage als Duplikat dieser other question markiert wurde, aber ich werde diese Frage lassen wie es ist, weil es eine scheinbare Unvereinbarkeit mit equals() identifiziert, während die andere Frage der Fehler übernimmt mit hashCode() liegt. Hoffentlich macht das Vorhandensein dieser Frage das Problem leichter durchsuchbar.

+13

Versuchen Hinzufügen '@ Override' zu' equals' und 'hashCode' Methoden (immer eine Best Practice) und sehen, ob Sie irgendwelche hilfreiche Informationen erhalten. – chrylis

+2

Um diese Art von Tippfehlern oder Fehlern in Zukunft zuzulassen, lassen Sie Ihre IDE immer die Methoden für Sie generieren. Dann zwicke sie, damit sie so aussehen, wie du es willst. Dies hätte die richtigen Methoden mit '@ Override'-Annotationen erzeugt. – Magnilex

Antwort

28

Das Problem in Ihrer equals() Methode liegt. Die Signatur von Object.equals() ist equals(OBJECT), aber in Ihrem Fall ist es equals(USER), also sind dies zwei völlig verschiedene Methoden und die hashmap ruft die mit Object Parameter. Sie können dies überprüfen, indem Sie eine @Override-Annotation über Ihr Gleichheitszeichen setzen. Dadurch wird ein Compilerfehler generiert.

Die Methode equals sein sollte:

@Override 
    public boolean equals(Object other) { 
    if(other instanceof User){ 
     User user = (User) other; 
     return userId.equals(user.userId); 
    } 

    return false; 
} 

Als Best Practice Sie immer @Override auf die Methoden setzen, sollten Sie außer Kraft setzen - es Ihnen eine Menge Ärger sparen können.

+0

Die interessante Frage für mich: Sollte das dynamische Tippen nicht die beste Übereinstimmung für den Anruf nennen? 'Objekt o = neuer Benutzer(); -> o.equals (o) 'könnte an die beste Übereinstimmung für die Laufzeittypen übergeben werden. - Aber Java verwendet statische Kompilierzeit-Typisierung, um die richtige Überladung, nicht dynamische Laufzeitparameter zu finden - Sie könnten das in Ihrer Antwort auf Vollständigkeit aufnehmen! – Falco

+0

Es wird zu dem besten "Kompilierzeit" -Typ gesendet. Java hat keine dynamische Typisierung –

+1

Hmm, Sie vermissen, dass die Hasmap kompiliert wird, um immer '' equals (object) 'aufzurufen, also wenn Sie' equals (user) 'haben, wird es nie aufrufen. Aber wenn du in deinem Code 'User user = new User(); user.equals (new User()) 'wird das' equals (User) 'aufgerufen. Aber wenn Sie wie in Ihrem Beispiel tun: Objektbenutzer = neuer Benutzer(); user.equals (new User()) wird das 'equals (object)' aufgerufen. Ich hoffe, das löscht es –

13

Ihre Equals-Methode überschreibt nicht equals, und die Typen in Map werden zur Laufzeit gelöscht, sodass die tatsächliche equals-Methode equals(Object) aufgerufen wird. Ihre Gleichen aussehen sollen wie folgt:

@Override 
public boolean equals(Object other) { 
    if (!(other instanceof User)) 
     return false; 
    User u = (User)other; 
    return userId.equals(u.userId); 
} 
3

OK, also zuerst, der Code kompiliert nicht. Fehlende diese Methode:

other.getUserId() 

Aber abgesehen davon, dass Sie zu @Override equals Methode benötigen, IDE wie Eclipse auch helfen kann, btw equals und hashCode zu erzeugen.

@Override 
public boolean equals(Object obj) 
{ 
    if(this == obj) 
    return true; 
    if(obj == null) 
    return false; 
    if(getClass() != obj.getClass()) 
    return false; 
    User other = (User) obj; 
    if(userId == null) 
    { 
    if(other.userId != null) 
     return false; 
    } 
    else if(!userId.equals(other.userId)) 
    return false; 
    return true; 
} 
2

Als Chrylis vorgeschlagen, durch Zugabe der @Override sowohl hashCode und equals Sie einen Kompilierungsfehler erhalten, weil die Unterschrift der equals Methode public boolean equals(Object other) ist, so dass Sie nicht wirklich überschreiben den Standard (von der Objektklasse) gleich Methode. Dies führt dazu, dass beide Benutzer im selben Bucket innerhalb der hashMap (HashCode ist überschrieben und beide Benutzer haben den gleichen Hash-Code), aber wenn sie auf Gleichheit überprüft werden, sind sie unterschiedlich wie die Standard-Equals-Methode verwendet wird, was bedeutet, dass Speicheradressen werden verglichen.

Ersetzen Sie die equals Methode mit dem folgenden das erwartete Ergebnis zu erhalten:

@Override 
public boolean equals(Object other) { 
    return getUserId().equals(((User)other).getUserId()); 
} 
+2

'equals' sollte nicht geworfen werden (Ihre Version wird, wenn die Typen nicht übereinstimmen). – Hulk

3

Wie andere beantwortet Sie ein Problem mit dem equals Methodensignatur hatte. Nach Java ist gleich Best Practice sollten Sie implementieren gleich wie folgt aus:

@Override 
    public boolean equals(Object o) { 
    if (this == o) return true; 
    if (o == null || getClass() != o.getClass()) return false; 

    User user = (User) o; 

    return userId.equals(user.userId); 
    } 

Gleiche gilt für die hashCode() Methode. siehe Overriding equals() and hashCode() method in Java

Das zweite Problem

Sie haben keine Duplikate mehr jetzt, aber Sie haben ein neues Problem, Ihr HashMap enthält nur ein Element:

map: {Arvo-Part=2} 

Dies liegt daran, Beide User Objekte verweisen auf den gleichen String (JVM String Interning), und aus der Perspektive HashMap Ihre beiden Objekte sind die gleichen, da beide Objekte eq sind uvalent in Hashcode und gleich Methoden. Wenn Sie also Ihr zweites Objekt zu HashMap hinzufügen, überschreiben Sie Ihr erstes Objekt. dieses Problem zu vermeiden, stellen Sie sicher, dass Sie eine eindeutige ID für jeden Benutzer verwenden

Eine einfache Demonstration auf die Benutzer:

enter image description here