8 października 2008

SCJP - Metody equals() i hashCode()

W ostatnim artykule z serii o SCJP – „SCJP - Serializacja” – zapowiedziałem powrót do nieco intensywniejszego trybu pracy… no i jestem. Ostatnio publikowałem 28 września, także zgodnie z obietnicą jest częściej niż co miesiąc. Dziś będzie o metodach equals() i hashCode().

Zacznijmy od podstaw, a mianowicie – do czego właściwie służy metoda equals()? Przecież mamy operator równości ‘==’? Mówiąc najprościej, operator równości służy do sprawdzenia, czy dwa obiekty są dokładnie tym samym (tym samym obiektem), zaś metoda equals() odpowiada na pytanie, czy dwa obiekty są „takie same”. Sformułowanie „takie same” ująłem w cudzysłów, bowiem co ono oznacza zależy tylko od naszego uznania, a mówiąc ściślej, właśnie od implementacji metody equals(). Metoda equals() jest zdefiniowana w klasie java.lang.Object w ten sposób, że jest tożsama z operatorem równości ‘==’, ale w wielu przypadkach nie jest to implementacja właściwa. Dla przykładu, w klasie java.lang.Integer metodę tę nadpisano w ten sposób, że dwa obiekty ‘x’ i ‘y’ są uznawane za „takie same” (‘x.equals(y) == true’), jeśli reprezentują tę samą wartość liczbową, tj. ‘x.intValue() == y.intValue()’.

Powiedzieliśmy sobie przed chwilą, że znaczenie sformułowania „takie same”, a więc implementacja metody equals(), mogą być dowolne i zależą wyłącznie od naszego uznania, ale nie do końca jest to prawdą. Metodę equals() obowiązuje bowiem kontrakt, którego musimy przestrzegać. Oto on:

- Relacja wyznaczona metodą equals() musi być zwrotna, tj. dla każdej zmiennej referencyjnej ‘x’ (różnej od ‘null’) wyrażenie ‘x.equals(x)’ ma wartość ‘true’.

- Relacja wyznaczona metodą equals() musi być symetryczna, tj. dla każdej pary zmiennych referencyjnych ‘x’ i ‘y’, wyrażenie ‘x.equals(y)’ ma wartość ‘true’ wtedy i tylko wtedy gdy ‘y.equals(x)’ ma wartość ‘true’. Chciałoby się powiedzieć, że ‘x.equals(y)’ musi dawać dokładnie ten sam wynik co ‘y.equals(x)’, ale tak nie jest. Jeśli bowiem ‘x’ wskazuje na pewien obiekt a ‘y’ ma wartość ‘null’, to ‘x.equals(y)’ ma wartość ‘false’ (musi być ‘false’, co jest kolejnym punktem kontraktu) a ‘y.equals(x)’ wywołuje wyjątek NullPointerException.

- Relacja wyznaczona metodą equals() musi być przechodnia, tj. dla dowolnych zmiennych referencyjnych ‘x’, ‘y’ i ‘z’, jeśli ‘x.equals(y)’ ma wartość ‘true’ oraz ‘y.equals(z)’ ma wartość ‘true’ to także ‘x.equals(z)’ musi mieć wartość ‘true’.

- Przy założeniu, że porównywane obiekty się w międzyczasie nie zmieniają, każdorazowe wywołanie funkcji ‘x.equals(y)’ musi dawać taki sam wynik, tj. albo zawsze ‘true’ albo zawsze ‘false’.

- Każdy obiekt musi być różny od wartości ‘null’, tj wywołanie ‘x.equals(null)’ musi zawsze zwracać wartość ‘false’.

Ściśle związaną z operacją equals() jest operacja hashCode(). Operację hashCode() również obowiązuje pewien kontrakt i to taki, który definiuje jej zachowanie w zależności od zachowania metody equals(). Stąd w typowej sytuacji, ilekroć nadpisujemy jedną z tych metod musimy nadpisać też i drugą. Oto kontrakt dla metody hashCode():

- Przy założeniu, że obiekt się w międzyczasie nie zmienia, każdorazowe wywołanie funkcji hashCode() musi zwracać, w ramach pojedynczego uruchomienia aplikacji, taką samą wartość. Co prawda wtrącenie, że „w ramach pojedynczego uruchomienia aplikacji” nie ma specjalnie sensu (inne uruchomienie, a więc inny obiekt), ale jest to wtrącone także w „oficjalnej” dokumentacji więc niech zostanie.

- Jeśli dwa obiekty ‘x’ i ‘y’ są „takie same”, tj. ‘x.equals(y)’ ma wartość ‘true’, to muszą one mieć takie same wartości skrótu (ang. hash code), tj. musi zachodzić ‘x.hashCode() == y.hashCode()’. Zauważmy, że implikacja działa tylko w jedną stronę, tj. jeśli obiekty „są różne” (tj. nie są „takie same”), to ich wartości skrótu wcale nie muszą być różne.

I na koniec jeszcze przykład. Zwróćmy szczególną uwagę na sygnatury metod. Upewnijmy się, że to co nam pokażą na egzaminie SCJP, to będą rzeczywiście nadpisania a nie przeciążenia.
public class MyClass {
private int someValue;

public int hashCode() {
return someValue;
}

public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass())
return false;

if (someValue != ((MyClass) obj).someValue)
return false;

return true;
}
}

5 komentarzy:

Anonimowy pisze...

"- Przy założeniu, że porównywane obiekty się w międzyczasie nie zmieniają, każdorazowe wywołanie funkcji ‘x.equals(y)’ musi dawać taki sam wynik, tj. albo zawsze ‘true’ albo zawsze ‘false’."

Ciekawostką jest, że ten kontrakt nie jest spełniany przez niektóre klasy w JRE. Opisałem przykład tutaj: http://java.zacheusz.eu/zmienne-wyniki-porownan-javaneturl/18/

stanb pisze...

W Twojej przykladowej implementacji equals(), nie rozumiem wyrazenia ((MyClass) obj).someValue) w warunku. Przeciez someValue jest private?

Mariusz Lipiński pisze...

Prywatne, to znaczy niedostępne dla innych klas, a tu kod jest w tej samej klasie co zmienna.

Unknown pisze...

Wiem że to było jakiś czas temu, ale czy jesteś pewien że wykorzystanie hashcode w equals to dobry pomysł? :)

Anonimowy pisze...

W tym wypadku jak najbardziej, chociażby ze względu że klasa zawiera jedno pole int, na podstawie którego sprawdzamy czy obiekty są takie same, bo skoro w jednym i drugim jest ta sama wartość int to znaczy że są takie same ;)