Kontrakt hashCode i equals

Jedną z pierwszych rzeczy, o których się dowiadujemy ucząc się Javy są informacje na temat metod equals oraz hashCode. Informacje na temat kontraktu tych dwóch metod są bardzo istotne w programowaniu w Javie, szczególnie w przypadku kolekcji, ale o tym będzie kiedy indziej.

Kontrakt

Metody equals oraz hashCode powinny spełniać poniższe warunki:

  • Jeżeli x == y to x.equals(y) == true
  • Jeżeli x.equals(y) == true to x.hashCode() == y.hashCode()
  • Jeżeli x.hashCode() == y.hashCode() to x.equals(y) może zarówno zwrócić true, jak i może zwrócić false

Wszystkie wartości zwracane przez powyższe funkcje muszą być determistyczne – zawsze zwracać te same wartości dla tych samym parametrów.

Domyślna implementacja

Metody hashCode i equals są zdefiniowane w Object, czyli w klasie nadrzędnej dla wszystkich klas w Java. Domyślne implementacje wyglądają następująco:

Domyślna implementacja metody equals opiera się na sprawdzeniu czy przekazany obiekt jest tą samą instancją, co obiekt, na którym wywołano metodę equals. Jak widać domyślna implementacja w żaden sposób nie opiera się na atrybutach klasy.

Klasa Object nie ma natomiast zdefiniowanego ciała metody hashCode. Każda implementacja JVM dostarcza swoją wersje tej metody. Zazwyczaj jest to adres pamięci, pod którym znajduje się obiekt, zamieniony na typ integer. Podejście takie byłoby prawidłowe, gdyby każdy obiekt był unikalny. W przypadku kiedy dwa obiekty są identyczne (bazując na atrybutach klasy) implementacja taka nie spełnia kontraktu tej metody.

Przykładowe implementacje

Oczywiście powyższe metody możemy (a często wręcz musimy) nadpisać i dostosować do naszych potrzeb. Załóżmy, że mamy poniższą klasę, do której chcemy dopisać nasze dwie metody, o których rozmawiamy.

Przykładowe implementacja metody hashCode mogą wyglądać następująco:

Jak widać wartość hashCode jest wyliczana przy użyciu mnożenia i dodawanie na podstawie wszystkich pól klasy.

Implementacja equals dla naszej klasy może wyglądać tak:

Tutaj już się dzieję trochę więcej… albo przynajmniej wygląda jakby się działo więcej.
W liniach 3-11 sprawdzamy identyczność obiektów na poziomie samego obiektu, a nie jego atrybutów. Sprawdzamy czy:

  • przekazany obiekt jest tym samym obiektem, na którym została wywołana metoda – jeżeli jest to oczywiście zwracamy true
  • przekazany obiekt nie jest nullem – jeżeli jest to zwracamy false – obiekt, na ktorym wywołaliśmy metodę z oczywistych powodów nie może być nullem 🙂
  • oba obiekty są tego samego typu – jeżeli nie są to oczywiście zwracamy false

Jeżeli obiekt przejdzie te 3 warunki to jest rzutowany na nasz typ klasy, a następnie każde pole klasy jest porównywane. Jeżeli, któreś z porównywanych pól jest inne to oczywiście zwracamy false, a w przeciwnym przypadku przechodzimy dalej. Kiedy już wszystkie pola zostały sprawdzony to zwracamy true.

Bardziej praktyczna implementacja

Implementacje powyższych metod nie są trudne, ale zajmują trochę linii kodu (szczególnie equals). Dodatkowo implementując ręcznie te metody dla klas z większą ilością pół łatwo o pomyłke. Co prawda każde IDE potrafi wygenerować implementacje powyższych metod na podstawie pól klasy, ale wciąż zostaje problem pierwszy – dużo linii i brzydko to wygląda 😉

Bardzo polecam używanie bibliotek, które zawierają wbudowany builder’y do tych metod. Jedną z takich bibliotek jest common-lang od Apache.
Implementacja hashCode przy użyciu tej metody wygląda następująco:

Jak widać kod dla tej metody został zdecydowanie uproszczony i jest bardziej przejrzysty.

Implementacja metody equals może wyglądać na przykład tak:

Jak widać kod w tym przypadku również się trochę uprościł. Trzy pierwsze if’y co prawda zostały, ale reszta kodu została zdecydowanie skrócona i jest bardziej czytelna.

Jak widać dzięki użyciu kodu z biblioteki mogliśmy stworzyć nasze metody pisząc mniej kodu, który jest jednak bardziej czytelny i przejrzysty.

A może by tak jeszcze coś ulepszyć?

Patrząc na implementacje metody equals można by się zastanowić czy użycie instanceof zamiast drugiego i trzeciego if’a nie byłoby dobrym pomysłem. Wtedy nasza klasa mogłaby wyglądać w następujący sposób:

Dzięki takiemu podejściu ‚oszczędzamy’ jednego if’a, a wszystko działa jak działało. Chociaż…

Spójrzmy na poniższą klase:

Stworzyliśmy klasę Student, która dziedziczy po Person. Nasza nowa klasa zawiera dodatkowe pole ‚school’.

Zróbmy teraz coś takiego:

Jak widać dochodzimy do sytuacji, gdzie tworzymy dwa różne obiekty i w zależności od tego, który do którego porównamy to otrzymujemy różne wyniki. Równość powinna być relacją symetryczną, a taka sytuacja zdecydowanie ją narusza.

Czy to oznacza, że to podejście jest błędne? Odpowiedź jest bardzo prosta i brzmi: to zależy 🙂

Wersja equals z getClass jest, powiedzmy, ‚restrykcyjna’, ale za to prosta i intuicyjna. Porównywany obiekt musi być dokładnie tego samego typu co obiekt, do którego jest porównywany.

Druga wersje może narobić nam trochę ambarasu, lecz zostawia trochę więcej swobody. Istotnym przypadkiem, kiedy takie podejście może być konieczne jest porównywanie obiektów, które są zarządzane przez jakiś framework np. część implementacji JPA opakowuje encje. W takiej sytuacji, dzięki użyciu instanceof w metodzie equals jesteśmy w stanie w poprawny sposób porównywać takie obiekty.

Jak widać obydwa sposoby implementacji metody equals mają swoje zastosowania. Najważniejsze, żeby świadomie wybierać podejście, którego potrzebujemy 🙂