2009-12-12 11 views
26

ho questo codice di prova:Comprendere il funzionamento del pari e hashCode in una HashMap

import java.util.*; 

class MapEQ { 

    public static void main(String[] args) { 
    Map<ToDos, String> m = new HashMap<ToDos, String>(); 
    ToDos t1 = new ToDos("Monday"); 
    ToDos t2 = new ToDos("Monday"); 
    ToDos t3 = new ToDos("Tuesday"); 
    m.put(t1, "doLaundry"); 
    m.put(t2, "payBills"); 
    m.put(t3, "cleanAttic"); 
    System.out.println(m.size()); 
} } 

class ToDos{ 

    String day; 

    ToDos(String d) { day = d; } 

    public boolean equals(Object o) { 
     return ((ToDos)o).day == this.day; 
} 

// public int hashCode() { return 9; } 
} 

Quando // public int hashCode() { return 9; } sia commentata m.size() restituisce 2, se la si lascia commentato restituisce tre. Perché?

risposta

41

Hai superato equals senza annullare hashCode. È necessario assicurarsi che in tutti i casi in cui equals restituisce true per due oggetti, hashCode restituisce lo stesso valore. Il codice hash è un codice che deve essere uguale se due oggetti sono uguali (l'inverso non deve essere vero). Quando inserisci il valore hard-coded di 9 in, soddisfi nuovamente il contratto.

Nella tua mappa hash, l'uguaglianza viene testata solo all'interno di un bucket hash. I tuoi due oggetti del lunedì dovrebbero essere uguali, ma poiché restituiscono diversi codici hash, il metodo equals non viene nemmeno chiamato per determinare la loro uguaglianza - vengono messi direttamente in diversi bucket e la possibilità che siano uguali non è nemmeno presa in considerazione .

+18

Vale anche la pena notare che la funzione di uguale è implementata in modo errato. Funziona solo su questo caso perché i due letterali stringa "Monday" sono internati e quindi hanno lo stesso riferimento. –

+1

Sì, ben individuato! Abbastanza corretto. –

+0

Il metodo equals dovrebbe anche essere in grado di gestire 'null' e gli argomenti di classi diverse (sottotipi di nota). Se si mantiene 'equals' (e' hashCode') in un modulo standard, allora è banale implementarlo correttamente (anche una macchina potrebbe codificarlo!). –

6

Quando non si esegue l'override del metodo hashCode(), la classe ToDos eredita il metodo hashCode() predefinito da Object, che assegna a ogni oggetto un codice hash distinto. Ciò significa che t1 e t2 hanno due codici hash diversi, anche se dovessi confrontarli, sarebbero uguali. A seconda della particolare implementazione di hashmap, la mappa è libera di memorizzarli separatamente (e questo è in effetti ciò che accade).

Quando si esegue correttamente l'override del metodo hashCode() per garantire che gli oggetti uguali ottengano i codici hash uguali, l'hashmap è in grado di trovare i due oggetti uguali e posizionarli nello stesso bucket hash.

Una migliore attuazione darebbe gli oggetti che sono non pari diversi codici hash, come questo:

public int hashCode() { 
    return (day != null) ? day.hashCode() : 0; 
} 
+0

Perché dici che è "meglio" dare oggetti che non sono uguali codici hash diversi? –

+2

Il codice hash viene utilizzato da HashMap per trovare rapidamente gli oggetti senza dover confrontare ogni oggetto. Se tutti i tuoi hashcode sono uguali, farà sì che tutto vada nello stesso bucket, rallentando molto le cose. –

+4

Perché questo riduce il numero di collisioni hash, che è vitale per una buona prestazione. Nel caso estremo di hashCode() che restituisce lo stesso valore per tutti gli oggetti, la HashMap degenera in una lista, e tutte quelle belle app di O (1) sono improvvisamente O (n). –

0

Quando hashCode è commentata, HashMap vede t1 e t2 come la stessa cosa; quindi, il valore di t2 clobbers quello di t1. Per capire come funziona, tieni presente che quando hashCode restituisce la stessa cosa per due istanze, finiscono per passare allo stesso bucket HashMap. Quando si tenta di inserire una seconda cosa nello stesso bucket (in questo caso t2 viene inserito quando t1 è già presente), HashMap esegue la scansione del bucket per un'altra chiave uguali. Nel tuo caso, t1 e t2 sono uguali perché hanno lo stesso giorno. A quel punto, i "salari" chiedono "doLaundry". Per quanto riguarda se t2 clobbers t1 come la chiave, credo che questo non è definito; quindi, entrambi i comportamenti sono ammessi.

Ci sono alcune cose importanti a cui pensare qui:

  1. sono due casi todos davvero uguali solo perché hanno lo stesso giorno della settimana?
  2. Ogni volta che si implementa equals, è necessario implementare hashCode in modo che qualsiasi due oggetti uguali abbiano gli stessi valori hashCode. Questa è un'ipotesi fondamentale che HashMap fa. Questo probabilmente è anche vero per qualsiasi altra cosa che si basa sul metodo hashCode.
  3. Progettare il metodo hashCode in modo che i codici hash siano equamente distribuiti; in caso contrario, non otterrai i vantaggi prestazionali dell'hashing. Da questa prospettiva, il ritorno 9 è una delle cose peggiori che puoi fare.
6

Non posso sottolineare abbastanza che è necessario leggere Chapter 3 in Effective Java (avviso: collegamento pdf). In questo capitolo imparerai tutto ciò che devi sapere sui metodi di sovrascrittura in Object e, in particolare, sul contratto equals. Josh Bloch ha un'ottima ricetta per ignorare il metodo equals che dovresti seguire. E ti aiuterà a capire perché dovresti usare equals e non == nella tua particolare implementazione del metodo equals.

Spero che questo aiuti. PER FAVORE LEGGETE. (Almeno i primi elementi della coppia ... e poi vorrai leggere il resto :-).

-Tom

+0

Per non parlare di una ricetta per l'override di hashCode()! Avviso – Tom

+1

: collegamento morto –

+1

nuovo collegamento http://uet.vnu.edu.vn/~chauttm/e-books/java/Effective.Java.2nd.Edition.May.2008.3000th.Release.pdf Era piuttosto lungo capitolo ma ben scritto e l'ho capito, vale la pena leggere –

3

quando si commenta, restituisce 3;

perché hashCode() ereditato dall'Oggetto è SOLO chiamato che restituisce 3 diversi hashcode per i 3 oggetti ToDos. Gli hashcode non uguali indicano che i 3 oggetti sono destinati a diversi bucket ed equals() restituiscono false poiché sono il primo concorrente nei rispettivi bucket. Se i codici hash sono diversi, si intende in anticipo che gli oggetti non sono uguali. Andranno in diversi secchi.

quando si decommenta, restituisce 2;

perché qui viene chiamato l'hashCode() sottoposto a override, che restituisce lo stesso valore per tutti i ToDos e dovranno passare tutti in un bucket, collegati linearmente. Gli hash code uguali non promettono nulla sull'eguaglianza o disuguaglianza degli oggetti.

hashCode() per t3 è 9 e poiché è il primo operatore, equals() è falso e t3 inserito nel secchio- dire bucket0.

Quindi t2 ottenendo lo stesso hashCode() come 9 è destinato allo stesso bucket0, un successivo equals() sul t3 già residente in bucket0 restituisce false con la definizione di uguale overridden().

Ora t1 con hashCode() come 9 è anche destinato a bucket0 e una successiva chiamata equals() restituisce true se confrontato con il t2 preesistente nello stesso bucket. t1 non riesce a entrare nella mappa. Quindi la dimensione netta di carta è di 2 -> {ToDos @ 9 = cleanAttic, Todos @ 9 = payBills}

Questo spiega l'importanza di attuare entrambe equals() e hashCode(), in modo tale che il i campi presi nella determinazione degli uguali() devono essere presi anche quando si determina hashCode(). Questo garantirà che se due oggetti sono uguali avranno sempre gli stessi hashCode. hashCodes non dovrebbe essere percepito come numeri pseudo-casuali poiché devono essere coerenti con gli uguali()

0

Piuttosto che pensare a hashCode in termini di mappatura hash-bucket, penso che sia più utile pensare in modo un po 'più astratto: un'osservazione che due oggetti hanno codici hash diversi costituisce un'osservazione che gli oggetti non sono uguali. Di conseguenza, un'osservazione che nessuno degli oggetti in una raccolta ha un particolare codice hash costituisce un'osservazione che nessuno degli oggetti in una raccolta è uguale a qualsiasi oggetto che abbia quel codice hash. Inoltre, un'osservazione che nessuno degli oggetti in una raccolta ha un codice hash con qualche tratto costituisce un'osservazione che nessuno di loro è uguale a qualsiasi oggetto che lo fa.

Le tabelle di hash funzionano generalmente definendo una famiglia di tratti, esattamente uno dei quali sarà applicabile al codice hash di ciascun oggetto (ad es."essere congruente a 0 mod 47", "essere congruente a 1 mod 47", ecc.), e quindi avere una collezione di oggetti con ogni tratto. Se a qualcuno viene dato un oggetto e può determinare quale tratto si applica ad esso, si può sapere che deve essere in una collezione di cose con quel tratto.

Queste tabelle di hash utilizzano generalmente una sequenza di bucket numerati è un dettaglio di implementazione; l'essenziale è che il codice hash di un oggetto sia rapidamente utilizzato per identificare molte cose alle quali non può essere uguale e con cui non dovrà quindi essere confrontato.

2

Secondo Effective Java,

Sempre sovrascrivere hashCode() quando si sovrascrive equals()

bene, perché? Semplice, perché oggetti diversi (contenuto, non riferimenti) dovrebbero ottenere codici hash diversi; d'altra parte, gli oggetti uguali dovrebbero ottenere lo stesso codice hash.

In base a quanto sopra, le strutture dati associative Java confrontano i risultati ottenuti dalle chiamate equals() e hashCode() per creare i bucket. Se entrambi sono uguali, gli oggetti sono uguali; altrimenti no.

Nel caso specifico (cioè quella presentata in precedenza), quando hashCode() è commentato, un numero casuale viene generato per ogni istanza (comportamento ereditato da Object) come hash, le equals() controlla i riferimenti di stringa (ricorda Java String Pool), quindi equals() deve restituire true ma hashCode() no, il risultato è 3 oggetti diversi memorizzati. Vediamo cosa succede nel caso in cui hashCode() rispetti il ​​contratto ma restituendo sempre 9 è non commentato. Beh, hashCode() è sempre lo stesso, l'equals() restituisce true per le due stringhe nel Pool (ad esempio "Monday"), e per loro il bucket sarà lo stesso con conseguente solo 2 elementi memorizzati.

Pertanto, è assolutamente necessario fare attenzione nell'usare l'override di hashCode() e equals(), in particolare quando i tipi di dati composti sono definiti dall'utente e vengono utilizzati con strutture di dati associative Java.

0

Ogni volta che si crea un nuovo oggetto in Java, verrà assegnato un codice hash univoco dalla stessa JVM. Se non si esegue l'override del metodo hashcode, l'oggetto otterrà un hascode univoco e quindi un bucket univoco (il bucket Imagine non è altro che un luogo in memoria in cui JVM andrà a trovare un oggetto).

(è possibile controllare l'unicità di un codice hash chiamando il metodo codice hash su ogni oggetto e stampare i loro valori sulla console)

Nel tuo caso quando si è un metodo commentting codice hash, hashmap in primo luogo cercare secchio avere lo stesso codice hash che metodo restituisce. E ogni volta che restituisci lo stesso hashcode. Ora, quando hashmap trova quel bucket, confronta l'oggetto corrente con l'oggetto che risiede nel bucket usando il metodo euqals. Qui trova "Monday" e quindi l'implementazione di hashmap non consente di aggiungerlo nuovamente perché esiste già un oggetto con lo stesso hashcode e la stessa implementazione di euqality.

Quando si commenta il metodo hashcode, JVM restituisce semplicemente un diverso codice hash per tutti e tre gli oggetti e quindi non si preoccupa nemmeno di eseguire il comando degli oggetti utilizzando il metodo equals. E quindi ci saranno tre diversi oggetti in Map aggiunti dall'implementazione di hashmap.

19

HashMap utilizza hashCode(), == e equals() per la ricerca di voci. La sequenza di ricerca per una data chiave k è la seguente:

  • Usa k.hashCode() per determinare quali secchio la voce viene memorizzata, se del caso
  • se trovato, per ogni voce della chiave k1 in quel secchio, se k == k1 || k.equals(k1), poi tornare ingresso
  • Eventuali altri esiti, senza voce corrispondente

per dimostrare con un esempio, supponiamo che vogliamo creare un s'dove le chiavi sono qualcosa che è 'logicamente equivalente' se hanno lo stesso valore intero, rappresentato dalla classe AmbiguousInteger. Quindi, costruiamo un HashMap, inseriamo una voce, quindi tentiamo di sovrascrivere il suo valore e recuperare il valore per chiave.

class AmbiguousInteger { 
    private final int value; 

    AmbiguousInteger(int value) { 
     this.value = value; 
    } 
} 

HashMap<AmbiguousInteger, Integer> map = new HashMap<>(); 
// logically equivalent keys 
AmbiguousInteger key1 = new AmbiguousInteger(1), 
       key2 = new AmbiguousInteger(1), 
       key3 = new AmbiguousInteger(1); 
map.put(key1, 1); // put in value for entry '1' 
map.put(key2, 2); // attempt to override value for entry '1' 
System.out.println(map.get(key1)); 
System.out.println(map.get(key2)); 
System.out.println(map.get(key3)); 

Expected: 2, 2, 2 

Non escludere hashCode() e equals(): per default Java genera diversi valori hashCode() per oggetti diversi, in modo da HashMap utilizza questi valori per mappare key1 e key2 in diversi secchi. key3 non ha un bucket corrispondente, quindi non ha alcun valore.

class AmbiguousInteger { 
    private final int value; 

    AmbiguousInteger(int value) { 
     this.value = value; 
    } 
} 

map.put(key1, 1); // map to bucket 1, set as entry 1[1] 
map.put(key2, 2); // map to bucket 2, set as entry 2[1] 
map.get(key1); // map to bucket 1, get as entry 1[1] 
map.get(key2); // map to bucket 2, get as entry 2[1] 
map.get(key3); // map to no bucket 
Expected: 2, 2, 2 
Output: 1, 2, null 

Override hashCode() solo:HashMap mappe key1 e key2 nello stesso secchio, ma rimangono voci diverse a causa di entrambe le key1 == key2 e key1.equals(key2) controlli non riescono, come per impostazione predefinita equals() utilizza == di controllo, e si riferiscono a diversi le istanze. key3 non riesce entrambi gli assegni == e equals() contro key1 e key2 e pertanto non ha alcun valore corrispondente.

class AmbiguousInteger { 
    private final int value; 

    AmbiguousInteger(int value) { 
     this.value = value; 
    } 

    @Override 
    public int hashCode() { 
     return value; 
    } 
} 

map.put(key1, 1); // map to bucket 1, set as entry 1[1] 
map.put(key2, 2); // map to bucket 1, set as entry 1[2] 
map.get(key1); // map to bucket 1, get as entry 1[1] 
map.get(key2); // map to bucket 1, get as entry 1[2] 
map.get(key3); // map to bucket 1, no corresponding entry 
Expected: 2, 2, 2 
Output: 1, 2, null 

Override equals() solo:HashMap mappe di tutte le chiavi in ​​diversi secchi a causa di predefinito diverso hashCode(). == o equals() qui il controllo è irrilevante poiché HashMap non raggiunge mai il punto in cui è necessario utilizzarli.

class AmbiguousInteger { 
    private final int value; 

    AmbiguousInteger(int value) { 
     this.value = value; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value; 
    } 
} 

map.put(key1, 1); // map to bucket 1, set as entry 1[1] 
map.put(key2, 2); // map to bucket 2, set as entry 2[1] 
map.get(key1); // map to bucket 1, get as entry 1[1] 
map.get(key2); // map to bucket 2, get as entry 2[1] 
map.get(key3); // map to no bucket 
Expected: 2, 2, 2 
Actual: 1, 2, null 

Override sia hashCode() e equals(): HashMap mappe key1, key2 e key3 nello stesso secchio. I controlli == non riescono quando si confrontano diverse istanze, ma i controlli equals() passano in quanto hanno tutti lo stesso valore e sono considerati "logicamente equivalenti" dalla nostra logica.

class AmbiguousInteger { 
    private final int value; 

    AmbiguousInteger(int value) { 
     this.value = value; 
    } 

    @Override 
    public int hashCode() { 
     return value; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value; 
    } 
} 

map.put(key1, 1); // map to bucket 1, set as entry 1[1] 
map.put(key2, 2); // map to bucket 1, set as entry 1[1], override value 
map.get(key1); // map to bucket 1, get as entry 1[1] 
map.get(key2); // map to bucket 1, get as entry 1[1] 
map.get(key3); // map to bucket 1, get as entry 1[1] 
Expected: 2, 2, 2 
Actual: 2, 2, 2 

E se hashCode() è casuale?: HashMap assegnerà un bucket diverso per ciascuna operazione, pertanto non si troverà mai la stessa voce inserita in precedenza.

class AmbiguousInteger { 
    private static int staticInt; 
    private final int value; 

    AmbiguousInteger(int value) { 
     this.value = value; 
    } 

    @Override 
    public int hashCode() { 
     return ++staticInt; // every subsequent call gets different value 
    } 

    @Override 
    public boolean equals(Object obj) { 
     return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value; 
    } 
} 

map.put(key1, 1); // map to bucket 1, set as entry 1[1] 
map.put(key2, 2); // map to bucket 2, set as entry 2[1] 
map.get(key1); // map to no bucket, no corresponding value 
map.get(key2); // map to no bucket, no corresponding value 
map.get(key3); // map to no bucket, no corresponding value 
Expected: 2, 2, 2 
Actual: null, null, null 

E se hashCode() è sempre lo stesso?: HashMap mappa tutte le chiavi in ​​un unico grande secchio. In questo caso, il codice è funzionalmente corretto, ma l'uso di HashMap è praticamente ridondante, poiché qualsiasi recupero dovrebbe scorrere tutte le voci in quel singolo bucket in O (N) tempo(), equivalente all'utilizzo di un List .

class AmbiguousInteger { 
    private final int value; 

    AmbiguousInteger(int value) { 
     this.value = value; 
    } 

    @Override 
    public int hashCode() { 
     return 0; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     return obj instanceof AmbiguousInteger && value == ((AmbiguousInteger) obj).value; 
    } 
} 

map.put(key1, 1); // map to bucket 1, set as entry 1[1] 
map.put(key2, 2); // map to bucket 1, set as entry 1[1] 
map.get(key1); // map to bucket 1, get as entry 1[1] 
map.get(key2); // map to bucket 1, get as entry 1[1] 
map.get(key3); // map to bucket 1, get as entry 1[1] 
Expected: 2, 2, 2 
Actual: 2, 2, 2 

E se equals è sempre falso?: == controllo passa quando si confronta la stessa istanza con se stessa, ma non riesce altrimenti, equals controlli non riesce sempre così key1, key2 e key3 sono considerati 'logicamente diversa', e le mappe di diverse voci, anche se sono ancora nella stessa benna a causa dello stesso hashCode().

class AmbiguousInteger { 
    private final int value; 

    AmbiguousInteger(int value) { 
     this.value = value; 
    } 

    @Override 
    public int hashCode() { 
     return 0; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     return false; 
    } 
} 

map.put(key1, 1); // map to bucket 1, set as entry 1[1] 
map.put(key2, 2); // map to bucket 1, set as entry 1[2] 
map.get(key1); // map to bucket 1, get as entry 1[1] 
map.get(key2); // map to bucket 1, get as entry 1[2] 
map.get(key3); // map to bucket 1, no corresponding entry 
Expected: 2, 2, 2 
Actual: 1, 2, null 

Va bene che cosa succede se equals è sempre vero oggi?: stai praticamente dicendo che tutti gli oggetti sono considerati 'logicamente equivalenti' a un altro, quindi tutti si associano allo stesso segmento (a causa dello stesso hashCode()), stessa voce.

class AmbiguousInteger { 
    private final int value; 

    AmbiguousInteger(int value) { 
     this.value = value; 
    } 

    @Override 
    public int hashCode() { 
     return 0; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     return true; 
    } 
} 

map.put(key1, 1); // map to bucket 1, set as entry 1[1] 
map.put(key2, 2); // map to bucket 1, set as entry 1[1], override value 
map.put(new AmbiguousInteger(100), 100); // map to bucket 1, set as entry1[1], override value 
map.get(key1); // map to bucket 1, get as entry 1[1] 
map.get(key2); // map to bucket 1, get as entry 1[1] 
map.get(key3); // map to bucket 1, get as entry 1[1] 
Expected: 2, 2, 2 
Actual: 100, 100, 100 
+0

La migliore risposta e spiegazione finora! Molte grazie! – abhiagNitk

+0

Ottima risposta! Grazie. –

Problemi correlati