Welche Probleme sollten beim Überschreiben von equals und hashCode in Java berücksichtigt werden?

617

Welche Probleme / Fallstricke müssen beim Überschreiben berücksichtigt werden equalsund hashCode?

Matt Sheppard
quelle

Antworten:

1439

Die Theorie (für die Sprachanwälte und die mathematisch geneigten):

equals()( javadoc ) muss eine Äquivalenzbeziehung definieren (sie muss reflexiv , symmetrisch und transitiv sein ). Außerdem muss es konsistent sein (wenn die Objekte nicht geändert werden, muss immer derselbe Wert zurückgegeben werden). Außerdem o.equals(null)muss immer false zurückgegeben werden.

hashCode()( javadoc ) muss ebenfalls konsistent sein (wenn das Objekt nicht in Bezug auf geändert wird equals(), muss es weiterhin denselben Wert zurückgeben).

Die Beziehung zwischen den beiden Methoden ist:

Wann immer a.equals(b), dann a.hashCode()muss das gleiche sein wie b.hashCode().

In der Praxis:

Wenn Sie einen überschreiben, sollten Sie den anderen überschreiben.

Verwenden Sie denselben Feldsatz, den Sie equals()zum Berechnen verwenden hashCode().

Verwenden Sie die hervorragenden Hilfsklassen EqualsBuilder und HashCodeBuilder aus der Apache Commons Lang- Bibliothek. Ein Beispiel:

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

Denken Sie auch daran:

Stellen Sie bei Verwendung einer Hash-basierten Sammlung oder Map wie HashSet , LinkedHashSet , HashMap , Hashtable oder WeakHashMap sicher, dass sich der HashCode () der Schlüsselobjekte, die Sie in die Sammlung einfügen , niemals ändert, während sich das Objekt in der Sammlung befindet. Die kugelsichere Möglichkeit, dies sicherzustellen, besteht darin, Ihre Schlüssel unveränderlich zu machen, was auch andere Vorteile bietet .

Antti Sykäri
quelle
12
Zusätzlicher Punkt zu appendSuper (): Sie sollten es in hashCode () und equals () genau dann verwenden, wenn Sie das Gleichheitsverhalten der Oberklasse erben möchten. Wenn Sie beispielsweise direkt von Object ableiten, macht dies keinen Sinn, da alle Objekte standardmäßig unterschiedlich sind.
Antti Kissaniemi
312
Sie können Eclipse veranlassen, die beiden Methoden für Sie zu generieren: Source> Generate hashCode () und equals ().
Rok Strniša
27
Gleiches gilt für Netbeans: developmentality.wordpress.com/2010/08/24/…
Seinecle
6
@Darthenius Eclipse generiert gleich verwendet getClass (), was in einigen Fällen Probleme verursachen kann (siehe Effektives Java-Element 8)
AndroidGecko
7
Die erste Nullprüfung ist nicht erforderlich, da instanceoffalse zurückgegeben wird, wenn der erste Operand null ist (wieder effektives Java).
Izaban
295

Es gibt einige Probleme, die Sie beachten sollten, wenn Sie mit Klassen arbeiten, die mit einem Object-Relationship Mapper (ORM) wie Hibernate beibehalten werden, wenn Sie nicht der Meinung sind, dass dies bereits unangemessen kompliziert ist!

Faul geladene Objekte sind Unterklassen

Wenn Ihre Objekte mithilfe eines ORM beibehalten werden, handelt es sich in vielen Fällen um dynamische Proxys, um zu vermeiden, dass Objekte zu früh aus dem Datenspeicher geladen werden. Diese Proxys werden als Unterklassen Ihrer eigenen Klasse implementiert. Dies bedeutet, dass this.getClass() == o.getClass()zurückkehren wird false. Zum Beispiel:

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

Wenn Sie mit einem ORM zu o instanceof Persontun haben, ist die Verwendung das einzige, was sich korrekt verhält.

Faul geladene Objekte haben Nullfelder

ORMs verwenden normalerweise die Getter, um das Laden von faul geladenen Objekten zu erzwingen. Dies bedeutet , dass person.namesein wird , nullwenn personfaul geladen ist, auch wenn person.getName()Kräfte Laden und kehrt „John Doe“. Nach meiner Erfahrung tritt dies häufiger in hashCode()und auf equals().

Wenn Sie mit einem ORM arbeiten, stellen Sie sicher, dass Sie immer Getter verwenden und niemals Referenzen in hashCode()und equals().

Durch das Speichern eines Objekts wird sein Status geändert

Persistente Objekte verwenden häufig ein idFeld, um den Schlüssel des Objekts zu halten. Dieses Feld wird automatisch aktualisiert, wenn ein Objekt zum ersten Mal gespeichert wird. Verwenden Sie kein ID-Feld in hashCode(). Aber Sie können es in verwenden equals().

Ein Muster, das ich oft benutze, ist

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

Aber: Sie können nicht getId()in aufnehmen hashCode(). Wenn Sie dies tun, werden die hashCodeÄnderungen eines Objekts geändert. Wenn sich das Objekt in a befindet HashSet, werden Sie es "nie" wiederfinden.

In meinem PersonBeispiel würde ich wahrscheinlich getName()für hashCodeund getId()plus getName()(nur für Paranoia) für verwenden equals(). Es ist in Ordnung, wenn das Risiko von "Kollisionen" besteht hashCode(), aber niemals in Ordnung equals().

hashCode() sollte die unveränderte Teilmenge der Eigenschaften von verwenden equals()

Johannes Brodwall
quelle
2
@ Johannes Brodwall: Ich verstehe nicht Saving an object will change it's state! hashCodemuss zurückkehren int, also wie werden Sie verwenden getName()? Können Sie ein Beispiel für IhrehashCode
Jimmybondy
@ Jimmybondy: getName gibt ein String-Objekt zurück, das auch einen Hash-Code hat, der verwendet werden kann
mateusz.fiolka
85

Eine Klarstellung über die obj.getClass() != getClass().

Diese Aussage ist das Ergebnis equals()einer unfreundlichen Vererbung. Die JLS (Java Language Specification) gibt an, dass wenn A.equals(B) == truedann B.equals(A)auch zurückgegeben werden muss true. Wenn Sie diese Anweisung weglassen, die Klassen erbt, die überschreiben equals()(und ihr Verhalten ändern), wird diese Spezifikation verletzt.

Betrachten Sie das folgende Beispiel dafür, was passiert, wenn die Anweisung weggelassen wird:

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

Tun Sie new A(1).equals(new A(1))auch, new B(1,1).equals(new B(1,1))Ergebnis geben wahr aus, wie es sollte.

Das sieht alles sehr gut aus, aber schauen Sie, was passiert, wenn wir versuchen, beide Klassen zu verwenden:

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

Offensichtlich ist das falsch.

Wenn Sie den symmetrischen Zustand sicherstellen möchten. a = b wenn b = a und das Liskov-Substitutionsprinzip super.equals(other)nicht nur im Fall einer BInstanz aufrufen , sondern nach folgender Prüfung prüfen A:

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;

Welches wird ausgegeben:

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

Wenn aes sich nicht um eine Referenz von handelt B, kann es sich auch um eine Referenz der Klasse handeln A(weil Sie sie erweitern). In diesem Fall rufen Sie super.equals() auch auf .

Ran Biron
quelle
2
Auf diese Weise können Sie Gleichungen symmetrisch machen (wenn Sie ein Objekt der Oberklasse mit einem Unterklassenobjekt vergleichen, verwenden Sie immer die Gleichheit der Unterklasse), wenn (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) return obj.equals (this);
Pihentagy
5
@pihentagy - dann würde ich einen Stackoverflow erhalten, wenn die Implementierungsklasse die equals-Methode nicht überschreibt. kein Spaß.
Ran Biron
2
Sie erhalten keinen Stackoverflow. Wenn die Methode equals nicht überschrieben wird, rufen Sie denselben Code erneut auf, aber die Bedingung für die Rekursion ist immer falsch!
Jacob Raihle
@pihentagy: Wie verhält sich das, wenn es zwei verschiedene abgeleitete Klassen gibt? Wenn a ThingWithOptionSetAgleich a sein kann, Thingvorausgesetzt, dass alle zusätzlichen Optionen Standardwerte haben, und ebenso für a ThingWithOptionSetB, sollte es möglich ThingWithOptionSetAsein, dass a ThingWithOptionSetBnur dann mit a verglichen wird, wenn alle Nicht-Basiseigenschaften beider Objekte mit ihren Standardeinstellungen übereinstimmen , aber Ich verstehe nicht, wie Sie das testen.
Supercat
7
Das Problem dabei ist, dass es die Transitivität bricht. Wenn Sie hinzufügen B b2 = new B(1,99), dann b.equals(a) == trueund a.equals(b2) == trueaber b.equals(b2) == false.
Nickgrim
46

Informationen zu einer vererbungsfreundlichen Implementierung finden Sie in der Lösung von Tal Cohen: Wie implementiere ich die equals () -Methode korrekt?

Zusammenfassung:

In seinem Buch Effective Java Programming Language Guide (Addison-Wesley, 2001) behauptet Joshua Bloch: "Es gibt einfach keine Möglichkeit, eine instanziierbare Klasse zu erweitern und einen Aspekt hinzuzufügen, während der gleichwertige Vertrag erhalten bleibt." Tal ist anderer Meinung.

Seine Lösung besteht darin, equals () zu implementieren, indem ein anderes unsymmetrisches blindlyEquals () in beide Richtungen aufgerufen wird. blindlyEquals () wird von Unterklassen überschrieben, equals () wird geerbt und niemals überschrieben.

Beispiel:

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

Beachten Sie, dass equals () über Vererbungshierarchien hinweg funktionieren muss, wenn das Liskov-Substitutionsprinzip erfüllt werden soll.

Kevin Wong
quelle
10
Schauen Sie sich die hier erläuterte canEqual-Methode an - nach demselben Prinzip funktionieren beide Lösungen, aber mit canEqual werden dieselben Felder nicht zweimal verglichen (oben wird px == this.x in beide Richtungen getestet): artima.com /lejava/articles/equality.html
Blaisorblade
2
Auf jeden Fall halte ich das nicht für eine gute Idee. Dies macht den Equals-Vertrag unnötig verwirrend - jemand, der zwei Punktparameter a und b verwendet, muss sich der Möglichkeit bewusst sein, dass a.getX () == b.getX () und a.getY () == b.getY () kann wahr sein, aber a.equals (b) und b.equals (a) sind beide falsch (wenn nur einer ein ColorPoint ist).
Kevin
Grundsätzlich ist dies so if (this.getClass() != o.getClass()) return false, aber flexibel, da es nur dann false zurückgibt, wenn sich die abgeleiteten Klassen die Mühe machen, Gleichheit zu ändern. Ist das richtig?
Aleksandr Dubinsky
31

Immer noch erstaunt, dass niemand die Guavenbibliothek dafür empfohlen hat.

 //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());
    }
Eugene
quelle
23
java.util.Objects.hash () und java.util.Objects.equals () sind Teil von Java 7 (veröffentlicht 2011), daher benötigen Sie dafür keine Guava.
Hermann
1
Natürlich, aber Sie sollten dies vermeiden, da Oracle keine öffentlichen Updates mehr für Java 6 bereitstellt (dies ist seit Februar 2013 der Fall).
Hermann
6
Ihr thisin this.getDate()bedeutet nichts (außer Unordnung)
Steve Kuo
1
Ihr Ausdruck "not instanceof" benötigt eine zusätzliche Klammer : if (!(otherObject instanceof DateAndPattern)) {. Stimmen Sie Hernan und Steve Kuo zu (obwohl dies eine Frage der persönlichen Präferenz ist), aber trotzdem +1.
Amos M. Carpenter
26

In der Superklasse gibt es zwei Methoden: java.lang.Object. Wir müssen sie zu einem benutzerdefinierten Objekt überschreiben.

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

Gleiche Objekte müssen denselben Hash-Code erzeugen, solange sie gleich sind. Ungleiche Objekte müssen jedoch keine unterschiedlichen Hash-Codes erzeugen.

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
}

Wenn Sie mehr erhalten möchten, überprüfen Sie bitte diesen Link als http://www.javaranch.com/journal/2002/10/equalhash.html

Dies ist ein weiteres Beispiel, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

Habe Spaß! @. @

Luna Kong
quelle
Entschuldigung, aber ich verstehe diese Aussage zur hashCode-Methode nicht: Es ist nicht legal, wenn mehr Variablen als equals () verwendet werden. Wenn ich jedoch mit mehr Variablen codiere, wird mein Code kompiliert. Warum ist es nicht legal?
Adryr83
19

Es gibt verschiedene Möglichkeiten, die Klassengleichheit zu überprüfen, bevor die Mitgliedergleichheit überprüft wird, und ich denke, beide sind unter den richtigen Umständen nützlich.

  1. Verwenden Sie den instanceofOperator.
  2. Verwenden Sie this.getClass().equals(that.getClass()).

Ich verwende # 1 in einer finalEquals-Implementierung oder bei der Implementierung einer Schnittstelle, die einen Algorithmus für Equals vorschreibt (wie die java.utilSammlungsschnittstellen - der richtige Weg, um mit (obj instanceof Set)oder der von Ihnen implementierten Schnittstelle zu prüfen ). Es ist im Allgemeinen eine schlechte Wahl, wenn Gleichheit überschrieben werden kann, da dies die Symmetrieeigenschaft bricht.

Mit Option 2 kann die Klasse sicher erweitert werden, ohne dass Gleichheit überschrieben oder die Symmetrie unterbrochen wird.

Wenn Ihre Klasse auch ist Comparable, sollten die Methoden equalsund compareToauch konsistent sein. Hier ist eine Vorlage für die equals-Methode in einer ComparableKlasse:

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

}
erickson
quelle
1
+1 dafür. Weder getClass () noch instanceof sind ein Allheilmittel, und dies ist eine gute Erklärung dafür, wie man sich beiden nähert. Denken Sie nicht, dass es einen Grund gibt, this.getClass () == that.getClass () nicht zu tun, anstatt equals () zu verwenden.
Paul Cantrell
Hier gibt es ein Problem. Anonyme Klassen, die keine Aspekte hinzufügen oder die Methode equals überschreiben, bestehen die getClass-Prüfung nicht, obwohl sie gleich sein sollten.
Steinybot
@Steiny Mir ist nicht klar, dass Objekte verschiedener Typen gleich sein sollten; Ich stelle mir verschiedene Implementierungen einer Schnittstelle als eine gemeinsame anonyme Klasse vor. Können Sie ein Beispiel geben, um Ihre Prämisse zu unterstützen?
Erickson
MyClass a = neue MyClass (123); MyClass b = new MyClass (123) {// Methode überschreiben}; // a.equals (b) ist falsch, wenn this.getClass () verwendet wird. equals (that.getClass ())
steinybot
1
@Steiny Richtig. Wie es in den meisten Fällen der Fall sein sollte, insbesondere wenn eine Methode überschrieben anstatt hinzugefügt wird. Betrachten Sie mein Beispiel oben. Wenn dies nicht finalder compareTo()Fall ist und die Methode zum Umkehren der Sortierreihenfolge überschrieben wurde, sollten Instanzen der Unterklasse und der Oberklasse nicht als gleich angesehen werden. Wenn diese Objekte zusammen in einem Baum verwendet wurden, sind Schlüssel, die gemäß einer instanceofImplementierung "gleich" waren, möglicherweise nicht auffindbar.
Erickson
16

Für Gleichberechtigte schauen Sie in Secrets of Equals von Angelika Langer . Ich liebe es sehr. Sie ist auch eine großartige FAQ über Generika in Java . Sehen Sie sich hier ihre anderen Artikel an (scrollen Sie nach unten zu "Core Java"), wo sie auch mit Teil 2 und "Mixed Type Comparison" fortfährt. Viel Spaß beim Lesen!

Johannes Schaub - litb
quelle
11

Die Methode equals () wird verwendet, um die Gleichheit zweier Objekte zu bestimmen.

da der int-Wert von 10 immer gleich 10 ist. Bei dieser Methode equals () geht es jedoch um die Gleichheit zweier Objekte. Wenn wir Objekt sagen, wird es Eigenschaften haben. Um über die Gleichheit zu entscheiden, werden diese Eigenschaften berücksichtigt. Es ist nicht erforderlich, dass alle Eigenschaften berücksichtigt werden, um die Gleichheit zu bestimmen, und in Bezug auf die Klassendefinition und den Kontext kann entschieden werden. Dann kann die equals () -Methode überschrieben werden.

Wir sollten die hashCode () -Methode immer überschreiben, wenn wir die equals () -Methode überschreiben. Wenn nicht, was wird passieren? Wenn wir in unserer Anwendung Hashtabellen verwenden, verhält sich diese nicht wie erwartet. Da der Hashcode zur Bestimmung der Gleichheit der gespeicherten Werte verwendet wird, gibt er nicht den richtigen entsprechenden Wert für einen Schlüssel zurück.

Die angegebene Standardimplementierung ist die hashCode () -Methode in der Object-Klasse. Sie verwendet die interne Adresse des Objekts und konvertiert sie in eine Ganzzahl und gibt sie zurück.

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;

  }
}

Beispiel für eine Code-Ausgabe:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: 1227465966
Rohan Kamat
quelle
7

Logischerweise haben wir:

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

Aber nicht umgekehrt!

Khaled.K
quelle
6

Ein Fall, den ich gefunden habe, ist, dass zwei Objekte Verweise aufeinander enthalten (ein Beispiel ist eine Eltern-Kind-Beziehung mit einer praktischen Methode für den Elternteil, um alle Kinder zu erhalten).
Diese Art von Dingen ist ziemlich häufig, wenn Sie beispielsweise Zuordnungen im Ruhezustand ausführen.

Wenn Sie beide Enden der Beziehung in Ihren Hashcode aufnehmen oder Tests gleich sind, können Sie in eine rekursive Schleife geraten, die in einer StackOverflowException endet.
Die einfachste Lösung besteht darin, die getChildren-Auflistung nicht in die Methoden aufzunehmen.

Darren Greaves
quelle
5
Ich denke, die zugrunde liegende Theorie besteht darin, zwischen den Attributen , Aggregaten und Assoziaten eines Objekts zu unterscheiden. Die Verbände sollten nicht teilnehmen equals(). Wenn ein verrückter Wissenschaftler ein Duplikat von mir erstellen würde, wären wir gleichwertig. Aber wir hätten nicht den gleichen Vater.
Raedwald