Po co komu hashCode i equals?

Ostatnio wpis był o tworzeniu klas i obiektów w Javie. Tak jak obiecałem teraz będzie o hashCode i equals. Czym są wymienione? Otóż hashCode() jest metodą, która zwraca nam wartość typu int, która identyfikuje liczbowo obiekt. Domyślnie dla każdego obiektu będzie to inna liczba, wytworzona na bazie adresu obiektu w pamięci. Metoda equals() natomiast pozwala na porównywanie obiektów. Dodatkowo obydwie metody są niejako ze sobą związane i jeżeli decydujemy się nadpisać ich domyślne implementacje, to powinniśmy nadpisać obydwie.

equals()

Metoda equals() jak wspomniane powyżej pozwala porównywać obiekty. Obiektów w Javie nie porównujemy poprzez podwójny znak równości “==”. Takie wyrażenie nie zwróci błędu, ponieważ jest ono dozwolone. Jednak nie porównuje do siebie wartości obiektów, a ich adresy w pamięci.

class Book {
   private String title;
   private int numberOfPages;
   private String authorName;

   public Book(String title, int numberOfPages, String authorName) {
	this.title = title;
	this.numberOfPages = numberOfPages;
	this.authorName = authorName;
}
}

Book book1 = new Book(“Title1”, 50, “AuthorName1”);
Book book2 = new Book(“Title1”, 50,  “AuthorName1”);

book1 == book2 -> false;
book1.equals(book2) -> true;

Aby nadpisać- metodę equals() dla naszej klasy musimy nadpisać metodę domyślną przy użyciu adnotacji @Override. Następnie definiujemy metodę equals():

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

   Book book = (Book) o;

   if(!title.equals(book.title)) return false;
   if (numberOfPages != book.numberOfPages) return false;
   return authorName.equals(book.authorName);
}

Metoda przyjmuje jako parametr inny obiekt i zwraca nam boolean (prawda lub fałsz). Następnie sprawdza czy porównywany obiekt istnieje i nie jest null’em. W kolejnym kroku upewnia się, że porównywany obiekt, będzie tego samego typu i przechodzi do porównywanie pola po polu naszego obiektu do obiektu porównywanego.

W powyższym przykładzie są trzy pola, title, numberOfPages i authorName. Najpierw sprawdzamy title i jeżeli jest takie samo dla obydwu obiektów, wtedy przechodzimy do sprawdzania kolejnego, numberOfPages i jeżeli ten też jest taki sam to sprawdzamy ostatni parametr, którego wynik zadecyduje o ostatecznym porównaniu obiektów. Jak widać, pola typu String porównuje się domyślną metodą equals() dla String’ów (String to też obiekt!!!), natomiast pole numberOfPages, które jest typu prymitywnego porównujemy poprzez znak równości (tak robimy dla typów prymitywnych, nie obiektów).

W przykładzie porównano wszystkie pola, lecz nie jest to wymuszone. Implementacja metody może być praktycznie dowolna. Można np porównać tylko tytuł i nazwę autora. Wtedy jeżeli stworzymy dwa obiekty mające ten sam tytuł oraz nazwę autora, różniące się jednak ilością stron to metoda equals() porównująca takie obiekty zwróci nam true.

hashCode()

Skoro możemy porównywać już obiekty tworzone na podstawie klasy, to teraz czas na identyfikator obiektu. Domyślna metoda hashCode() zwraca dla każdego obiektu wytworzonego w pamięci inną wartość. Nawet gdy obiekty będą miały takie same wartości pól (tak jak w przykładzie z obiektami typu Book jw.). Dzieje się tak ponieważ wartość hash jest wyliczana na podstawie adresu pamięci obiektu, a ten jest inny  dla każdego nowego. Jest to bardzo istotna wiadomość, którą teraz zapamiętaj (dalej wytłumaczę co ta wiedza implikuje).

Aby stworzyć swoją implementację metody hashCode() należy tak jak w przypadku equals() nadpisać metodę domyślną.

@Override
public int hashCode() {
   int result = title.hashCode();
   result = 31 * result + numberOfPages;
   result = 31 * result + authorFirstName.hashCode();
   return result;
}

Jak widać każdy nowy obiekt będzie oznakowany kodem, który powstanie na bazie prymitywnych zmiennych obiektu oraz haschCode’ów pól będących obiektami (tutaj String).

Zaczynamy od wartości startowej tutaj hashCode pola title.  Następnie mnożymy wartość hasha przez 31 i zmienną będącą wartością prymitywną. Kolejne pola dodają swoje wartości do poprzednio wytworzonej. W efekcie końcowym mamy hashCode dla nowego obiektu.

 Liczba 31 jest specjalnie wybrana do tworzenia hashCode. Magiczne 31 (liczba pierwsza) pozwala uzyskać lepszą wydajność obliczeniową oraz lepszą dystrybucję hashCode’ów.

Po co to wszystko?

HashCode jak i equals potrzebne są przy organizacji struktury danych opartych o hash (np. HashMap). Jeżeli w przypadku małych programów może nie jest to najistotniejsze to w przypadku operacji na dużych zasobach danych, która wykorzystuje np hash mapę, brak lub błędna implementacja hashCode i equals() może doprowadzić, do problemów wydajnościowych oraz implikować bardzo nieciekawe sytuacje. Przykładem może być wystąpienie niechcianych duplikatów danych (taki bug w aplikacji finansowej to nie lada problem). I tutaj wracam do miejsca, gdy prosiłem o zapamiętanie informacji o domyślnej implementacji metody hashCode(). Skoro domyślnie każdy obiekt dostaje inny hash code, nawet wtedy gdy pola obiektów są takie same, to w przypadku tworzenia aplikacji wykorzystującej hash’e i czułej na duplikaty danych, MUSIMY nadpisać metody hashCode i equals! Pozwoli to na uniknięcie duplikatów oraz przyspieszy proces przetwarzania informacji.

Poniżej lista pewnych zasad godnych zapamiętania w przypadku nadpisywania ww. metod:

  • Jeżeli obiekt1.equals(obiekt2) wtedy obiekt1.hashCode == obiekt2.hashCode jest równe true.
  • Natomiast jeżeli obiekt1.hashCode == obiekt2.hashCode zwróci true to nie znaczy, że obiekt1.equals(obiekt2) zwróci true,
  • Jeżeli używamy zestawu pól klasy do implementacji metody equals() to używajmy tych samych pól do generowania hashCode. Pozwoli to na utrzymanie poprawnego kontraktu, czyli zależności pomiędzy equals() oraz hashCode() w przypadku zmian w systemie.

Dodaj komentarz

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