Migliore implementazione per il metodo hashCode per una raccolta


299

Come possiamo decidere la migliore implementazione del hashCode()metodo per una raccolta (supponendo che il metodo uguale sia stato sovrascritto correttamente)?


2
con Java 7+, suppongo che Objects.hashCode(collection)dovrebbe essere una soluzione perfetta!
Diablo

3
@Diablo Non credo che risponda alla domanda - quel metodo semplicemente restituisce collection.hashCode()( hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/… )
cbreezier

Risposte:


438

La migliore implementazione? Questa è una domanda difficile perché dipende dal modello di utilizzo.

Una per quasi tutti i casi l'attuazione ragionevole buon stato proposto in Josh Bloch s' Effective Java nel punto 8 (seconda edizione). La cosa migliore è guardarlo lì perché l'autore spiega perché l'approccio è buono.

Una versione corta

  1. Creare a int resulte assegnare un valore diverso da zero .

  2. Per ogni campo f testato nel equals()metodo, calcola un codice hash cper:

    • Se il campo f è a boolean: calcola (f ? 0 : 1);
    • Se il campo f è una byte, char, shorto int: calcolare (int)f;
    • Se il campo f è a long: calcola (int)(f ^ (f >>> 32));
    • Se il campo f è a float: calcola Float.floatToIntBits(f);
    • Se il campo f è a double: calcola Double.doubleToLongBits(f)e gestisci il valore di ritorno come ogni valore lungo;
    • Se il campo f è un oggetto : utilizzare il risultato del hashCode()metodo o 0 se f == null;
    • Se il campo f è un array : vedi ogni campo come elemento separato e calcola il valore di hash in modo ricorsivo e combina i valori come descritto di seguito.
  3. Combina il valore hash ccon result:

    result = 37 * result + c
  4. Ritorno result

Ciò dovrebbe comportare una corretta distribuzione dei valori di hash per la maggior parte delle situazioni d'uso.


45
Sì, sono particolarmente curioso di sapere da dove viene il numero 37.
Chiudi il

17
Ho usato l'articolo 8 del libro "Effective Java" di Josh Bloch.
dmeister,

39
@dma_k La ragione per usare i numeri primi e il metodo descritto in questa risposta è assicurarsi che l' hashcode calcolato sarà univoco . Quando si utilizzano numeri non primi, non è possibile garantirlo. Non importa quale numero primo scegli, non c'è nulla di magico nel numero 37 (peccato che 42 non sia un numero primo, eh?)
Simon Forsberg,

34
@ SimonAndréForsberg Bene, il codice hash calcolato non può essere sempre univoco :) È un codice hash. Comunque ho avuto l'idea: il numero primo ha solo un moltiplicatore, mentre il numero primo ne ha almeno due. Ciò crea una combinazione aggiuntiva per l'operatore di moltiplicazione per ottenere lo stesso hash, ovvero causare una collisione.
dma_k,


140

Se sei soddisfatto dell'implementazione effettiva di Java consigliata da dmeister, puoi usare una chiamata in libreria invece di eseguire il roll-roll del tuo:

@Override
public int hashCode() {
    return Objects.hashCode(this.firstName, this.lastName);
}

Ciò richiede Guava ( com.google.common.base.Objects.hashCode) o la libreria standard in Java 7 ( java.util.Objects.hash) ma funziona allo stesso modo.


8
A meno che non si abbia una buona ragione per non usarli, si dovrebbero assolutamente usare questi in ogni caso. (Formulandolo più forte, poiché dovrebbe essere formulato l'IMHO). Si applicano gli argomenti tipici per l'utilizzo di implementazioni / librerie standard (migliori pratiche, ben testate, meno soggette a errori, ecc.).
Kissaki,

7
@ justin.hughey sembra che tu sia confuso. L'unico caso che dovresti ignorare hashCodeè se hai un'abitudine equals, ed è proprio per questo che sono progettati questi metodi di libreria. La documentazione è abbastanza chiara sul loro comportamento in relazione a equals. Un'implementazione delle librerie non pretende di assolverti dal sapere quali sono le caratteristiche di hashCodeun'implementazione corretta : queste librerie ti rendono più semplice implementare un'implementazione così conforme per la maggior parte dei casi in cui equalsviene superato.
bacar,

6
Per tutti gli sviluppatori Android che guardano la classe java.util.Objects, è stato introdotto solo in API 19, quindi assicurati di eseguire KitKat o versioni successive, altrimenti otterrai NoClassDefFoundError.
Andrew Kelly,

3
Migliore risposta IMO, anche se a titolo di esempio avrei preferito scegliere il java.util.Objects.hash(...)metodo JDK7 piuttosto che il com.google.common.base.Objects.hashCode(...)metodo guava . Penso che molte persone sceglierebbero la libreria standard piuttosto che una dipendenza aggiuntiva.
Malte Skoruppa,

2
Se ci sono due o più argomenti e se uno di essi è un array, il risultato potrebbe non essere quello che ti aspetti perché hashCode()per un array è solo suo java.lang.System.identityHashCode(...).
Starikoff,

59

È meglio usare la funzionalità fornita da Eclipse che fa un ottimo lavoro e puoi mettere i tuoi sforzi ed energia nello sviluppo della logica di business.


4
+1 Una buona soluzione pratica. La soluzione di dmeister è più completa, ma tendo a dimenticare di gestire i valori null quando provo a scrivere da solo gli hashcode.
Quantum7

1
+1 Concordo con Quantum7, ma direi che è anche molto buono capire cosa sta facendo l'implementazione generata da Eclipse e da dove ottiene i dettagli dell'implementazione.
jwir3,

15
Siamo spiacenti ma le risposte che implicano "funzionalità fornita da [alcuni IDE]" non sono realmente pertinenti nel contesto del linguaggio di programmazione in generale. Ci sono dozzine di IDE e questo non risponde alla domanda ... vale a dire perché si tratta più di determinazione algoritmica e direttamente associata all'implementazione uguale () - qualcosa di cui un IDE non saprà nulla.
Darrell Teague,

57

Sebbene sia collegato alla Androiddocumentazione (Wayback Machine) e al mio codice su Github , funzionerà per Java in generale. La mia risposta è un'estensione della Risposta di dmeister con solo codice che è molto più facile da leggere e comprendere.

@Override 
public int hashCode() {

    // Start with a non-zero constant. Prime is preferred
    int result = 17;

    // Include a hash for each field.

    // Primatives

    result = 31 * result + (booleanField ? 1 : 0);                   // 1 bit   » 32-bit

    result = 31 * result + byteField;                                // 8 bits  » 32-bit 
    result = 31 * result + charField;                                // 16 bits » 32-bit
    result = 31 * result + shortField;                               // 16 bits » 32-bit
    result = 31 * result + intField;                                 // 32 bits » 32-bit

    result = 31 * result + (int)(longField ^ (longField >>> 32));    // 64 bits » 32-bit

    result = 31 * result + Float.floatToIntBits(floatField);         // 32 bits » 32-bit

    long doubleFieldBits = Double.doubleToLongBits(doubleField);     // 64 bits (double) » 64-bit (long) » 32-bit (int)
    result = 31 * result + (int)(doubleFieldBits ^ (doubleFieldBits >>> 32));

    // Objects

    result = 31 * result + Arrays.hashCode(arrayField);              // var bits » 32-bit

    result = 31 * result + referenceField.hashCode();                // var bits » 32-bit (non-nullable)   
    result = 31 * result +                                           // var bits » 32-bit (nullable)   
        (nullableReferenceField == null
            ? 0
            : nullableReferenceField.hashCode());

    return result;

}

MODIFICARE

In genere, quando si esegue l'override hashcode(...), si desidera anche eseguire l'override equals(...). Quindi, per quelli che saranno o sono già stati implementati equals, ecco un buon riferimento dal mio Github ...

@Override
public boolean equals(Object o) {

    // Optimization (not required).
    if (this == o) {
        return true;
    }

    // Return false if the other object has the wrong type, interface, or is null.
    if (!(o instanceof MyType)) {
        return false;
    }

    MyType lhs = (MyType) o; // lhs means "left hand side"

            // Primitive fields
    return     booleanField == lhs.booleanField
            && byteField    == lhs.byteField
            && charField    == lhs.charField
            && shortField   == lhs.shortField
            && intField     == lhs.intField
            && longField    == lhs.longField
            && floatField   == lhs.floatField
            && doubleField  == lhs.doubleField

            // Arrays

            && Arrays.equals(arrayField, lhs.arrayField)

            // Objects

            && referenceField.equals(lhs.referenceField)
            && (nullableReferenceField == null
                        ? lhs.nullableReferenceField == null
                        : nullableReferenceField.equals(lhs.nullableReferenceField));
}

1
La documentazione Android ora non include più il codice sopra, quindi ecco una versione memorizzata nella cache della Wayback Machine - Documentazione Android (7 febbraio 2015)
Christopher Rucinski,

17

Innanzitutto assicurarsi che equals sia implementato correttamente. Da un articolo IBM DeveloperWorks :

  • Simmetria: per due riferimenti, aeb, a.equals (b) se e solo se b.equals (a)
  • Riflessività: per tutti i riferimenti non nulli, a.equals (a)
  • Transitività: se a.equals (b) e b.equals (c), quindi a.equals (c)

Quindi assicurati che la loro relazione con hashCode rispetti il ​​contatto (dallo stesso articolo):

  • Coerenza con hashCode (): due oggetti uguali devono avere lo stesso valore hashCode ()

Infine, una buona funzione hash dovrebbe sforzarsi di avvicinarsi alla funzione hash ideale .


11

about8.blogspot.com, hai detto

se equals () restituisce true per due oggetti, hashCode () dovrebbe restituire lo stesso valore. Se equals () restituisce false, allora hashCode () dovrebbe restituire valori diversi

Non posso essere d'accordo con te. Se due oggetti hanno lo stesso hashcode non significa che siano uguali.

Se A è uguale a B, allora A.hashcode deve essere uguale a B.hascode

ma

se A.hashcode è uguale a B.hascode, ciò non significa che A deve essere uguale a B


3
Se (A != B) and (A.hashcode() == B.hashcode())questo è ciò che chiamiamo collisione della funzione hash. È perché il codice della funzione hash è sempre finito, mentre il suo dominio di solito non lo è. Più è grande il codice, meno spesso dovrebbe verificarsi la collisione. Le buone funzioni hash dovrebbero restituire hash diversi per oggetti diversi con la massima possibilità ottenibile date le dimensioni particolari del codice. Tuttavia, raramente può essere completamente garantito.
Krzysztof Jabłoński

Questo dovrebbe essere solo un commento al post sopra per Gray. Buone informazioni ma in realtà non risponde alla domanda
Christopher Rucinski,

Buoni commenti, ma fai attenzione all'uso del termine "oggetti diversi" ... perché equals () e quindi l'implementazione di hashCode () non riguardano necessariamente oggetti diversi in un contesto OO ma di solito sono più circa le loro rappresentazioni del modello di dominio (ad esempio, due le persone possono essere considerate uguali se condividono un codice paese e un ID paese - sebbene questi possano essere due diversi "oggetti" in una JVM - sono considerati "uguali" e con un determinato codice hash) ...
Darrell Teague

7

Se usi eclipse, puoi generare equals()e hashCode()usare:

Sorgente -> Genera hashCode () ed equals ().

Utilizzando questa funzione è possibile decidere quali campi si desidera utilizzare per il calcolo dell'uguaglianza e del codice hash ed Eclipse genera i metodi corrispondenti.


7

C'è un'implementazione bene della Effective Java s' hashcode()e equals()la logica in Apache Commons Lang . Acquista HashCodeBuilder ed EqualsBuilder .


1
Il rovescio della medaglia di questa API è che paghi il costo di costruzione dell'oggetto ogni volta che chiami uguale e hashcode (a meno che il tuo oggetto non sia immutabile e precomponi l'hash), che può essere molto in alcuni casi.
James McMahon,

questo era il mio approccio preferito, fino a poco tempo fa. Ho incontrato StackOverFlowError mentre utilizzavo un criterio per l'associazione SharedKey OneToOne. Inoltre , la Objectsclasse fornisce metodi hash(Object ..args)e equals()metodi da Java7 in poi. Questi sono raccomandati per tutte le applicazioni che usano jdk 1.7+
Diablo

@Diablo Immagino che il tuo problema fosse un ciclo nel grafico degli oggetti e quindi sei sfortunato con la maggior parte dell'implementazione in quanto devi ignorare qualche riferimento o interrompere il ciclo (obbligando un IdentityHashMap). FWIW Uso un codice hash basato su ID ed è uguale per tutte le entità.
maaartinus,

6

Solo una breve nota per completare un'altra risposta più dettagliata (in termini di codice):

Se considero la domanda come faccio a creare una tabella hash-in-java e soprattutto la voce FAQ di jGuru , credo che alcuni altri criteri su cui si possa giudicare un codice hash sono:

  • sincronizzazione (l'algo supporta l'accesso simultaneo o no)?
  • iterazione sicura (l'algo rileva una raccolta che cambia durante l'iterazione)
  • valore null (il codice hash supporta il valore null nella raccolta)

4

Se capisco correttamente la tua domanda, hai una classe di raccolta personalizzata (ovvero una nuova classe che si estende dall'interfaccia di Raccolta) e vuoi implementare il metodo hashCode ().

Se la tua classe di raccolta estende AbstractList, quindi non devi preoccuparti, esiste già un'implementazione di equals () e hashCode () che funziona iterando attraverso tutti gli oggetti e aggiungendo i loro hashCodes () insieme.

   public int hashCode() {
      int hashCode = 1;
      Iterator i = iterator();
      while (i.hasNext()) {
        Object obj = i.next();
        hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
      }
  return hashCode;
   }

Ora, se quello che vuoi è il modo migliore per calcolare il codice hash per una classe specifica, normalmente uso l'operatore ^ (bitwise exclusive o) per elaborare tutti i campi che utilizzo nel metodo equals:

public int hashCode(){
   return intMember ^ (stringField != null ? stringField.hashCode() : 0);
}

2

@ about8: c'è un bug piuttosto grave lì.

Zam obj1 = new Zam("foo", "bar", "baz");
Zam obj2 = new Zam("fo", "obar", "baz");

stesso hashcode

probabilmente vuoi qualcosa del genere

public int hashCode() {
    return (getFoo().hashCode() + getBar().hashCode()).toString().hashCode();

(puoi ottenere hashCode direttamente da int in Java in questi giorni? Penso che faccia un po 'di autocasting .. in tal caso, salta il toString, è brutto.)


3
il bug è nella lunga risposta di about8.blogspot.com - ottenere l'hashcode da una concatenazione di stringhe ti lascia con una funzione di hash che è la stessa per qualsiasi combinazione di stringhe che si sommano alla stessa stringa.
SquareCog,

1
Quindi questa è meta-discussione e non è affatto collegata alla domanda? ;-)
Huppie il

1
È una correzione a una risposta proposta che presenta un difetto abbastanza significativo.
SquareCog,

Questa è un'implementazione molto limitata
Christopher Rucinski,

L'implementazione evita il problema e ne introduce un altro; Scambio fooe barporta allo stesso hashCode. Il tuo toStringAFAIK non si compila, e se lo fa, è terribile inefficiente. Qualcosa di simile 109 * getFoo().hashCode() + 57 * getBar().hashCode()è più veloce, più semplice e non produce collisioni inutili.
maaartinus,

2

Come hai specificamente richiesto le raccolte, vorrei aggiungere un aspetto che le altre risposte non hanno ancora menzionato: una HashMap non si aspetta che le loro chiavi cambino il loro codice hash una volta che sono state aggiunte alla raccolta. Avrebbe sconfitto l'intero scopo ...



2

Uso un piccolo wrapper Arrays.deepHashCode(...)perché gestisce correttamente le matrici fornite come parametri

public static int hash(final Object... objects) {
    return Arrays.deepHashCode(objects);
}


1

Preferisco utilizzare metodi di utilità dalla libreria di Google Collections della classe Oggetti che mi aiutano a mantenere pulito il mio codice. Molto spesso equalse i hashcodemetodi sono fatti dal modello IDE, quindi non sono puliti da leggere.


1

Ecco un'altra dimostrazione di approccio JDK 1.7+ con logiche di superclasse prese in considerazione. Lo vedo abbastanza comodo con la classe Object hashCode () spiegata, pura dipendenza JDK e nessun lavoro manuale aggiuntivo. notare cheObjects.hash() è tollerante null.

Non ho incluso alcuna equals()implementazione, ma in realtà ovviamente ne avrai bisogno.

import java.util.Objects;

public class Demo {

    public static class A {

        private final String param1;

        public A(final String param1) {
            this.param1 = param1;
        }

        @Override
        public int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param1);
        }

    }

    public static class B extends A {

        private final String param2;
        private final String param3;

        public B(
            final String param1,
            final String param2,
            final String param3) {

            super(param1);
            this.param2 = param2;
            this.param3 = param3;
        }

        @Override
        public final int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param2,
                this.param3);
        }
    }

    public static void main(String [] args) {

        A a = new A("A");
        B b = new B("A", "B", "C");

        System.out.println("A: " + a.hashCode());
        System.out.println("B: " + b.hashCode());
    }

}

1

L'implementazione standard è debole e il suo utilizzo porta a collisioni non necessarie. Immagina a

class ListPair {
    List<Integer> first;
    List<Integer> second;

    ListPair(List<Integer> first, List<Integer> second) {
        this.first = first;
        this.second = second;
    }

    public int hashCode() {
        return Objects.hashCode(first, second);
    }

    ...
}

Adesso,

new ListPair(List.of(a), List.of(b, c))

e

new ListPair(List.of(b), List.of(a, c))

hanno lo stesso hashCode, vale 31*(a+b) + ca dire il moltiplicatore utilizzato perList.hashCode viene riutilizzato qui. Ovviamente, le collisioni sono inevitabili, ma produrre collisioni inutili è solo ... inutile.

Non c'è nulla di sostanzialmente intelligente nell'uso 31. Il moltiplicatore deve essere dispari per evitare di perdere informazioni (qualsiasi moltiplicatore pari perde almeno il bit più significativo, i multipli di quattro ne perdono due, ecc.). È possibile utilizzare qualsiasi moltiplicatore dispari. I piccoli moltiplicatori possono portare a un calcolo più rapido (la JIT può utilizzare turni e aggiunte), ma dato che la moltiplicazione ha una latenza di soli tre cicli sui moderni Intel / AMD, questo non ha importanza. I piccoli moltiplicatori portano anche a una maggiore collisione per piccoli input, che a volte può essere un problema.

L'uso di un numero primo è inutile poiché i numeri primi non hanno alcun significato nell'anello Z / (2 ** 32).

Quindi, consiglierei di usare un grande numero dispari scelto casualmente (sentiti libero di prendere un numero primo). Poiché le CPU i86 / amd64 possono utilizzare un'istruzione più breve per gli operandi che si adattano a un singolo byte con segno, esiste un vantaggio di velocità minuscola per moltiplicatori come 109. Per ridurre al minimo le collisioni, prendere qualcosa come 0x58a54cf5.

L'uso di moltiplicatori diversi in luoghi diversi è utile, ma probabilmente non è sufficiente per giustificare il lavoro aggiuntivo.


0

Quando combino i valori hash, di solito utilizzo il metodo di combinazione utilizzato nella libreria boost c ++, vale a dire:

seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

Questo fa un buon lavoro nel garantire una distribuzione uniforme. Per alcune discussioni sul funzionamento di questa formula, vedere il post StackOverflow: numero magico in boost :: hash_combine

C'è una buona discussione delle diverse funzioni hash su: http://burtleburtle.net/bob/hash/doobs.html


1
Questa è una domanda su Java, non su C ++.
dano,

-1

Per una classe semplice è spesso più semplice implementare hashCode () in base ai campi della classe che sono controllati dall'implementazione equals ().

public class Zam {
    private String foo;
    private String bar;
    private String somethingElse;

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj == null) {
            return false;
        }

        if (getClass() != obj.getClass()) {
            return false;
        }

        Zam otherObj = (Zam)obj;

        if ((getFoo() == null && otherObj.getFoo() == null) || (getFoo() != null && getFoo().equals(otherObj.getFoo()))) {
            if ((getBar() == null && otherObj. getBar() == null) || (getBar() != null && getBar().equals(otherObj. getBar()))) {
                return true;
            }
        }

        return false;
    }

    public int hashCode() {
        return (getFoo() + getBar()).hashCode();
    }

    public String getFoo() {
        return foo;
    }

    public String getBar() {
        return bar;
    }
}

La cosa più importante è mantenere hashCode () ed equals () coerenti: se equals () restituisce true per due oggetti, allora hashCode () dovrebbe restituire lo stesso valore. Se equals () restituisce false, allora hashCode () dovrebbe restituire valori diversi.


1
Come SquareCog hanno già notato. Se codice hash viene generato una volta dalla concatenazione di due stringhe è estremamente facile da generare masse di collisioni: ("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc"). È un grave difetto. Sarebbe meglio valutare l'hashcode per entrambi i campi e quindi calcolare la loro combinazione lineare (preferibilmente usando i numeri primi come coefficienti).
Krzysztof Jabłoński il

@ KrzysztofJabłoński Giusto. Inoltre, lo scambio fooe barproduce anche una collisione inutile.
maaartinus,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.