Quali sono i problemi da considerare quando si sovrascrive uguale a hashCode in Java?


617

Quali problemi / insidie ​​devono essere considerati quando si esegue l'override equals e hashCode?

Risposte:


1439

La teoria (per gli avvocati di lingua e matematicamente inclini):

equals()( javadoc ) deve definire una relazione di equivalenza (deve essere riflessiva , simmetrica e transitiva ). Inoltre, deve essere coerente (se gli oggetti non vengono modificati, deve continuare a restituire lo stesso valore). Inoltre, o.equals(null)deve sempre restituire false.

hashCode()( javadoc ) deve anche essere coerente (se l'oggetto non viene modificato in termini di equals(), deve continuare a restituire lo stesso valore).

La relazione tra i due metodi è:

Ogni volta a.equals(b), quindi a.hashCode()deve essere uguale a b.hashCode().

In pratica:

Se si esegue l'override di uno, è necessario ignorare l'altro.

Usa lo stesso set di campi che usi per calcolare equals()per calcolarehashCode() .

Usa le eccellenti classi di supporto EqualsBuilder e HashCodeBuilder dalla libreria Lang di Apache Commons . Un esempio:

public class Person {
    private String name;
    private int age;
    // ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
       if (!(obj instanceof Person))
            return false;
        if (obj == this)
            return true;

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

Ricorda anche:

Quando si utilizza una raccolta o una mappa basata su hash come HashSet , LinkedHashSet , HashMap , Hashtable o WeakHashMap , assicurarsi che hashCode () degli oggetti chiave inseriti nella raccolta non cambi mai mentre l'oggetto si trova nella raccolta. Il modo antiproiettile per garantire ciò è rendere immutabili le tue chiavi, che ha anche altri vantaggi .


12
Punto aggiuntivo su appendSuper (): dovresti usarlo in hashCode () e equals () se e solo se vuoi ereditare il comportamento di uguaglianza della superclasse. Ad esempio, se derivate direttamente dall'oggetto, non ha senso perché tutti gli oggetti sono distinti per impostazione predefinita.
Antti Kissaniemi,

312
Puoi ottenere Eclipse per generare i due metodi per te: Sorgente> Genera hashCode () ed equals ().
Rok Strniša,

27

6
@Darthenius Eclipse generato equivale a getClass () che potrebbe causare problemi in alcuni casi (vedi l'articolo 8 di Java efficace)
AndroidGecko

7
Il primo controllo null non è necessario dato che instanceofrestituisce false se il suo primo operando è null (di nuovo Java effettivo).
Izaban,

295

Ci sono alcuni problemi da notare se hai a che fare con classi che persistono usando un Object-Relationship Mapper (ORM) come Hibernate, se non pensavi che fosse già irragionevolmente complicato!

Gli oggetti caricati in modo pigro sono sottoclassi

Se i tuoi oggetti sono persistenti usando un ORM, in molti casi avrai a che fare con proxy dinamici per evitare di caricare l'oggetto troppo presto dall'archivio dati. Questi proxy sono implementati come sottoclassi della tua classe. Ciò significa che this.getClass() == o.getClass()tornerà false. Per esempio:

Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy

Se hai a che fare con un ORM, l'utilizzo o instanceof Personè l'unica cosa che si comporterà correttamente.

Gli oggetti caricati in modo pigro hanno campi null

Gli ORM di solito usano i getter per forzare il caricamento di oggetti carichi pigri. Questo significa che person.namesarà nullse personè pigro, anche se il person.getName()caricamento delle forze e restituisce "John Doe". Nella mia esperienza, questo cresce più spesso in hashCode()eequals() .

Se hai a che fare con un ORM, assicurati di usare sempre getter e non inserire mai i riferimenti in hashCode()e equals().

Il salvataggio di un oggetto cambierà il suo stato

Gli oggetti persistenti usano spesso un idcampo per contenere la chiave dell'oggetto. Questo campo verrà automaticamente aggiornato quando un oggetto viene salvato per la prima volta. Non utilizzare un campo ID in hashCode(). Ma puoi usarlo in equals().

Un modello che uso spesso è

if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

Ma: non puoi includere getId()in hashCode(). Se lo fai, quando un oggetto è persistente, hashCodecambia. Se l'oggetto è in a HashSet, "non" lo troverai mai più.

Nel mio Personesempio, probabilmente userei getName()for hashCodee getId()plus getName()(solo per paranoia) per equals(). Va bene se ci sono dei rischi di "collisioni" per hashCode(), ma mai va bene equals().

hashCode() dovrebbe usare il sottoinsieme non modificabile di proprietà da equals()


2
@Johannes Brodwall: non capisco Saving an object will change it's state! hashCodedeve tornare int, quindi come utilizzerai getName()? Puoi fare un esempio per il tuohashCode
jimmybondy

@jimmybondy: getName restituirà un oggetto String che ha anche un hashCode che può essere usato
mateusz.fiolka

85

Un chiarimento sul obj.getClass() != getClass().

Questa affermazione è il risultato equals()dell'essere ereditarietà ostile. Il JLS (specifica del linguaggio Java) specifica che se A.equals(B) == truepoi B.equals(A)deve anche tornare true. Se si omette quell'istruzione che eredita le classi che sovrascrivono equals()(e ne modificano il comportamento), si infrange questa specifica.

Considera il seguente esempio di ciò che accade quando l'istruzione viene omessa:

    class A {
      int field1;

      A(int field1) {
        this.field1 = field1;
      }

      public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
      }
    }

    class B extends A {
        int field2;

        B(int field1, int field2) {
            super(field1);
            this.field2 = field2;
        }

        public boolean equals(Object other) {
            return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
        }
    }    

Facendo new A(1).equals(new A(1))anche, new B(1,1).equals(new B(1,1))risultato dare vero, come dovrebbe.

Sembra tutto molto bello, ma guarda cosa succede se proviamo a usare entrambe le classi:

A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;

Ovviamente, questo è sbagliato.

Se si desidera garantire la condizione simmetrica. a = b se b = ae il principio di sostituzione di Liskov chiama super.equals(other)non solo in caso di Besempio, ma controlla dopo per Aesempio:

if (other instanceof B )
   return (other != null && ((B)other).field2 == field2 && super.equals(other)); 
if (other instanceof A) return super.equals(other); 
   else return false;

Che produrrà:

a.equals(b) == true;
b.equals(a) == true;

Dove, se anon è un riferimento di B, allora potrebbe essere un essere un riferimento di classe A(perché si estenderla), in questo caso si chiama super.equals() troppo .


2
È possibile rendere equals simmetrici in questo modo (se si confronta un oggetto superclasse con un oggetto sottoclasse, utilizzare sempre gli uguali della sottoclasse) if (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) return obj.equals (questo);
Pihentagy,

5
@pihentagy - quindi otterrei uno stackoverflow quando la classe di implementazione non sovrascrive il metodo equals. non è divertente.
Ran Biron,

2
Non otterrai uno stackoverflow. Se il metodo equals non viene sovrascritto, si chiamerà di nuovo lo stesso codice, ma la condizione per la ricorsione sarà sempre falsa!
Jacob Raihle,

@pihentagy: come si comporta se ci sono due diverse classi derivate? Se a ThingWithOptionSetApuò essere uguale a un a Thingcondizione che tutte le opzioni extra abbiano valori predefiniti, e allo stesso modo per a ThingWithOptionSetB, allora dovrebbe essere possibile che un ThingWithOptionSetAconfronto sia uguale a un ThingWithOptionSetBsolo se tutte le proprietà non base di entrambi gli oggetti corrispondono ai loro valori predefiniti, ma Non vedo come fai a provarlo.
supercat,

7
Il problema è che rompe la transitività. Se aggiungi B b2 = new B(1,99), quindi b.equals(a) == truee a.equals(b2) == truema b.equals(b2) == false.
nickgrim,

46

Per un'implementazione intuitiva, controlla la soluzione di Tal Cohen, Come posso implementare correttamente il metodo equals ()?

Sommario:

Nel suo libro Effective Java Programming Language Guide (Addison-Wesley, 2001), Joshua Bloch afferma che "Semplicemente non c'è modo di estendere una classe istantanea e aggiungere un aspetto preservando il contratto uguale". Tal non è d'accordo.

La sua soluzione è implementare equals () chiamando un altro non simmetrico blindlyEquals () in entrambi i modi. blindlyEquals () viene sovrascritto da sottoclassi, equals () viene ereditato e mai ignorato.

Esempio:

class Point {
    private int x;
    private int y;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return (p.x == this.x && p.y == this.y);
    }
    public boolean equals(Object o) {
        return (this.blindlyEquals(o) && o.blindlyEquals(this));
    }
}

class ColorPoint extends Point {
    private Color c;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return (super.blindlyEquals(cp) && 
        cp.color == this.color);
    }
}

Si noti che equals () deve funzionare attraverso le gerarchie ereditarie se il Principio di sostituzione di Liskov deve essere soddisfatto.


10
Dai un'occhiata al metodo canEqual spiegato qui - lo stesso principio fa funzionare entrambe le soluzioni, ma con canEqual non confronti due volte gli stessi campi (sopra, px == this.x verrà testato in entrambe le direzioni): artima.com /lejava/articles/equality.html
Blaisorblade il

2
In ogni caso, non credo sia una buona idea. Ciò rende inutilmente confuso il contratto Equals: qualcuno che accetta due parametri Point, a e b, deve essere consapevole della possibilità che a.getX () == b.getX () e a.getY () == b.getY () può essere vero, ma a.equals (b) e b.equals (a) sono entrambi falsi (se solo uno è un ColorPoint).
Kevin,

Fondamentalmente, questo è simile if (this.getClass() != o.getClass()) return false, ma flessibile in quanto restituisce falso solo se le classi derivate si preoccupano di modificare uguali. È giusto?
Aleksandr Dubinsky,

31

Ancora stupito che nessuno abbia raccomandato la libreria guava per questo.

 //Sample taken from a current working project of mine just to illustrate the idea

    @Override
    public int hashCode(){
        return Objects.hashCode(this.getDate(), this.datePattern);
    }

    @Override
    public boolean equals(Object obj){
        if ( ! obj instanceof DateAndPattern ) {
            return false;
        }
        return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
                && Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
    }

23
java.util.Objects.hash () e java.util.Objects.equals () fanno parte di Java 7 (rilasciato nel 2011), quindi non è necessario Guava per questo.
herman,

1
certo, ma dovresti evitarlo poiché Oracle non fornisce più aggiornamenti pubblici per Java 6 (questo è il caso da febbraio 2013).
herman,

6
Il tuo thisin this.getDate()non significa nulla (diverso dal disordine)
Steve Kuo,

1
La vostra espressione "non instanceof" ha bisogno di una staffa in più: if (!(otherObject instanceof DateAndPattern)) {. Concordo con Hernan e Steve Kuo (anche se quello è una questione di preferenze personali), ma comunque +1.
Amos M. Carpenter,

26

Esistono due metodi in super classe come java.lang.Object. Dobbiamo sovrascriverli su oggetti personalizzati.

public boolean equals(Object obj)
public int hashCode()

Gli oggetti uguali devono produrre lo stesso codice hash purché siano uguali, tuttavia gli oggetti disuguali non devono produrre codici hash distinti.

public class Test
{
    private int num;
    private String data;
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if((obj == null) || (obj.getClass() != this.getClass()))
            return false;
        // object must be Test at this point
        Test test = (Test)obj;
        return num == test.num &&
        (data == test.data || (data != null && data.equals(test.data)));
    }

    public int hashCode()
    {
        int hash = 7;
        hash = 31 * hash + num;
        hash = 31 * hash + (null == data ? 0 : data.hashCode());
        return hash;
    }

    // other methods
}

Se vuoi ottenere di più, controlla questo link come http://www.javaranch.com/journal/2002/10/equalhash.html

Questo è un altro esempio, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

Divertiti! @. @


Siamo spiacenti ma non capisco questa affermazione sul metodo hashCode: non è legale se utilizza più variabili di equals (). Ma se codifico con più variabili, il mio codice viene compilato. Perché non è legale?
Adryr83,

19

Esistono un paio di modi per verificare l'uguaglianza di classe prima di verificare l'uguaglianza dei membri e penso che entrambi siano utili nelle giuste circostanze.

  1. Usa l' instanceofoperatore.
  2. Usa this.getClass().equals(that.getClass()).

Uso il numero 1 in finalun'implementazione uguale o quando implemento un'interfaccia che prescrive un algoritmo uguale (come le java.utilinterfacce di raccolta) il modo giusto per verificare con(obj instanceof Set) o qualunque interfaccia stai implementando). È generalmente una cattiva scelta quando è possibile ignorare gli uguali perché ciò rompe la proprietà di simmetria.

L'opzione n. 2 consente di estendere in modo sicuro la classe senza sovrascrivere uguali o interrompere la simmetria.

Se anche la tua classe è Comparable, anche i metodi equalse compareTodovrebbero essere coerenti. Ecco un modello per il metodo uguale in una Comparableclasse:

final class MyClass implements Comparable<MyClass>
{

  

  @Override
  public boolean equals(Object obj)
  {
    /* If compareTo and equals aren't final, we should check with getClass instead. */
    if (!(obj instanceof MyClass)) 
      return false;
    return compareTo((MyClass) obj) == 0;
  }

}

1
+1 per questo. Né getClass () né instanceof è una panacea, e questa è una buona spiegazione di come affrontare entrambi. Non pensare che ci sia motivo di non fare this.getClass () == that.getClass () invece di usare equals ().
Paul Cantrell,

C'è un problema con questo. Le classi anonime che non aggiungono alcun aspetto né sovrascrivono il metodo equals falliranno il controllo getClass anche se dovrebbero essere uguali.
steinybot,

@Steiny Non è chiaro per me che oggetti di tipi diversi dovrebbero essere uguali; Sto pensando alle diverse implementazioni di un'interfaccia come a una classe anonima comune. Puoi fare un esempio per supportare la tua premessa?
Erickson,

MyClass a = new MyClass (123); MyClass b = new MyClass (123) {// Ignora alcuni metodi}; // a.equals (b) è falso quando si utilizza this.getClass (). equals (that.getClass ())
steinybot

1
@Steiny Right. Come dovrebbe nella maggior parte dei casi, soprattutto se un metodo viene sostituito anziché aggiunto. Considera il mio esempio sopra. In caso contrario final, e il compareTo()metodo è stato ignorato per invertire l'ordinamento, le istanze della sottoclasse e della superclasse non devono essere considerate uguali. Quando questi oggetti venivano usati insieme in un albero, le chiavi che erano "uguali" in base a instanceofun'implementazione potrebbero non essere disponibili.
Erickson,


11

Il metodo equals () viene utilizzato per determinare l'uguaglianza di due oggetti.

come valore int di 10 è sempre uguale a 10. Ma questo metodo equals () riguarda l'uguaglianza di due oggetti. Quando diciamo oggetto, avrà proprietà. Per decidere sull'uguaglianza si considerano tali proprietà. Non è necessario tenere conto di tutte le proprietà per determinare l'uguaglianza e rispetto alla definizione della classe e al contesto può essere deciso. Quindi il metodo equals () può essere ignorato.

dovremmo sempre sovrascrivere il metodo hashCode () ogni volta che sovrascriviamo il metodo equals (). In caso contrario, cosa accadrà? Se utilizziamo gli hashtable nella nostra applicazione, non funzionerà come previsto. Poiché hashCode viene utilizzato per determinare l'uguaglianza dei valori memorizzati, non restituirà il valore corrispondente corretto per una chiave.

L'implementazione predefinita fornita è il metodo hashCode () nella classe Object che utilizza l'indirizzo interno dell'oggetto, lo converte in numero intero e lo restituisce.

public class Tiger {
  private String color;
  private String stripePattern;
  private int height;

  @Override
  public boolean equals(Object object) {
    boolean result = false;
    if (object == null || object.getClass() != getClass()) {
      result = false;
    } else {
      Tiger tiger = (Tiger) object;
      if (this.color == tiger.getColor()
          && this.stripePattern == tiger.getStripePattern()) {
        result = true;
      }
    }
    return result;
  }

  // just omitted null checks
  @Override
  public int hashCode() {
    int hash = 3;
    hash = 7 * hash + this.color.hashCode();
    hash = 7 * hash + this.stripePattern.hashCode();
    return hash;
  }

  public static void main(String args[]) {
    Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
    Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
    Tiger siberianTiger = new Tiger("White", "Sparse", 4);
    System.out.println("bengalTiger1 and bengalTiger2: "
        + bengalTiger1.equals(bengalTiger2));
    System.out.println("bengalTiger1 and siberianTiger: "
        + bengalTiger1.equals(siberianTiger));

    System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
    System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
    System.out.println("siberianTiger hashCode: "
        + siberianTiger.hashCode());
  }

  public String getColor() {
    return color;
  }

  public String getStripePattern() {
    return stripePattern;
  }

  public Tiger(String color, String stripePattern, int height) {
    this.color = color;
    this.stripePattern = stripePattern;
    this.height = height;

  }
}

Esempio di output del codice:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: 1227465966

7

Logicamente abbiamo:

a.getClass().equals(b.getClass()) && a.equals(b)a.hashCode() == b.hashCode()

Ma non viceversa!


6

Un gotcha che ho trovato è dove due oggetti contengono riferimenti reciproci (un esempio è una relazione genitore / figlio con un metodo di convenienza sul genitore per ottenere tutti i figli).
Questo genere di cose è abbastanza comune quando si eseguono mappature di Hibernate, ad esempio.

Se includi entrambe le estremità della relazione nel tuo hashCode o equivale ai test è possibile entrare in un ciclo ricorsivo che termina in StackOverflowException.
La soluzione più semplice è quella di non includere la raccolta getChildren nei metodi.


5
Penso che la teoria alla base qui sia quella di distinguere tra attributi , aggregati e associati di un oggetto. Le associazioni non dovrebbero partecipare equals(). Se uno scienziato pazzo creasse un mio duplicato saremmo equivalenti. Ma non avremmo lo stesso padre.
Raedwald,
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.