CodeGym /Blog Java /Random-PL /metody equals i hashCode: najlepsze praktyki
Autor
Milan Vucic
Programming Tutor at Codementor.io

metody equals i hashCode: najlepsze praktyki

Opublikowano w grupie Random-PL
Cześć! Dzisiaj porozmawiamy o dwóch ważnych metodach w Javie: equals()i hashCode(). To nie pierwszy raz, kiedy ich spotykamy: kurs CodeGym zaczyna się od krótkiej lekcji na temat equals()— przeczytaj ją, jeśli zapomniałeś lub nie widziałeś jej wcześniej… metody equals i hashCode: najlepsze praktyki — 1W dzisiejszej lekcji porozmawiamy o szczegółowo te pojęcia. I uwierz mi, mamy o czym rozmawiać! Ale zanim przejdziemy do nowego, odświeżmy to, co już omówiliśmy :) Jak pamiętacie, zwykle nie jest dobrym pomysłem porównywanie dwóch obiektów za pomocą operatora, ==ponieważ ==porównuje referencje. Oto nasz przykład z samochodami z ostatniej lekcji:

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
Wyjście konsoli:

false
Wygląda na to, że stworzyliśmy dwa identyczne Carobiekty: wartości odpowiednich pól dwóch obiektów samochodów są takie same, ale wynik porównania jest nadal fałszywy. Znamy już przyczynę: odwołania car1i car2wskazują różne adresy pamięci, więc nie są one równe. Ale nadal chcemy porównać dwa obiekty, a nie dwa odniesienia. Najlepszym rozwiązaniem do porównywania obiektów jest equals()metoda.

równa się().

Być może pamiętasz, że nie tworzymy tej metody od zera, raczej ją nadpisujemy: metoda equals()jest zdefiniowana w Objectklasie. To powiedziawszy, w swojej zwykłej formie jest mało przydatne:

public boolean equals(Object obj) {
   return (this == obj);
}
W ten sposób equals()metoda jest zdefiniowana w Objectklasie. To jeszcze raz porównanie referencji. Dlaczego tak to zrobili? Skąd twórcy języka wiedzą, które obiekty w twoim programie są uważane za równe, a które nie? :) To jest główny punkt metody equals()— twórcą klasy jest ten, kto określa, jakie cechy są używane podczas sprawdzania równości obiektów klasy. Następnie nadpisujesz equals()metodę w swojej klasie. Jeśli nie do końca rozumiesz znaczenie „określa, które cechy”, rozważmy przykład. Oto prosta klasa reprezentująca człowieka: Man.

public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   // Getters, setters, etc.
}
Załóżmy, że piszemy program, który musi określić, czy dwie osoby są bliźniakami jednojajowymi, czy po prostu sobowtórami. Mamy pięć cech: rozmiar nosa, kolor oczu, fryzurę, obecność blizn i wyniki badań DNA (dla uproszczenia przedstawiamy to jako kod całkowity). Jak myślisz, która z tych cech pozwoliłaby naszemu programowi zidentyfikować bliźnięta jednojajowe? metody equals i hashCode: najlepsze praktyki — 2Oczywiście tylko test DNA może dać gwarancję. Dwie osoby mogą mieć ten sam kolor oczu, fryzurę, nos, a nawet blizny — na świecie jest wielu ludzi i nie można zagwarantować, że nie ma tam żadnych sobowtórów. Potrzebujemy jednak niezawodnego mechanizmu: dopiero wynik testu DNA pozwoli nam wyciągnąć trafny wniosek. Co to oznacza dla naszej equals()metody? Musimy to nadpisać w plikuManklasy, biorąc pod uwagę wymagania naszego programu. Metoda powinna porównywać int dnaCodepole dwóch obiektów. Jeśli są równe, to przedmioty są równe.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Czy to naprawdę takie proste? Nie bardzo. Coś przeoczyliśmy. W przypadku naszych obiektów zidentyfikowaliśmy tylko jedno pole, które jest istotne dla ustalenia równości obiektów: dnaCode. Teraz wyobraź sobie, że mamy nie 1, ale 50 odpowiednich pól. A jeśli wszystkie 50 pól dwóch obiektów jest równych, to obiekty są równe. Taki scenariusz też jest możliwy. Główny problem polega na tym, że ustalenie równości poprzez porównanie 50 pól jest procesem czasochłonnym i wymagającym dużych zasobów. Teraz wyobraź sobie, że oprócz naszej Manklasy mamy Womanklasę z dokładnie tymi samymi polami, które istnieją w Man. Jeśli inny programista użyje naszych klas, może z łatwością napisać taki kod:

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

   Woman woman = new Woman(.........); // The same bunch of parameters.

   System.out.println(man.equals(woman));
}
W tym przypadku sprawdzanie wartości pól jest bezcelowe: od razu widać, że mamy obiekty dwóch różnych klas, więc nie ma możliwości, aby były sobie równe! Oznacza to, że powinniśmy dodać do equals()metody czek, porównujący klasy porównywanych obiektów. Dobrze, że o tym pomyśleliśmy!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Ale może o czymś jeszcze zapomnieliśmy? Hmm... Przynajmniej powinniśmy sprawdzić, czy nie porównujemy obiektu z samym sobą! Jeśli referencje A i B wskazują na ten sam adres pamięci, to są to te same obiekty i nie musimy tracić czasu na porównywanie 50 pól.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Nie zaszkodzi również dodać sprawdzenie null: żaden obiekt nie może być równy null. Tak więc, jeśli parametr metody ma wartość null, dodatkowe kontrole nie mają sensu. Mając to wszystko na uwadze, nasza equals()metoda dla Manklasy wygląda następująco:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Przeprowadzamy wszystkie wstępne kontrole, o których mowa powyżej. Na koniec dnia, jeśli:
  • porównujemy dwa obiekty tej samej klasy
  • a porównywane obiekty nie są tym samym obiektem
  • a przekazany obiekt nienull
…następnie przechodzimy do porównania odpowiednich cech. Dla nas oznacza to dnaCodepola dwóch obiektów. Podczas zastępowania equals()metody należy przestrzegać następujących wymagań:
  1. refleksyjność.

    Gdy equals()metoda jest używana do porównania dowolnego obiektu ze sobą, musi zwracać wartość true.
    Spełniliśmy już ten wymóg. Nasza metoda obejmuje:

    
    if (this == o) return true;
    

  2. Symetria.

    Jeśli a.equals(b) == true, to b.equals(a)musi wrócić true.
    Nasza metoda spełnia również to wymaganie.

  3. przechodniość.

    Jeśli dwa przedmioty są równe jakiemuś trzeciemu przedmiotowi, to muszą być sobie równe.
    Jeśli a.equals(b) == truei a.equals(c) == true, to b.equals(c)również musi zwrócić wartość true.

  4. Trwałość.

    Wynik equals()musi się zmienić tylko wtedy, gdy zmienione zostaną odpowiednie pola. Jeśli dane dwóch obiektów nie zmieniają się, wynik equals()musi być zawsze taki sam.

  5. Nierówność z null.

    Dla każdego obiektu a.equals(null)musi zwrócić wartość false
    To nie jest tylko zestaw "przydatnych zaleceń", ale raczej ścisła umowa , określona w dokumentacji Oracle

metoda hashCode().

Porozmawiajmy teraz o hashCode()metodzie. Dlaczego jest to konieczne? Dokładnie w tym samym celu — do porównywania obiektów. Ale już mamy equals()! Dlaczego inna metoda? Odpowiedź jest prosta: poprawić wydajność. Funkcja skrótu, reprezentowana w Javie przy użyciu hashCode()metody, zwraca wartość liczbową o stałej długości dla dowolnego obiektu. W Javie hashCode()metoda zwraca 32-bitową liczbę ( int) dla dowolnego obiektu. Porównywanie dwóch liczb jest znacznie szybsze niż porównywanie dwóch obiektów za pomocą equals()metody, zwłaszcza jeśli ta metoda uwzględnia wiele pól. Jeśli nasz program porównuje obiekty, jest to znacznie prostsze przy użyciu kodu skrótu. Tylko wtedy, gdy obiekty są równe w oparciu o hashCode()metodę, porównanie przechodzi doequals()metoda. Nawiasem mówiąc, tak działają struktury danych oparte na hashach, na przykład znajome HashMap! Metoda hashCode(), podobnie jak equals()metoda, jest nadpisywana przez programistę. I podobnie jak equals()metoda hashCode()ma oficjalne wymagania określone w dokumentacji Oracle:
  1. Jeśli dwa obiekty są równe (tzn. equals()metoda zwraca true), to muszą mieć ten sam kod skrótu.

    Inaczej nasze metody nie miałyby sensu. Jak wspomnieliśmy powyżej, hashCode()najpierw należy sprawdzić, aby poprawić wydajność. Gdyby kody skrótu były różne, sprawdzenie zwróciłoby fałsz, mimo że obiekty są w rzeczywistości równe zgodnie z tym, jak zdefiniowaliśmy metodę equals().

  2. Jeśli hashCode()metoda jest wywoływana kilka razy na tym samym obiekcie, musi za każdym razem zwracać ten sam numer.

  3. Reguła 1 nie działa w przeciwnym kierunku. Dwa różne obiekty mogą mieć ten sam kod skrótu.

Trzecia zasada jest nieco myląca. Jak to może być? Wyjaśnienie jest dość proste. Metoda hashCode()zwraca plik int. An intjest liczbą 32-bitową. Ma ograniczony zakres wartości: od -2 147 483 648 do +2 147 483 647. Innymi słowy, istnieje nieco ponad 4 miliardy możliwych wartości dla int. Teraz wyobraź sobie, że tworzysz program do przechowywania danych o wszystkich ludziach żyjących na Ziemi. Każda osoba będzie odpowiadała własnemu Personprzedmiotowi (podobnie jak w Manklasie). Na planecie żyje około 7,5 miliarda ludzi. Innymi słowy, bez względu na to, jak sprytny napiszemy algorytm konwersjiPersonobiektów na int, po prostu nie mamy wystarczającej liczby możliwych liczb. Mamy tylko 4,5 miliarda możliwych wartości typu int, ale ludzi jest o wiele więcej. Oznacza to, że bez względu na to, jak bardzo się staramy, niektóre osoby będą miały te same kody skrótu. Kiedy tak się dzieje (kody skrótu pokrywają się dla dwóch różnych obiektów), nazywamy to kolizją. Podczas przesłaniania hashCode()metody jednym z celów programisty jest zminimalizowanie potencjalnej liczby kolizji. Biorąc pod uwagę wszystkie te zasady, jak będzie hashCode()wyglądać metoda na Personzajęciach? Lubię to:

@Override
public int hashCode() {
   return dnaCode;
}
Zaskoczony? :) Jeśli spojrzysz na wymagania, zobaczysz, że spełniamy je wszystkie. Obiekty, dla których nasza equals()metoda zwróci wartość true, również będą równe zgodnie z hashCode(). Jeśli nasze dwa Personobiekty są równe equals(czyli mają to samo dnaCode), to nasza metoda zwraca tę samą liczbę. Rozważmy trudniejszy przykład. Załóżmy, że nasz program powinien wybierać luksusowe samochody dla kolekcjonerów samochodów. Kolekcjonerstwo może być złożonym hobby z wieloma osobliwościami. Konkretny samochód z 1963 roku może kosztować 100 razy więcej niż samochód z 1964 roku. Czerwony samochód z 1970 roku może kosztować 100 razy więcej niż niebieski samochód tej samej marki z tego samego roku. metody equals i hashCode: najlepsze praktyki — 4W naszym poprzednim przykładzie z Personklasą odrzuciliśmy większość pól (tj. cechy ludzkie) jako nieistotne i użyliśmy tylkodnaCodepole w porównaniach. Pracujemy teraz w bardzo specyficznej dziedzinie, w której nie ma nieistotnych szczegółów! Oto nasza LuxuryAutoklasa:

public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   // ...getters, setters, etc.
}
Teraz musimy wziąć pod uwagę wszystkie pola w naszych porównaniach. Każdy błąd może kosztować klienta setki tysięcy dolarów, więc lepiej byłoby być zbyt bezpiecznym:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
W naszej equals()metodzie nie zapomnieliśmy o wszystkich kontrolach, o których mówiliśmy wcześniej. Ale teraz porównujemy każde z trzech pól naszych obiektów. Do tego programu potrzebujemy absolutnej równości, tj. równości każdego pola. co z hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Pole modelw naszej klasie to String. Jest to wygodne, ponieważ Stringklasa już zastępuje hashCode()metodę. Obliczamy modelkod skrótu pola, a następnie dodajemy do niego sumę pozostałych dwóch pól liczbowych. Programiści Java mają prostą sztuczkę, której używają, aby zmniejszyć liczbę kolizji: podczas obliczania kodu skrótu pomnóż wynik pośredni przez nieparzystą liczbę pierwszą. Najczęściej używaną liczbą jest 29 lub 31. Nie będziemy teraz zagłębiać się w matematyczne subtelności, ale w przyszłości pamiętajmy, że mnożenie wyników pośrednich przez odpowiednio dużą liczbę nieparzystą pomaga „rozłożyć” wyniki funkcji haszującej i, w konsekwencji zmniejsz liczbę obiektów z tym samym kodem skrótu. Dla naszej hashCode()metody w LuxuryAuto wyglądałoby to tak:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Możesz przeczytać więcej o wszystkich zawiłościach tego mechanizmu w tym poście na StackOverflow , a także w książce Effective Java autorstwa Joshua Bloch. Na koniec jeszcze jedna ważna kwestia, o której warto wspomnieć. Za każdym razem, gdy zastępowaliśmy metodę equals()i hashCode(), wybieraliśmy pewne pola instancji, które są brane pod uwagę w tych metodach. Te metody uwzględniają te same pola. Ale czy możemy rozważyć różne dziedziny w equals()i hashCode()? Technicznie możemy. Ale to zły pomysł, a oto dlaczego:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Oto nasze equals()i hashCode()metody dla LuxuryAutoklasy. Metoda hashCode()pozostała niezmieniona, ale usunęliśmy modelpole z equals()metody. Model nie jest już cechą używaną, gdy equals()metoda porównuje dwa obiekty. Ale przy obliczaniu kodu skrótu to pole jest nadal brane pod uwagę. Co otrzymujemy w rezultacie? Stwórzmy dwa samochody i przekonajmy się!

public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
Błąd! Używając różnych pól dla metod equals()i hashCode(), naruszyliśmy umowy, które zostały dla nich ustanowione! Dwa obiekty, które są równe zgodnie z equals()metodą, muszą mieć ten sam kod skrótu. Otrzymaliśmy za nie różne wartości. Takie błędy mogą prowadzić do absolutnie niewiarygodnych konsekwencji, szczególnie podczas pracy z kolekcjami, które używają skrótu. W rezultacie, kiedy zastępujesz equals()i hashCode(), powinieneś wziąć pod uwagę te same pola. Ta lekcja była dość długa, ale dużo się dzisiaj nauczyłeś! :) Teraz czas wrócić do rozwiązywania zadań!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION