2016-07-15 16 views
17

Wydaje się, że otrzymuję zduplikowane klucze w standardowej Java HashMap. Przez "duplikat" rozumiem, że klucze są równe ich metodzie equals(). Oto kod problematyczne:Dlaczego otrzymuję zduplikowane klucze w 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)); 
    } 
} 

A oto Wynikiem:

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 

Jak widać, metoda equals() na dwóch User obiektów wraca true i ich kody hash są takie same , ale każdy z nich tworzy odrębną key w map. Ponadto map nadal rozróżnia dwa klucze User w ostatnich czterech połączeniach get().

to bezpośrednio zaprzecza documentation:

Bardziej formalnie, czy ta mapa zawiera mapowania z klucza k do wartości v takie, że (key == null k == null? Key.equals (k)), wówczas ta metoda zwraca v; w przeciwnym razie zwraca wartość null. (Może być co najwyżej jedno takie mapowanie.)

Czy to błąd? Czy coś mi umyka? Używam wersji Java 1.8.0_92, którą zainstalowałem przez Homebrew.

EDIT: To pytanie zostało oznaczone jako duplikat tego other question, ale zostawię to pytanie jak to jest, ponieważ identyfikuje pozorną niespójność z equals(), natomiast inne pytanie zakłada, że ​​błąd leży po hashCode(). Mam nadzieję, że obecność tego pytania sprawi, że ten problem będzie łatwiejszy do przeszukiwania.

+13

spróbuj dodać '@ Override' do' equals' oraz metody 'hashCode' (zawsze najlepsza praktyka) i zobacz, czy uzyskasz przydatne informacje. – chrylis

+2

Aby pozwolić na ten rodzaj literówek lub błędów w przyszłości, pozwól, aby twoje IDE generowało metody dla ciebie. Następnie dostosuj je, aby wyglądały tak, jak chcesz. W ten sposób powstałyby poprawne metody z adnotacjami "@ Override". – Magnilex

Odpowiedz

28

Problem leży w Twojej metodzie equals(). Podpis Object.equals() to equals(OBJECT), ale w twoim przypadku jest to equals(USER), więc są to dwie zupełnie różne metody, a hashmap wywołuje tę z parametrem Object. Możesz to sprawdzić, umieszczając adnotację @Override na równych sobie - spowoduje to błąd kompilatora.

równości metoda powinna być:

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

    return false; 
} 

Jako najlepszych praktyk należy zawsze umieścić @Override na metodach zastąpić - może to zaoszczędzić sporo kłopotów.

+0

Interesujące pytanie dla mnie: czy dynamiczne pisanie nie powinno wywoływać najlepszego dopasowania do połączenia? 'Object o = new User(); -> o.equals (o) 'może zostać wysłane do najlepszego dopasowania dla typów runtime. - Ale Java używa statycznego pisania w czasie kompilacji w celu znalezienia właściwego przeciążenia, a nie dynamicznych parametrów środowiska wykonawczego - możesz to uwzględnić w odpowiedzi na kompletność! – Falco

+0

Zostanie wysłany do najlepszego typu 'czas kompilacji '. java nie ma dynamicznego pisania –

+1

Hmm, brakuje ci tego, że hasmap jest skompilowany, aby wywoływać zawsze "równy (obiekt)", więc jeśli masz 'equals (użytkownik)' to nigdy go nie wywoła. Ale jeśli w swoim kodzie robisz "User user = new User(); user.equals (new User()) ', zostanie wywołany' 'equals (User)'. Ale jeśli zrobisz to w swoim przykładzie 'Object user = new User(); user.equals (new User()) zostanie wywołany '' 'equals (object)'. Mam nadzieję, że to wyczyści –

13

Twoja metoda equals nie zastępują equals i typy w Map są usuwane w czasie wykonywania, więc rzeczywista równa metoda zwana jest equals(Object). Twoje równe wartości powinny wyglądać mniej więcej tak:

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

OK, więc po pierwsze, kod się nie kompiluje. Brakuje tej metody:

other.getUserId() 

Ale poza tym, trzeba @Override equals metody, jak Eclipse IDE może również pomóc w generowaniu equals i hashCode btw.

@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

Jak sugeruje Chrylis, dodając @Override zarówno hashCode i equals dostaniesz błąd kompilacji, ponieważ podpis metody equals jest public boolean equals(Object other), więc nie są faktycznie przesłanianie domyślne (z klasy Object) równa się metoda. Prowadzi to do sytuacji, w której obaj użytkownicy kończą w tym samym segmencie wewnątrz hashMap (hashCode jest overriden i obaj użytkownicy mają ten sam kod skrótu), ale gdy są zaznaczeni na równość są różne, ponieważ używana jest domyślna metoda equals, co oznacza, że adresy pamięci są porównywane.

Wymień metody equals z następujących czynności, aby uzyskać oczekiwany rezultat:

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

'equals' nie powinno rzucać (twoja wersja będzie, jeśli typy nie pasują). – Hulk

3

Jak inni odpowiedział miałeś problem z podpisem equals metody. Według Java równa najlepszych praktyk należy wdrożyć równa jak następuje:

@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); 
    } 

To samo odnosi się do sposobu hashCode(). zobacz Overriding equals() and hashCode() method in Java

Drugi problem

nie masz duplikaty już teraz, ale masz nowy problem, twój HashMap zawiera tylko jeden element:

map: {Arvo-Part=2} 

to dlatego, zarówno obiekty User odwołują się do tego samego ciągu znaków (JVM String Interning), jak iz perspektywy HashMap oba obiekty są takie same, ponieważ oba obiekty są równe dwuwartościowy w hashcode i równa się metodom. więc kiedy dodasz drugi obiekt do HashMap, zastępujesz pierwszy. aby uniknąć tego problemu, upewnij się, że używasz unikalny identyfikator dla każdego Użytkownika

prosty przykład na swoich użytkowników:

enter image description here

Powiązane problemy