hashCode i equals – co grozi Ci za złamanie kontraktu między nimi?

hashCode i equals

Metody hashCode i equals to jedne z podstawowych metod wykorzystywanych w Javie. Ich deklaracja znajduje się już w klasie Object. Mimo iż obie metody posiadają domyślną implementację, to wykorzystanie jej lub próba napisania własnej, może przysporzyć programistom nie lada problemów.

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

Porównywanie typów prostych w Javie

Do porównywania typów prostych, takich jak liczby całkowite, liczby zmiennoprzecinkowe czy wartości boolean przeznaczony jest standardowy operator porównania ==.

1 == 1

1.5 == 2.5

true == false

Porównywanie obiektów w Javie

W przypadku obiektów sprawa nie jest już tak prosta, jak w przypadku typów prostych. Operator porównania może okazać się zwodniczy.

Operator porównania ==

Java udostępnia standardowy operator porównania ==, który sprawdza, czy obiekty są „takie same – identyczne”. Co w praktyce oznacza, że muszą znajdować się dokładnie w tym samym miejscu w pamięci komputera. Jednak nawet jeżeli obiekty będą miały taką samą wartość, np. liczba 1 (typ Integer), to mogą znajdować się w innym miejscu w pamięci.

Metoda equals

Kolejny sposób porównywania obiektów to metoda equals, która w zamyśle wykorzystana jest do porównania obiektów wg ich wartości. Jednak jej ostateczne zachowanie zależne jest od implementacji. Przykładowo, domyślna implementacja to standardowy operator porównania, sprawdzający identyczność obiektów.

Klasy typów opakowujących (np. Integer, Long itp.) oraz String nadpisują implementację metody equals, w celu porównywania obiektów po wartości.

Dla własnych klas taką implementację należy napisać samemu.

Metoda hashCode

Metoda hashCode powstała w celach optymalizacyjnych. Jej zadaniem jest wygenerowanie dla obiektu szybkiego skrótu (hashu) w formie liczby całkowitej.

Hash generowany jest na podstawie stanu obiektu – wartości pól obiektu, które wyróżniają go od innych obiektów tej samej klasy. Powstały kod powinien zapewniać jak największą unikatowość – tak by możliwie niewielka liczba obiektów mogła wygenerować identyczny kod.

HashCode jest wykorzystywana przez wbudowane w Javie struktury danych (np. HashMap). Najpierw obiekty są dzielone na tak zwane kubełki, wg wartości kodu hashCode. A dopiero następnie są między sobą porównywane z wykorzystaniem metody equals. Dzięki temu nie ma potrzeby porównywania ze sobą wszystkich obiektów.

Kontrakt hashCode i equals

Kontrakt hashCode i equals

Kontrakt między metodami hashCode() i equals()

O tych metodach zazwyczaj mówi się jednocześnie, ponieważ został zdefiniowany między nimi kontrakt. Jego wypełnienie chroni programistę przed trudnymi do zdiagnozowania błędami.

  1. Każde wywołanie metody hashCode na tym samym obiekcie musi kończyć się zwróceniem tej samej liczy całkowitej.
  2. Jeżeli dwa obiekty są sobie równe (wg metody equals), to ich hashCode również musi być równy.
  3. Jeżeli obiekty są różne (wg metody equals), to ich hashCode może być równy, jednak ze względów wydajnościowych powinno to być unikane.
  4. Relacja wyznaczona metodą equals musi być zwrotna, czyli dla każdej zmiennej x różnej od null wyrażenie x.equals(x) musi zwracać wartość = true.
  5. Relacja wyznaczona metodą equals musi być symetryczna, czyli dla każdej pary zmiennych x i y, wyrażenie x.equals(y) ma wartość true i wtedy i tylko wtedy gdy y.equals(x) = true.
  6. Relacja wyznaczona metodą equals musi być przechodnia, czyli dla dowolnych zmiennych z, y i z, jeżeli x.equals(y) = true oraz y.equals(z) = true, to x.equals(z) musi również = true.
  7. Relacja wyznaczona metodą equals musi być spójna, czyli każdorazowe wywołanie x.equals(y) (przy założeniu, że między wywołaniami obiekty nie były modyfikowane) zawsze musi zwracać tę samą wartość – zawsze true albo zawsze false.
  8. Każdy obiekt jest różny od null, czyli wywołanie x.equals(null) dla obiektu x różnego od null, zawsze musi zwrócić false.

Potencjalne konsekwencje niezachowania kontraktu

Brak zachowania kontraktu niestety nie powoduje błędów kompilacji. Może jednak przyczynić się do powstania trudnych do wyśledzenia błędów podczas działania aplikacji.

Jednym z częściej spotykanych problemów jest możliwość wielokrotnego dodania takich samych obiektów do zbioru danych (Set) lub „gubienie” się tych obiektów w secie. Może to przyczynić się do utraty wielu godzin na debugowaniu takiego błędu.

Implementacja metod hashCode i equals

Bardzo często wystarczy automatyczna implementacja wygenerowana przez IDE, Lombok’a, AutoValue lub inne podobne narzędzia. Tylko w bardzo specyficznych sytuacjach zachodzi potrzeba implementowania tych metod ręcznie.

We własnej implementacji warto zwrócić uwagę między innymi, by zmienne oznaczane jako transient lub których stan jest wyliczany wlocie, nie były porównywane. Tego typu pola, np. po deserializacji obiektu mogą mieć nieaktualne wartości.

Więcej na temat problemów z implementacją i utrzymaniem tych metod można przeczytać w artykule o boilerplate code.

Testy hashCode i equals

Testy hashCode i equals

Jak testować hashCode i equals?

Bardzo często pojawia się pytanie o sensowność testowania implementacji hashCode i equals. Prawda jest jednak taka, że te metody również zawierają logikę. Rezygnacja z ich testowania może przyczynić się do powstania różnego rodzaju błędów, tak samo, jak w przypadku innych metod.

Sytuacja nie jest jednak taka zła, na jaką wygląda. Ponieważ  większość implementacji tych metod jest do siebie zbliżona, można z testów wyciągnąć część wspólną i je również, przynajmniej częściowo, zautomatyzować.

Poniżej przedstawiam listę przykładowych możliwości testowania metod hashCode oraz equals.

1. Własna klasa testująca

W ramach testów przygotowałem klasę testującą kontrakt hashCode i equals: ManualEqualsTester oraz jej przykładowe wykorzystanie w teście UserManualTest.

 

Warto zrobić coś takiego w celu poznania warunków, jakie muszą spełniać te metody. Jednak w praktyce spokojnie swoje testy można opierać o gotowe rozwiązania, takie jak EqualsVerifier czy Guava, które są opisane w kolejnych punktach.

2. EqualsVerifier

EqualsVerifier to niewielka, ale bardzo pomocna przy testowaniu implementacji equals i hashCode biblioteka. Kod źródłowy oraz szczegółową dokumentację można znaleźć na stronie projektu.

Jeżeli metoda equals jest napisana z wykorzystaniem getClass() zamiast instanceof, należy w wywołaniu dodać usingGetClass(), w przeciwnym wypadku jest to zbędne.

3. Guava Testing Library

Guava w swoim arsenale również posiada klasę EqualsTester, która może być bardzo pomocna przy automatycznym testowaniu poprawności metod equals i hashCode.

Biblioteka guava-testlib dostępna jest na repozytorium maven.

Podsumowanie

Metody hashCode i equals są na tyle powszechne w Javie, że prędzej czy później każdy programista Java będzie musiał się z nimi zmierzyć. Warto oczywiście znać wszystkie warunki, jakie te metody muszą spełniać, jednak o ile to możliwe, warto unikać pisania ich ręcznie. Ich implementację lepiej powierzyć gotowym bibliotekom lub generatorom kodu. Dzięki czemu zaoszczędzimy nie tylko na samym czasie implementacji, ale również unikniemy konieczności ich testowania i ewentualnych błędów.

Programista – Pytania rekrutacyjne

Lista pytań rekrutacyjnych, które pozwolą przygotować Ci się na rozmowę kwalifikacyjną.

No comments
Share:

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *