java typy generyczne
Typy generyczne w języku Java
26 marca 2016
kolekcje w języku Java
Kolekcje w języku Java
9 sierpnia 2016
Equals i hashCode w języku Java

Equals i hashCode w języku Java

Cześć! W dzisiejszym artykule będziesz mógł przeczytać o właściwym sposobie porównywania obiektów i typów prostych w języku Java. Dowiesz się do czego służą metody equals oraz hashCode oraz przeczytasz o tak zwanym kontrakcie między tymi metodami. Na koniec będzie na Ciebie czekało małe ćwiczenie do wykonania samodzielnie. Zapraszam do artykułu.

To jest jeden z artykułów w ramach darmowego kursu programowania w Javie. Proszę zapoznaj się z pozostałymi częściami, mogą one być pomocne w zrozumieniu materiału z tego artykułu.

Porównywanie typów prostych

Do sprawdzenia „równości” typów prostych służą operatory == oraz !=. Dzięki nim możemy porównać ze sobą każdą zmienną typu prostego. Wynikiem takiego porównania jest wartość typu booleantrue jeśli porównywane obiekty są równe i false w przeciwnym wypadku. Proszę spójrz na przykład poniżej:

Kolejne linijki porównują odpowiednio:

  • liczby typu int,
  • zmienne typu boolean,
  • znaki typu char,
  • liczby typu long.

Priorytety operatorów

Drobna dygresja dotycząca priorytetów operatorów. W języku Java wszystkie operatory mają tak zwany priorytet. Oznacza to tyle, że priorytet operatorów określa kolejność wykonywania działań. Proszę spójrz na przykład niżej

Operator + ma wyższy priorytet niż operator ==. W związku z tym operacja dodawania wykonana zostanie jako pierwsza i porównanie zwróci true. Podobnie * ma wyższy priorytet niż +. Zatem na początku wykonana zostanie operacja mnożenia a na końcu dodawanie.

Czasami jednak domyślny priorytet operatorów nie jest odpowiedni, chcielibyśmy wykonać operacje w innej kolejności. Z pomocą przychodzą nawiasy, które pozwalają na modyfikację zachowania programu. Przykład poniżej pomaga zrozumieć jak to działa:

Mimo tego, że operator * ma wyższy priorytet niż + operacja mnożenia zostanie wykonana jako druga. Pierwsze zostanie wykonane dodawanie ponieważ zostało otoczone parą nawiasów.

Po tym wstępie mogę wyjaśnić dlaczego w przykładzie poniżej potrzebujemy nawiasów:

Bez dodatkowej pary nawiasów pierwszeństwo miałby operator +. "10 == 10: " + 10 == 10 inaczej możemy zapisać jako "10 == 10: 10"  == 10 a taki zapis nie jest poprawny, ponieważ operatorem == nie możemy porównać instancji typu String i wartości typu int.

Porównywanie zmiennoprzecinkowych typów prostych

O ile sprawdzanie równości wartości liczb całkowitych nie jest trudne to ta sama operacja dla typów zmiennoprzecinkowych jest trochę bardziej skomplikowana. W związku ze sposobem reprezentacji liczb zmiennoprzecinkowych typu float i double w pamięci komputera nie jest możliwe ich dokładne porównywanie. Operacja taka jest dopuszczalna ale może prowadzić do dziwnych rezultatów (na przykład, liczby, które teoretycznie powinny być równe według komputera nie są).

W związku z tym liczby zmiennoprzecinkowe powinno się porównywać z pewną dokładnością.

W przykładzie powyżej została użyta metoda Math.abs(). Metoda ta zwraca wartość bezwzględną danej liczby. Następnie wartość tą porównujemy z bardzo małą liczbą. Liczba ta reprezentuje dokładność porównania. Jeśli różnica liczb jest mniejsza niż nasza założona dokładność uznajemy, że porównywane liczby są równe.

Porównanie obiektów

Używając operatora == do porównywania obiektów uzyskamy błędne rezultaty. Do porównania tego typu powinniśmy używać metody equals.

Dlaczego tak się dzieje? Otóż w przypadku obiektów operator == porównuje referencje obiektów (adresy na stercie). Mając dwie różne instancje obiektów mają one dwa różne adresy w pamięci w związku z tym zawsze ich adresy są różne. Innymi słowy w przypadku obiektów przy pomocy operatora == możemy sprawdzić czy dwie referencje wskazują na ten sam obiekt.

Metoda equals

Metoda equals jest jedną z metod dostępnych w klasie Object. W związku z tym, że każdy obiekt w języku Java ma tą klasę w swojej hierarchii dziedziczenia możemy tą metodą wywołać na każdym obiekcie.

W większości przypadków domyślna implementacja metody equals nie jest odpowiednia1 w związku z tym programista tworzący nowy obiekt musi tą metodę zaimplementować jeśli chce sprawdzać czy instancje tej klasy są równe.

Istnieje zestaw wytycznych, które metoda equals powinna spełniać aby była poprawnie zaimplementowana. Opiszę je po kolei:

Metoda equals powinna być zwrotna

Oznacza to tyle, że dla każdego obiektu operacja object.equals(object) powinna zwrócić true.

Metoda equals powinna być symetryczna

Oznacza to tyle, że dla każdej pary obiektów X i Y powinna zachodzić właściwość jeśli X.equals(Y) == true wówczas także Y.equals(X) == true.

Metoda equals powinna być przechodnia

Jeśli mamy trzy obiekty X, Y i Z oraz jeśli X.equals(Y) == true i Y.equals(Z) == true to także X.equals(Z) jest prawdą.

Metoda equals powinna być spójna

Innymi słowy kilkukrotne wywołanie metody equals na tych samych obiektach zawsze powinno zwrócić ten sam wynik (zakładając, że obiekty nie były modyfikowane pomiędzy wywołaniami).

Metoda equals powinna zwrócić false przy porówaniu z null

Dla każdego obiektu X, który nie jest null porównanie typu X.equals(null) powinno zwrócić false.
Przykład implementacji metody equals

Załóżmy, że mamy klasę Chair. Możemy powiedzieć, że krzesła są „równe” jeśli zostały wyprodukowane w tym samym roku, przez tego samego producenta oraz są tego samego modelu. Założenia te zostały zaimplementowane poniżej.

Zauważ, że w naszej implementacji metody equals używamy także metody equals z typu String aby sprawdzić czy model i producent są równi.

Nowy może być także operator instanceof, służy on do sprawdzenia czy dana instancja jest typu Chair. Po tym sprawdzeniu możemy bezpiecznie rzutować obiekt obj i mamy pewność, że nie zostanie rzucony wyjątek ClassCastException.

Metoda hashCode

Podobnie jak w przypadku equals hashCode jest zaimplementowane w klasie Object. Zawsze kiedy programista implementuję metodęhashCode powinien też zaimplementować metodęequals.

Metoda ta zwraca liczbę typu int, która służy do przyporządkowania danego obiektu do grupy. Dzięki metodzie hashCode jesteśmy w stanie podzielić wszystkie możliwe instancje danej klasy na rozdzielne grupy. Każda z tych grup reprezentowana jest przez liczbę zwracaną przez metodę hashCode.

hashCode zasada działania

hashCode zasada działania

Obrazowe przyporządkowanie obiektów do grup zostało przedstawione na diagramie powyżej. Koła i trójkąt zostały przyporządkowane do tej samej grupy, rąb i pięciokąt do grupy Hash#2 natomiast trapez został przyporządkowany do grupy Hash#3.

Metoda hashCode wykorzystywana jest przez niektóre kolekcje (tablice na sterydach), o których przeczytasz w jednym z kolejnych artykułów. Implementacja metody hashCode sprowadza się do zwrócenia odpowiedniej liczby, tak zwanego hasha. Przyporządkuje on dany obiekt do grupy używanej w niektórych kolekcjach. Najczęściej metodę hashCode implementuje się w oparciu o hashe atrybutów danej instancji. Hashe atrybutów zazwyczaj mnoży się przez liczby pierwsze i sumuje ze sobą. Użycie liczb pierwszych pomaga w uzyskaniu „dobrych hashy”. Dobra implementacja hashCode pozwala na uzyskanie jak największej liczby grup (hashy), do których przyporządkowujemy obiekty.

Posłużę sie tu klasą Chair wspomianą wyżej. Zakładając, że nasza klasa ma trzy atrybuty i żaden z nich nie może mieć wartości null przykładowa implemetacja może wyglądać następująco:

W większości przypadków użycie metody Objects.hash przy implementacji metody hashCode jest dobrym pomysłem.

Kontrakt między metodami equals i hashCode

Metody hashCode i equals są ze sobą powiązane i ich implementacja powinna być spójna. Tą zależność określa się kontraktem między metodami hashCode i equals.

* Jeśli X.equals(Y) == true wówczas wymagane jest aby X.hashCode() == Y.hashCode(),
* Kilkukrotne wywołanie metody hashCode na tym samym obiekcie, który nie był modyfikowany pomiędzy wywołaniami musi zwrócić tą samą wartość,
* Jeśli X.hashCode() == Y.hashCode() to nie jest wymagane aby X.equals(Y) == true.

Trzeci przypadek jest ilustrowany na obrazku powyżej gdzie koła i trójkąt mają ten sam hashCode jednak koło i trójkąt nie są równe.

Generatory metod hashCode i equals

Implementacja tych metod w większości przypadków jest dość prosta. W większości z nich także nie jest to kod zbyt skomplikowany. Jednak za każdym razem pisanie tych metod jest uciążliwe. Z pomocą przychodzi IDE. Polecam generowanie tych metod przy jego pomocy. W przypadku InteliJ IDEA pomocny może okazać się skrót klawiaturowy Alt+Insert. Po jego naciśnięciu pokaże się menu kontekstowe pozwalające na wygenerowanie tych metod.

Dodatkowo warto przyjrzeć się klasie Objects i bibliotekom Guava czy Apache commons-lang. Zawierają one metody pomocnicze użyteczne podczas implementacji metod hashCode i equals.

Zadanie

Na koniec krótkie zadanie dla Ciebie. Napisz klasę reprezentującą człowieka, zaimplementuj metody hashCode i equals. Zastanów się czy to, że ktoś ma to samo imię i nazwisko sprawia, że jest to ta sama osoba? Jaki atrybut może posłużyć do sprawdzenia czy dwie instancje klasy Human reprezentują tę samą osobę?

Jeśli będziesz miał problem z rozwiązaniem zadania przykładowe rozwiązanie umieściłem na githubie. Jak zwykle zachęcam do samodzielnych prób, wtedy nauczysz się najwięcej.

Materiały dodatkowe

Kod źródłowy wszystkich przykładów użytych w artykule znajduje się na githubie. Jeśli chcesz poczytać więcej na temat metod equals i hashCode zapraszam do materiałów dodatkowych:

Podsumowanie

Bardzo się cieszę, że dotarłeś do końca artykułu. Mam nadzieję, że był on dla Ciebie ciekawy i przydatny. Na koniec mam do Ciebie prośbę, podziel się artykułem ze swoimi znajomymi, zależy mi na dotarciu do jak największej liczby czytelników. Jeśli nie chcesz przegapić nowych artykułów polub nas na facebooku 🙂 W przypadku jakichkolwiek pytań proszę zdaj je w komentarzach, postaram się odpowiedzieć.

Do następnego razu!

[FM_form id=”3″]

Zdjęcie dzięki uprzejmości https://www.flickr.com/photos/badgreeb_records/6453502559

  1. Domyślna implementacja zachowuje się jak operator ==, porównuje adresy obiektów.
  • lukasz

    A czy są może odpowiedzi do zadań?

    • Marcin Pietraszek

      Cześć Łukasz!

      Do tej pory nie było, właśnie je wrzuciłem, jeśli będziesz miał jakiekolwiek pytania to wal śmiało 😉

  • Łukasz

    Czesc. Mnie tak zastanawia czemu jest
    return super.equals(obj);
    Jesli wstawimy this to program też działa.

  • Łukasz

    Już chyba wiem.. w tamtym miejscu musimy obojetnie co zwrocic true lub false

    • Marcin Pietraszek

      To bardzo mocno zależy od kontekstu, w którym taką linijkę wstawisz. Jeśli dany obiekt nie ma danej metody (np. equals) i jest ona zdefiniowana w nadklasie (np. w klasie Object) wówczas this.equals() i super.equals() odnoszą sie do tej samej metody.

      Proszę pokaż większy fragment kodu, wtedy będę mógł coś więcej powiedzieć (możesz do tego użyć https://gist.github.com).

  • Łukasz

    Chodziło mi o super z Twojego przykładowego kodu z klasą Chair na tej stronie.

    • Marcin Pietraszek

      To nie jest do końca prawda, że zamiana super na this nic nie zmienia. Zastanów się co stałoby się przy wywołaniu takiego kodu jeśli w metodzie equals mielibyśmy this a nie super

      Takie wywołąnie skończyłoby się rzuceniem wyjątku java.lang.StackOverflowError gdyż metoda Chair.equals wywoływałaby samą siebie do momentu przepełnienia stosu.

      Uprościłem kod w przykładzie żeby rozwiać takie wątpliwości, dzięki za uwagę, nowa implementacja jest lepsza 😉

  • Barti

    A właściwie to dlaczego nadpisujesz metodę HashCode w obiekcie Human? Przecież nadpisanie metody equals wystarczy, żeby porównać osobę (imię, nazwisko i pesel).

    • Marcin Pietraszek

      Bez nadpisania metody hashCode złanany byłby kontrakt między hashCode i equals. Kontrakt ten opisany jest w jednym z ostatnich akapitów.

      Kontrakt ten jest istotny na przykład jeśli używamy obiektu razem z kolekcjami. Jeśli kontrakt byłby złamany moglibyśmy dodać do zbioru HashSet dwa obiekty, które byłyby sobie równe (według implementacji equals).

  • MarcinJC

    Marcinie rozwiń proszę metodę hashCode. Metoda equals jest dosyć „oczywista” natomiast hashCode to zupełnie inna „para kaloszy”. Przydałby się przykład implementacji. Zadanie co prawda jest, ale bez przykładu musiałem zerknąć do githuba, żeby podejrzeć rozwiązanie, co mi się przy poprzednich częściach kursu nie zdarzało!

    • Hej MarcinJC! Przykład implementacji hashCode był w akapicie wyżej 🙂 Powtórzyłem to także w tym dotyczącym samego hashCode. Rozbudowałem też sam artykuł dodają sekcję o generatorach, których używa się bardzo często w wielu projektach.

      • MarcinJC

        Z tym brakiem implementacji hashCode() to miałem chyba „pomroczność jasną” :-). Przejrzałem parę przykładów funkcji hashCode() i doszedłem do takich samych wniosków, jak te które dopisałeś w akapicie o w/w metodzie czyli użycie liczby pierwszej i skorzystanie z metody hashCode dla każdego z pól klasy. Najprościej, tak jak piszesz, skorzystać z generatora, ale moim zdaniem warto rozumieć na czym ta metoda polega, gdyż, jak doczytałem jest istotna z punktu widzenia Javy. Niemniej jednak dziękuję za odpowiedź i uzupełnienie artykułu. Myślę, że następnym użytkownikom Twojego świetnego kursu będzie łatwiej w tej kwestii.