Jakie problemy należy wziąć pod uwagę przy nadpisywaniu równości i hashCode w Javie?

617

Jakie problemy / pułapki należy wziąć pod uwagę przy nadpisywaniu equalsi hashCode?

Matt Sheppard
źródło

Odpowiedzi:

1439

Teoria (dla prawników językowych i matematycznie skłonnych):

equals()( javadoc ) musi zdefiniować relację równoważności (musi być zwrotny , symetryczny i przechodni ). Ponadto musi być spójny (jeśli obiekty nie są modyfikowane, musi zwracać tę samą wartość). Ponadto o.equals(null)zawsze musi zwracać wartość false.

hashCode()( javadoc ) musi być również spójny (jeśli obiekt nie został zmodyfikowany pod względem equals(), musi zwracać tę samą wartość).

Zależność między dwóch metod:

Ilekroć a.equals(b), a.hashCode()musi być taki sam jak b.hashCode().

W praktyce:

Jeśli zastąpisz jedno, powinieneś zastąpić drugie.

Użyj tego samego zestawu pól, których używasz do obliczania equals()do obliczaniahashCode() .

Skorzystaj z doskonałych klas pomocniczych EqualsBuilder i HashCodeBuilder z biblioteki Apache Commons Lang . Przykład:

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

Pamiętaj także:

Podczas korzystania z kolekcji lub mapy opartej na haszowaniu , takich jak HashSet , LinkedHashSet , HashMap , Hashtable lub WeakHashMap , upewnij się, że hashCode () kluczowych obiektów, które umieszczasz w kolekcji, nigdy się nie zmienia, gdy obiekt jest w kolekcji. Kuloodpornym sposobem na to jest uczynienie kluczy niezmiennymi, co ma również inne zalety .

Antti Sykäri
źródło
12
Dodatkowa uwaga na temat appendSuper (): powinieneś używać jej w hashCode () i equals () tylko wtedy, gdy chcesz odziedziczyć zachowanie równości superklasy. Na przykład, jeśli wywodzisz się bezpośrednio z obiektu, nie ma sensu, ponieważ wszystkie obiekty są domyślnie różne.
Antti Kissaniemi
312
Możesz przekonać Eclipse do wygenerowania dwóch metod: Źródło> Wygeneruj hashCode () i equals ().
Rok Strniša
27
To samo dotyczy Netbeans: developmentality.wordpress.com/2010/08/24/…
seinecle
6
@Darthenius wygenerowane przez Eclipse jest równe, używa getClass (), co może powodować problemy w niektórych przypadkach (patrz Efektywna pozycja Java 8)
AndroidGecko
7
Pierwsze sprawdzenie wartości NULL nie jest konieczne, biorąc pod uwagę fakt, że instanceofzwraca false, jeśli jego pierwszy argument operacji ma wartość NULL (ponownie skuteczna Java).
izaban
295

Istnieją pewne problemy, na które warto zwrócić uwagę, jeśli masz do czynienia z klasami, które są utrwalane za pomocą Mapera Relacji Obiektowych (ORM), takich jak Hibernacja, jeśli nie sądzisz, że to już było nadmiernie skomplikowane!

Leniwie ładowane obiekty są podklasami

Jeśli obiekty są utrwalane przy użyciu ORM, w wielu przypadkach będziesz mieć do czynienia z dynamicznymi serwerami proxy, aby uniknąć zbyt wczesnego ładowania obiektu z magazynu danych. Te proxy są implementowane jako podklasy własnej klasy. Oznacza to, że this.getClass() == o.getClass()wróci false. Na przykład:

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

Jeśli masz do czynienia z ORM, używanie o instanceof Personjest jedyną rzeczą, która będzie działać poprawnie.

Leniwie ładowane obiekty mają pola zerowe

ORM zwykle używają modułów pobierających, aby wymusić ładowanie leniwie ładowanych obiektów. Oznacza to, że person.namebędzie, nulljeśli personjest leniwie załadowany, nawet jeśli person.getName()wymusza ładowanie i zwraca „John Doe”. Z mojego doświadczenia wynika, że ​​pojawia się częściej w hashCode()iequals() .

Jeśli masz do czynienia z ORM, pamiętaj, aby zawsze używać getterów i nigdy nie podawać referencji w hashCode()i equals().

Zapisanie obiektu zmieni jego stan

Trwałe obiekty często używają idpola do przechowywania klucza obiektu. To pole zostanie automatycznie zaktualizowane przy pierwszym zapisaniu obiektu. Nie używaj pola identyfikatora w hashCode(). Ale możesz go użyć w equals().

Często używam wzoru

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

Ale: nie można zawierać getId()w hashCode(). Jeśli to zrobisz, gdy obiekt zostanie utrwalony, jego hashCodezmiany zostaną zmienione. Jeśli obiekt znajduje się wHashSet „nigdy”, nie znajdziesz go ponownie.

W moim Personprzykładzie, prawdopodobnie użyłby getName()do hashCodei getId()Plus getName()(tylko dla paranoi) dla equals(). Jest w porządku, jeśli istnieje ryzyko „kolizji” hashCode(), ale nigdy nie jest w porządku equals().

hashCode() powinien używać niezmiennego podzbioru właściwości z equals()

Johannes Brodwall
źródło
2
@Johannes Brodwall: nie rozumiem Saving an object will change it's state! hashCodemusi wrócić int, więc jak będziesz używać getName()? Czy możesz podać przykład swojegohashCode
jimmybondy
@jimmybondy: getName zwróci obiekt String, który również ma hashCode, którego można użyć
mateusz.fiolka
85

Wyjaśnienie na temat obj.getClass() != getClass().

To stwierdzenie jest wynikiem equals()nieprzyjaznego dziedziczenia. JLS (specyfikacja języka Java) określa, że ​​jeśli A.equals(B) == truewtedy B.equals(A)musi również zwrócić true. Pominięcie instrukcji dziedziczenia klas, które zastąpią equals()(i zmienią jej zachowanie) spowoduje uszkodzenie tej specyfikacji.

Rozważ następujący przykład tego, co dzieje się, gdy instrukcja zostanie pominięta:

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

Robiąc new A(1).equals(new A(1))również, new B(1,1).equals(new B(1,1))wynik dawaj prawdziwy, tak jak powinien.

Wszystko wygląda bardzo dobrze, ale spójrz, co się stanie, jeśli spróbujemy użyć obu klas:

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

Oczywiście to źle.

Jeśli chcesz zapewnić warunek symetryczny. a = b, jeśli b = a, a zasada podstawienia Liskowa wywołuje super.equals(other)nie tylko w przypadku Binstancji, ale sprawdza na Aprzykład po :

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;

Co da wynik:

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

Jeśli, jeśli anie jest odwołaniem do B, może to być odwołanie do klasy A(ponieważ go rozszerzasz), w tym przypadku super.equals() również wywołujesz .

Ran Biron
źródło
2
W ten sposób możesz ustawić symetryczny równość (porównując obiekt nadklasy z obiektem podklasy, zawsze używaj równości podklasy), jeśli (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) return obj.equals (this);
pihentagy
5
@pihentagy - wtedy dostanę przepływ stosu, gdy klasa implementacji nie zastąpi metody równości. nie śmieszne.
Ran Biron,
2
Nie otrzymasz przepływu stosu. Jeśli metoda równa się nie zostanie zastąpiona, ponownie wywołasz ten sam kod, ale warunek rekurencji zawsze będzie fałszywy!
Jacob Raihle
@pihentagy: Jak to działa, jeśli istnieją dwie różne klasy pochodne? Jeśli a ThingWithOptionSetAmoże być równe Thingpod warunkiem, że wszystkie dodatkowe opcje mają wartości domyślne, i podobnie dla a ThingWithOptionSetB, to powinno być możliwe ThingWithOptionSetAporównanie a równe ThingWithOptionSetBtylko wtedy, gdy wszystkie nie-bazowe właściwości obu obiektów odpowiadają ich wartościom domyślnym, ale Nie rozumiem, jak to testujesz.
supercat
7
Problem w tym, że łamie przechodniość. Jeśli dodasz B b2 = new B(1,99), to b.equals(a) == truei a.equals(b2) == trueale b.equals(b2) == false.
nickgrim
46

Aby uzyskać implementację przyjazną dziedziczeniu, sprawdź rozwiązanie Tal Cohen: Jak poprawnie wdrożyć metodę equals ()?

Podsumowanie:

W swojej książce Effective Java Programming Language Guide (Addison-Wesley, 2001) Joshua Bloch twierdzi, że „po prostu nie ma sposobu, aby rozszerzyć możliwą do utworzenia klasę i dodać aspekt przy jednoczesnym zachowaniu równości”. Tal się nie zgadza.

Jego rozwiązaniem jest zaimplementowanie equals () poprzez wywołanie innej niesymetrycznej ślepoEquals () w obie strony. blindlyEquals () jest zastępowane przez podklasy, equals () jest dziedziczone i nigdy nie jest zastępowane.

Przykład:

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

Zauważ, że equals () musi działać w hierarchiach dziedziczenia, jeśli zasada podstawienia Liskowa ma być spełniona.

Kevin Wong
źródło
10
Spójrz na wyjaśnioną tutaj metodę canEqual - ta sama zasada sprawia, że ​​oba rozwiązania działają, ale dzięki canEqual nie porównujesz dwukrotnie tych samych pól (powyżej, px == this.x będzie testowany w obu kierunkach): artima.com /lejava/articles/equality.html
Blaisorblade,
2
W każdym razie nie sądzę, że to dobry pomysł. Sprawia, że ​​umowa Equals jest niepotrzebnie myląca - ktoś, kto bierze dwa parametry punktu, a i b, musi być świadomy możliwości, że a.getX () == b.getX () i a.getY () == b.getY () może być prawdziwe, ale zarówno a.equals (b), jak i b.equals (a) są fałszywe (jeśli tylko jeden jest ColorPoint).
Kevin,
Zasadniczo jest to podobne if (this.getClass() != o.getClass()) return false, ale elastyczne, ponieważ zwraca fałsz tylko wtedy, gdy pochodne klasy zawracają sobie głowę modyfikacją równości. Czy to prawda?
Aleksandr Dubinsky
31

Nadal jestem zdumiony, że nikt nie zalecił do tego biblioteki guava.

 //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
źródło
23
java.util.Objects.hash () i java.util.Objects.equals () są częścią Java 7 (wydanej w 2011 roku), więc nie potrzebujesz do tego Guava.
herman
1
oczywiście, ale należy tego unikać, ponieważ Oracle nie udostępnia już publicznych aktualizacji Java 6 (tak jest od lutego 2013 r.).
herman
6
Twój thisw this.getDate()nic nie znaczy (inne niż bałaganu)
Steve Kuo
1
Twój „nie jest instancją” wyrażenie musi dodatkowy wspornik: if (!(otherObject instanceof DateAndPattern)) {. Zgadzam się z Hernanem i Steve'em Kuo (choć jest to kwestia osobistych preferencji), ale mimo to daje +1.
Amos M. Carpenter,
26

Istnieją dwie metody w superklasie jako java.lang.Object. Musimy przesłonić je do obiektu niestandardowego.

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

Równe obiekty muszą generować ten sam kod skrótu, o ile są one równe, jednak nierówne obiekty nie muszą tworzyć odrębnych kodów skrótu.

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
}

Jeśli chcesz uzyskać więcej, sprawdź ten link jako http://www.javaranch.com/journal/2002/10/equalhash.html

To kolejny przykład http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

Baw się dobrze! @. @

Luna Kong
źródło
Przepraszam, ale nie rozumiem tego stwierdzenia o metodzie hashCode: nie jest legalne, jeśli używa więcej zmiennych niż equals (). Ale jeśli koduję z większą liczbą zmiennych, mój kod się kompiluje. Dlaczego to nie jest legalne?
Adryr83
19

Istnieje kilka sposobów sprawdzenia równości klas przed sprawdzeniem równości członków i myślę, że obie są przydatne w odpowiednich okolicznościach.

  1. Użyj instanceofoperatora.
  2. Zastosowanie this.getClass().equals(that.getClass()).

Używam nr 1 w finalimplementacji równości lub podczas implementacji interfejsu, który określa algorytm równości (np. java.utilInterfejsy kolekcji - właściwy sposób sprawdzenia za pomocą(obj instanceof Set) lub dowolnego interfejsu, który implementujesz). Zasadniczo jest to zły wybór, gdy równość może zostać zastąpiona, ponieważ psuje to właściwość symetrii.

Opcja nr 2 umożliwia bezpieczne rozszerzenie klasy bez nadpisywania równości lub łamania symetrii.

Jeśli twoja klasa jest również Comparable, metody equalsi również compareTopowinny być spójne. Oto szablon dla metody równej w Comparableklasie:

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
źródło
1
+1 za to. Ani getClass (), ani instanceof nie jest panaceum i jest to dobre wytłumaczenie tego, jak podejść do obu. Nie sądzę, aby istniał jakiś powód, aby tego nie robić.getClass () == that.getClass () zamiast używania equals ().
Paul Cantrell
Jest z tym jeden problem. Anonimowe klasy, które nie dodają żadnych aspektów ani nie zastępują metody equals, nie przejdą kontroli getClass, nawet jeśli powinny być równe.
steinybot
@ Steiny Nie jest dla mnie jasne, że obiekty różnych typów powinny być równe; Myślę o różnych implementacjach interfejsu jako wspólnej anonimowej klasie. Czy możesz podać przykład na poparcie swojego założenia?
erickson
MyClass a = nowa MyClass (123); MyClass b = nowa MyClass (123) {// Zastąp jakąś metodę}; // a.equals (b) jest fałszem przy użyciu this.getClass (). equals (that.getClass ())
steinybot
1
@Steiny Right. Tak jak powinno w większości przypadków, szczególnie jeśli metoda zostanie zastąpiona zamiast dodana. Rozważ mój przykład powyżej. Jeśli tak nie było final, a compareTo()metoda została zastąpiona w celu odwrócenia kolejności sortowania, wystąpienia podklasy i nadklasy nie powinny być uważane za równe. Gdy te obiekty zostały użyte razem w drzewie, klucze, które były „równe” zgodnie z instanceofimplementacją, mogą nie zostać znalezione.
erickson,
16

Dla równych, spójrz na Secrets of Equals autorstwa Angeliki Langer . Bardzo to kocham. Jest również świetnym FAQ na temat generics w Javie . Zobacz jej inne artykuły tutaj (przewiń w dół do „Core Java”), gdzie ona również kontynuuje część 2 i „porównanie typów mieszanych”. Miłej lektury!

Johannes Schaub - litb
źródło
11

Metoda equals () służy do ustalenia równości dwóch obiektów.

ponieważ wartość int 10 jest zawsze równa 10. Ale ta metoda equals () dotyczy równości dwóch obiektów. Kiedy mówimy obiekt, będzie miał właściwości. Aby podjąć decyzję o równości, bierze się pod uwagę te właściwości. Nie jest konieczne, aby wszystkie właściwości były brane pod uwagę w celu ustalenia równości, a w odniesieniu do definicji klasy i kontekstu można zdecydować. Następnie można zastąpić metodę equals ().

zawsze powinniśmy nadpisywać metodę hashCode () za każdym razem, gdy nadpisujemy metodę equals (). Jeśli nie, co się stanie? Jeśli w naszej aplikacji wykorzystamy tabele skrótów, nie będą one działały zgodnie z oczekiwaniami. Ponieważ hashCode jest używany do określania równości przechowywanych wartości, nie zwróci odpowiedniej odpowiedniej wartości dla klucza.

Domyślną implementacją jest metoda hashCode () w klasie Object używa wewnętrznego adresu obiektu i konwertuje go na liczbę całkowitą i zwraca go.

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;

  }
}

Przykładowy kod wyjściowy:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: 1227465966
Rohan Kamat
źródło
7

Logicznie mamy:

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

Ale nie odwrotnie!

Khaled.K
źródło
6

Znalazłem jedną gotcha, w której dwa obiekty zawierają odniesienia do siebie (jednym przykładem jest relacja rodzic / dziecko z metodą wygodną dla rodzica, aby uzyskać wszystkie dzieci).
Tego rodzaju rzeczy są dość powszechne, na przykład podczas mapowania hibernacji.

Jeśli umieścisz oba końce relacji w swoim hashCode lub testy równości, możesz dostać się do pętli rekurencyjnej, która kończy się wyjątkiem StackOverflowException.
Najprostszym rozwiązaniem jest niewłączenie kolekcji getChildren do metod.

Darren Greaves
źródło
5
Myślę, że podstawową teorią tutaj jest rozróżnienie między atrybutami , agregatami i skojarzeniami obiektu. W asssociations nie powinien uczestniczyć w equals(). Gdyby szalony naukowiec stworzył moją kopię, bylibyśmy równoważni. Ale nie mielibyśmy tego samego ojca.
Raedwald