2016-07-15 133 views
17

Mi sembra di avere chiavi duplicate nella HashMap standard di Java. Per "duplicato", intendo che i tasti sono uguali per il loro metodo equals(). Ecco il codice problematico:Perché sto ottenendo chiavi duplicate 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)); 
    } 
} 

Ed ecco l'output risultante:

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 

Come si può vedere, il metodo equals() sui due User oggetti sta tornando true ed i loro codici hash sono gli stessi , tuttavia ognuno di essi forma uno key distinto in map. Inoltre, map continua a distinguere tra le due chiavi User nelle ultime quattro chiamate get().

Questo contraddice direttamente la documentation:

Più formalmente, se questa mappa contiene una mappatura da una chiave k ad un valore tale che v (tasto == null k == null:? Key.equals (k)), quindi questo metodo restituisce v; altrimenti restituisce null. (Ci può essere al massimo una tale mappatura.)

Si tratta di un errore? Mi sto perdendo qualcosa qui? Sto eseguendo la versione 1.8.0_92 di Java, che ho installato tramite Homebrew.

EDIT: Questa domanda è stato contrassegnato come un duplicato di questo other question, ma lascio questa domanda è perché identifica un'incongruenza apparente con equals(), mentre l'altra questione assume l'errore si trova con hashCode(). Speriamo che la presenza di questa domanda renda la ricerca più facile.

+13

Prova ad aggiungere '@ Override' al vostro' equals' e metodi 'hashCode' (sempre una best practice) e vedere se si ottengono informazioni utili. – chrylis

+2

Per consentire questo tipo di errori di battitura, o errori, in futuro, lasciare sempre che l'IDE generi i metodi per te. Quindi modificali per farli apparire come desideri. Questo avrebbe creato i metodi corretti con annotazioni '@ Override'. – Magnilex

risposta

28

Il problema risiede nel metodo equals(). La firma di Object.equals() è equals(OBJECT), ma nel tuo caso è equals(USER), quindi questi sono due metodi completamente diversi e l'hashmap sta chiamando quello con il parametro Object. Puoi verificarlo inserendo un'annotazione @Override sui tuoi pari - genererà un errore del compilatore.

Il metodo dovrebbe essere pari a:

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

    return false; 
} 

Come procedura si dovrebbe sempre mettere @Override sui metodi di ignorare - si può risparmiare un sacco di guai.

+0

La domanda interessante per me: non dovrebbe la digitazione dinamica chiamare la migliore corrispondenza per la chiamata? 'Oggetto o = nuovo utente(); -> o.equals (o) 'potrebbe essere inviato alla migliore corrispondenza per i tipi di runtime. - Ma Java usa la tipizzazione statica in fase di compilazione per trovare il giusto sovraccarico, non i parametri di runtime dinamici - potresti includerlo nella tua risposta per completezza! – Falco

+0

Sarà inviato al miglior tipo di 'tempo di compilazione'. java non ha una digitazione dinamica –

+1

Hmm, ti manca che l'hasmap sia compilato per chiamare sempre 'equals (object)', quindi se hai 'equals (user)' non lo chiamerà mai. Ma se nel tuo codice fai 'User user = new User(); user.equals (new User()) ', verrà chiamato' equals (User) '. Ma se fai come nel tuo esempio 'Oggetto utente = nuovo Utente(); user.equals (new User()) 'verrà chiamato' equals (oggetto) '. Spero che lo cancelli –

13

Il metodo equals non sovrascrive equals ei tipi nello Map vengono cancellati in fase di runtime, quindi il metodo di equals effettivo chiamato è equals(Object). I suoi eguali dovrebbe apparire più simile a questo:

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

OK, quindi prima di tutto, il codice non viene compilato. Manca questo metodo:

other.getUserId() 

Ma a parte questo, è necessario @Override equals metodo, IDE come Eclipse può anche contribuire a generare equals e 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

Come suggerito Chrylis, aggiungendo la @Override sia hashCode e equals si otterrà un errore di compilazione perché la firma del metodo equals è public boolean equals(Object other), quindi non sono in realtà sovrascrivendo il default (dalla classe Object) metodo uguale Questo porta alla situazione in cui entrambi gli utenti finiscono nello stesso bucket all'interno dello hashMap (hashCode è sovrascritto e entrambi gli utenti hanno lo stesso codice hash), ma quando vengono controllati per l'uguaglianza sono diversi in quanto viene utilizzato il metodo di default uguale che significa che il gli indirizzi di memoria sono confrontati.

sostituire il metodo equals con il seguente per ottenere il risultato atteso:

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

'equals' non deve essere lanciato (la tua versione verrà eseguita se i tipi non corrispondono). – Hulk

3

Come altri risposto hai avuto un problema con la firma equals metodo. Secondo Java è uguale migliori pratiche è necessario implementare uguale come la seguente:

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

Stessa cosa vale per il metodo hashCode(). vedi Overriding equals() and hashCode() method in Java

Il secondo problema

non devi duplicati più ora, ma avete un nuovo problema, la vostra HashMap contiene un solo elemento:

map: {Arvo-Part=2} 

Questo perché entrambi gli oggetti User fanno riferimento alla stessa stringa (JVM String Interning) e dalla prospettiva HashMap i due oggetti sono uguali, poiché entrambi gli oggetti sono eq uivalente nei metodi hashcode e equals. quindi quando aggiungi il tuo secondo oggetto a HashMap, sostituisci il tuo primo. Per evitare questo problema, assicurarsi di utilizzare un ID univoco per ogni utente

Una semplice dimostrazione sugli utenti:

enter image description here

Problemi correlati