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

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 [equals]

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.

public boolean equals(Object obj) {
return (this == obj);
}

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 między innymi 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 między equals i hashCode

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, 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.

public class User {
private String name;
private String username;
private Integer age;
@Override
public int hashCode() {
int hash = 5;
hash = 37 * hash + Objects.hashCode(this.name);
hash = 37 * hash + Objects.hashCode(this.username);
hash = 37 * hash + Objects.hashCode(this.age);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final User other = (User) obj;
if (!Objects.equals(this.name, other.name)) {
return false;
}
if (!Objects.equals(this.username, other.username)) {
return false;
}
if (!Objects.equals(this.age, other.age)) {
return false;
}
return true;
}
}
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.

public class ManualEqualsTester {
private static final int ITERATIONS = 10;
private final Object reference, equalReference, secondEqualReference, notEqualReference;
public ManualEqualsTester(Object reference, Object equalReference, Object secondEqualReference, Object notEqualReference) {
this.reference = reference;
this.equalReference = equalReference;
this.notEqualReference = notEqualReference;
this.secondEqualReference = secondEqualReference;
}
public void verify() {
testConstancyHashCode();
testEqualityOfHashCode();
testIsReflexive();
testIsSymmetric();
testIsTransitive();
testConstancyEquals();
testNullIsNotEquals();
}
private void testConstancyHashCode() {
int hashCode = reference.hashCode();
for (int i = 0; i < ITERATIONS; i++) {
Assert.assertEquals(hashCode, reference.hashCode());
}
}
private void testEqualityOfHashCode() {
Assert.assertEquals(reference.hashCode(), equalReference.hashCode());
}
private void testIsReflexive() {
Assert.assertTrue(reference.equals(reference));
Assert.assertTrue(equalReference.equals(equalReference));
Assert.assertTrue(notEqualReference.equals(notEqualReference));
}
private void testIsSymmetric() {
Assert.assertTrue(reference.equals(equalReference));
Assert.assertTrue(equalReference.equals(reference));
Assert.assertFalse(reference.equals(notEqualReference));
Assert.assertFalse(notEqualReference.equals(reference));
}
private void testIsTransitive() {
Assert.assertTrue(reference.equals(equalReference));
Assert.assertTrue(equalReference.equals(secondEqualReference));
Assert.assertTrue(reference.equals(secondEqualReference));
}
private void testConstancyEquals() {
for (int i = 0; i < ITERATIONS; i++) {
Assert.assertTrue(reference.equals(equalReference));
Assert.assertTrue(reference.equals(secondEqualReference));
Assert.assertFalse(reference.equals(notEqualReference));
}
}
private void testNullIsNotEquals() {
Assert.assertFalse(reference.equals(null));
Assert.assertFalse(equalReference.equals(null));
Assert.assertFalse(secondEqualReference.equals(null));
Assert.assertFalse(notEqualReference.equals(null));
}
}

 

public class UserManualTest {
private static ManualEqualsTester manualEqualsTester;
@BeforeClass
public static void setUp() throws Exception {
User user = new User("Tomasz", "Woliński", 100);
User userEqual = new User("Tomasz", "Woliński", 100);
User userSecondEqual = new User("Tomasz", "Woliński", 100);
User userNotEqual = new User("", "", 1);
manualEqualsTester = new ManualEqualsTester(user, userEqual, userSecondEqual, userNotEqual);
}
@Test
public void testEquality() {
manualEqualsTester.verify();
}
}

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.

public class UserEqualsVerifierTest {
@Test
public void testEquality() {
EqualsVerifier.forClass(User.class).usingGetClass().verify();
}
}

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.

package pl.stormit.hashcodeequals;
import com.google.common.testing.EqualsTester;
import org.junit.BeforeClass;
import org.junit.Test;
public class UserGuavaTest {
private static EqualsTester equalsTester;
private static User user1, user2;
@BeforeClass
public static void setUp() throws Exception {
equalsTester = new EqualsTester();
user1 = new User("Tomasz", "Woliński", 1000);
user2 = new User("Tomasz", "Woliński", 1000);
}
@Test
public void testEquality() {
equalsTester.addEqualityGroup(user1, user2);
equalsTester.testEquals();
}
}

 

Podsumowanie – hashCode i equals

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.

 

kierunek java


Jak zostać programistą

8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista.

Jak zostać programistą
No comments
Share:

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Chcesz wejść do IT lub zmienić branżę i zostać programistą?

Skorzystaj z DARMOWEJ WIEDZY o Javie! >> KierunekJava.pl

Lista 8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista!

Dołączam do newslettera
i odbieram materiały!

PAMIĘTAJ, żeby odebrać wiadomość potwierdzającą i kliknąć w przycisk.


Zapisując się na newsletter, zgadzasz się na przetwarzanie Twoich danych osobowych w celu wysyłania na wskazany przez Ciebie adres e-mail informacji handlowych o nowościach, promocjach, produktach i usługach związanych z serwisami stormit.pl i kierunekprogramista.pl. Będzie to marketing bezpośredni. Administratorem Twoich danych osobowych będzie Tomasz Woliński prowadzący działalność gospodarczą Tomasz Woliński Storm IT, Przytulna 38/43, 80-176 Gdańsk, NIP: 7431875586. Przysługuje Ci prawo do cofnięcia zgody, żądania wglądu do Twoich danych, wniesienia sprzeciwu co do ich przetwarzania, sprostowania, usunięcia i ograniczenia przetwarzania. Więcej informacji o tym jak przetwarzam Twoje dane znajdziesz na stormit.pl/polityka-prywatnosci/.