JavaRush /Blog Java /Random-PL /metody równości i hashCode: praktyka użycia

metody równości i hashCode: praktyka użycia

Opublikowano w grupie Random-PL
Cześć! Dzisiaj porozmawiamy o dwóch ważnych metodach w Javie - equals()i hashCode(). Nie jest to nasze pierwsze spotkanie z nimi: na początku kursu JavaRush był krótki wykład na ten temat equals()– przeczytaj, jeśli go zapomniałeś lub nie widziałeś wcześniej. Metody równają się &  hashCode: praktyka użytkowania - 1Na dzisiejszej lekcji omówimy szczegółowo te pojęcia – uwierz mi, jest o czym rozmawiać! A zanim przejdziemy do czegoś nowego, odświeżmy pamięć o tym, co już omówiliśmy :) Jak pamiętacie, zwykłe porównywanie dwóch obiektów za pomocą operatora „ ==” to zły pomysł, ponieważ „ ==” porównuje odniesienia. Oto nasz przykład z samochodami z niedawnego wykładu:
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
Wydawać by się mogło, że stworzyliśmy dwa identyczne obiekty tej klasy Car: wszystkie pola na obu maszynach są takie same, lecz wynik porównania jest w dalszym ciągu fałszywy. Znamy już powód: linki car1i car2prowadzą do różnych adresów w pamięci, więc nie są sobie równe. 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, ale ją nadpisujemy – w końcu metoda equals()jest zdefiniowana w klasie Object. Jednak w swojej zwykłej formie jest mało przydatny:
public boolean equals(Object obj) {
   return (this == obj);
}
W ten sposób zdefiniowano metodę equals()w klasie Object. To samo porównanie linków. Dlaczego został taki stworzony? No cóż, skąd twórcy języka mają wiedzieć, które obiekty w Twoim programie są uważane za równe, a które nie? :) To jest główna idea metody equals()- twórca klasy sam określa cechy, za pomocą których sprawdzana jest równość obiektów tej klasy. Robiąc to, zastępujesz metodę equals()w swojej klasie. Jeśli nie do końca rozumiesz znaczenie wyrażenia „sam definiujesz cechy”, spójrzmy na przykład. Oto prosta klasa osób - 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;
}

   // pobierające, ustawiające itp.
}
Załóżmy, że piszemy program, który musi ustalić, czy dwie osoby są spokrewnione przez bliźnięta, czy tylko sobowtóry. Mamy pięć cech: rozmiar nosa, kolor oczu, fryzura, obecność blizn i wyniki biologicznego testu DNA (dla uproszczenia - w postaci numeru kodowego). Jak myślisz, która z tych cech pozwoli naszemu programowi zidentyfikować krewnych bliźniaków? Metody równają się &  hashCode: praktyka użytkowania - 2Oczywiście gwarancję może dać tylko test biologiczny. Dwie osoby mogą mieć ten sam kolor oczu, fryzurę, nos, a nawet blizny – ludzi na świecie jest wiele i nie da się uniknąć zbiegów okoliczności. Potrzebujemy niezawodnego mechanizmu: dopiero wynik testu DNA pozwala wyciągnąć dokładne wnioski. Co to oznacza dla naszej metody equals()? Musimy to na nowo zdefiniować na zajęciach, Manbiorąc pod uwagę wymagania naszego programu. Metoda musi porównać pole int dnaCodedwóch obiektów i jeśli są one równe, to obiekty 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ś nam umknęło. W tym przypadku dla naszych obiektów zdefiniowaliśmy tylko jedno „istotne” pole, za pomocą którego ustalana jest ich równość - dnaCode. Teraz wyobraźmy sobie, że takich „znaczących” pól mielibyśmy nie 1, ale 50. A jeśli wszystkie 50 pól dwóch obiektów jest równych, to obiekty są równe. To również może się zdarzyć. Głównym problemem jest to, że obliczenie równości 50 pól jest procesem czasochłonnym i pochłaniającym zasoby. Teraz wyobraźmy sobie, że oprócz klasy Manmamy klasę Womanz dokładnie tymi samymi polami, co w Man. A jeśli inny programista korzysta z Twoich zajęć, to bez problemu może napisać w swoim programie coś takiego:
public static void main(String[] args) {

   Man man = new Man(........); //kilka parametrów w konstruktorze

   Woman woman = new Woman(.........);//ta sama grupa parametrów.

   System.out.println(man.equals(woman));
}
W tym przypadku nie ma sensu sprawdzać wartości pól: widzimy, że patrzymy na obiekty dwóch różnych klas, a one w zasadzie nie mogą być równe! Oznacza to, że musimy zastosować w metodzie kontrolę equals()— porównanie obiektów dwóch identycznych klas. 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 zapomnieliśmy o czymś jeszcze? Hmm... Powinniśmy przynajmniej sprawdzić, czy nie porównujemy obiektu ze sobą! Jeśli odniesienia A i B wskazują na ten sam adres w pamięci, to są to ten sam obiekt 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;
}
Ponadto nie zaszkodzi dodać sprawdzenie null: żaden obiekt nie może być równy null, w takim przypadku nie ma sensu przeprowadzać dodatkowych kontroli. Biorąc to wszystko pod uwagę, nasza metoda equals()klasowa Manbędzie 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;
}
Wykonujemy wszystkie wymienione powyżej kontrole wstępne. Jeśli okaże się, że:
  • porównujemy dwa obiekty tej samej klasy
  • to nie jest ten sam obiekt
  • z którym nie porównujemy naszego obiektunull
...następnie przechodzimy do porównania istotnych cech. W naszym przypadku pola dnaCodedwóch obiektów. Zastępując metodę equals(), należy przestrzegać następujących wymagań:
  1. Refleksyjność.

    Każdy przedmiot musi być equals()dla siebie.
    Ten wymóg już uwzględniliśmy. Nasza metoda stwierdza:

    if (this == o) return true;

  2. Symetria.

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

  3. Przechodniość.

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

  4. Trwałość.

    Wyniki pracy equals()powinny ulec zmianie dopiero wtedy, gdy ulegną zmianie zawarte w niej pola. Jeżeli dane dwóch obiektów nie uległy zmianie, wyniki sprawdzenia equals()powinny być zawsze takie same.

  5. Nierówność z null.

    Dla każdego obiektu sprawdzenie a.equals(null)musi zwrócić false.To
    nie jest tylko zestaw kilku „przydatnych rekomendacji”, ale ścisły kontrakt metod , opisany w dokumentacji Oracle

metoda hashCode().

Porozmawiajmy teraz o metodzie hashCode(). Dlaczego jest to potrzebne? Dokładnie w tym samym celu - porównywaniu obiektów. Ale my już to mamy equals()! Dlaczego inna metoda? Odpowiedź jest prosta: poprawić produktywność. Funkcja skrótu, reprezentowana przez metodę , w języku Java hashCode(), zwraca wartość liczbową o stałej długości dla dowolnego obiektu. W przypadku Javy metoda hashCode()zwraca 32-bitową liczbę typu int. Porównanie dwóch liczb ze sobą jest znacznie szybsze niż porównywanie dwóch obiektów metodą equals(), szczególnie jeśli wykorzystuje ona wiele pól. Jeśli nasz program będzie porównywał obiekty, znacznie łatwiej jest to zrobić za pomocą kodu skrótu i ​​dopiero w przypadku, gdy są one równe hashCode()- przejdź do porównania za pomocą equals(). Nawiasem mówiąc, tak działają struktury danych oparte na skrótach — na przykład te, które znasz HashMap! Metodę hashCode(), podobnie jak i equals(), nadpisuje sam programista. I podobnie jak dla equals(), metoda hashCode()ma oficjalne wymagania określone w dokumentacji Oracle:
  1. Jeśli dwa obiekty są równe (to znaczy, że metoda equals()zwraca wartość true), muszą mieć ten sam kod skrótu.

    W przeciwnym razie nasze metody będą pozbawione sensu. hashCode()Jak powiedzieliśmy, sprawdzenie przez , powinno być najważniejsze w celu poprawy wydajności. Jeśli kody mieszające są różne, sprawdzenie zwróci wartość false, nawet jeśli obiekty są w rzeczywistości równe (jak zdefiniowaliśmy w metodzie equals()).

  2. Jeśli metoda hashCode()jest wywoływana wielokrotnie na tym samym obiekcie, powinna za każdym razem zwracać tę samą liczbę.

  3. Zasada 1 nie działa w odwrotną stronę. 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 int. intjest liczbą 32-bitową. Ma ograniczoną liczbę wartości - od -2 147 483 648 do +2 147 483 647. Innymi słowy, istnieje nieco ponad 4 miliardy odmian tej liczby int. Teraz wyobraź sobie, że tworzysz program przechowujący dane o wszystkich żyjących ludziach na Ziemi. Każda osoba będzie miała swój własny obiekt klasy Man. Na Ziemi żyje około 7,5 miliarda ludzi. ManInnymi słowy, niezależnie od tego, jak dobry napiszemy algorytm konwersji obiektów na liczby, po prostu nie będziemy mieli wystarczającej liczby liczb. Mamy tylko 4,5 miliarda opcji i znacznie więcej ludzi. Oznacza to, że niezależnie od tego, jak bardzo się staramy, kody skrótu będą takie same dla różnych osób. Ta sytuacja (pasujące kody skrótu dwóch różnych obiektów) nazywana jest kolizją. Jednym z celów programisty podczas zastępowania metody hashCode()jest maksymalne zmniejszenie potencjalnej liczby kolizji. Jak będzie wyglądać nasza metoda hashCode()dla zajęć Man, biorąc pod uwagę wszystkie te zasady? Lubię to:
@Override
public int hashCode() {
   return dnaCode;
}
Zaskoczony? :) Nieoczekiwanie, ale jeśli spojrzysz na wymagania, zobaczysz, że spełniamy wszystko. Obiekty, dla których nasza equals()zwraca wartość true, będą równe w hashCode(). Jeśli nasze dwa obiekty Manmają taką samą wartość equals(to znaczy mają tę samą wartość dnaCode), nasza metoda zwróci tę samą liczbę. Spójrzmy na bardziej skomplikowany przykład. Załóżmy, że nasz program powinien wybierać luksusowe samochody dla klientów kolekcjonerskich. Kolekcjonerstwo to skomplikowana sprawa i ma wiele cech. Samochód z 1963 roku może kosztować 100 razy więcej niż ten sam 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 równają się &  hashCode: praktyka użytkowania - 4W pierwszym przypadku, w przypadku klasy Man, odrzuciliśmy większość pól (tj. cech osoby) jako nieistotne i wykorzystaliśmy jedynie pole do porównania dnaCode. Tutaj pracujemy z bardzo wyjątkowym obszarem i nie może być drobnych szczegółów! Oto nasza klasa LuxuryAuto:
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;
   }

   //... pobierające, ustawiające itp.
}
Tutaj przy porównywaniu musimy wziąć pod uwagę wszystkie pola. Każdy błąd może kosztować klienta setki tysięcy dolarów, dlatego lepiej się zabezpieczyć:
@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 metodzie equals()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. W tym programie równość musi być absolutna w każdej dziedzinie. Co powiesz na hashCode?
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Pole modelw naszej klasie jest ciągiem znaków. Jest to wygodne: Stringmetoda hashCode()została już nadpisana w klasie. Obliczamy kod skrótu pola modeli dodajemy do niego sumę pozostałych dwóch pól numerycznych. W Javie istnieje mała sztuczka, która pozwala zmniejszyć liczbę kolizji: podczas obliczania kodu skrótu należy pomnożyć wynik pośredni przez nieparzystą liczbę pierwszą. Najczęściej używaną liczbą jest 29 lub 31. Nie będziemy teraz wdawać się w szczegóły matematyczne, ale na przyszłość pamiętaj, że pomnożenie wyników pośrednich przez wystarczająco dużą liczbę nieparzystą pomaga „rozłożyć” wyniki skrótu funkcji i skończyć z mniejszą liczbą obiektów z tym samym kodem skrótu. Dla naszej metody hashCode()w LuxuryAuto będzie to wyglądać następująco:
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Więcej o wszystkich zawiłościach tego mechanizmu możesz przeczytać w tym poście na StackOverflow , a także w książce Joshuy Blocha „ Efektywna Java ”. Na koniec warto wspomnieć o jeszcze jednej ważnej kwestii. equals()Za każdym razem przy nadpisywaniu hashCode()zaznaczaliśmy określone pola obiektu, które były uwzględniane w tych metodach. Ale czy możemy wziąć pod uwagę różne pola 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 metody equals()dla hashCode()klasy LuxuryAuto. Metoda hashCode()pozostała niezmieniona, a equals()my usunęliśmy pole z metody model. Teraz model nie jest cechą do porównywania dwóch obiektów według equals(). Ale nadal jest to brane pod uwagę przy obliczaniu kodu skrótu. Co w rezultacie otrzymamy? Stwórzmy dwa samochody i sprawdźmy!
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(„Czy te dwa przedmioty są sobie równe?”);
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println(„Jakie są ich kody skrótu?”);
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Эти два obiektа равны друг другу?
true
Какие у них хэш-kodы?
-1372326051
1668702472
Błąd! Używając różnych pól dla nich equals(), hashCode()naruszyliśmy ustanowioną dla nich umowę! Dwa równe equals()obiekty muszą mieć ten sam kod skrótu. Mamy dla nich różne znaczenia. Takie błędy mogą prowadzić do najbardziej niewiarygodnych konsekwencji, szczególnie podczas pracy z kolekcjami korzystającymi z skrótów. Dlatego przy przedefiniowaniu equals()i hashCode()prawidłowe będzie użycie tych samych pól. Wykład okazał się dość długi, ale dzisiaj nauczyliście się wielu nowych rzeczy! :) Czas wrócić do rozwiązywania problemów!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION