Metoda equals w Javie. Jak poprawnie ją zaimplementować?

sages.pl 3 lat temu
W języku Java wszystkie klasy dziedziczą po klasie "java.lang.Object". Pośród dziedziczonych metod znajduje się metoda "equals". Poprawna implementacja tej metody jest kluczowa dla poprawności programu i - wbrew pozorom - niekoniecznie trywialna. Wiele struktur danych polega na jej adekwatnej implementacji. W związku z tym błędna jej implementacja skutkuje ich niewłaściwym zachowaniem.

### Metoda java.lang.Object.equals

Metoda ```equals``` umożliwia stwierdzenie, czy dwa obiekty są sobie równe. Jej domyślna definicja dostarczana przez klasę ```Object``` bazuje na referencjach obiektów. Taka implementacja w wielu przypadkach jest wystarczająca. Generalnie, dla klas, których celem jest dostarczanie jakiejś funkcjonalności raczej nie implementuje się metody ```equals```. Przykładem może tu być klient HTTP. Ciężko w ogóle wyobrazić sobie jak mogłoby wyglądać porównywanie takich obiektów inaczej, niż przez identyczność. W takich właśnie przypadkach powinno się polegać na domyślnej implementacji.
Sytuacja wygląda inaczej w przypadku obiektów reprezentujących byty z modelowanego świata, np. obiekty reprezentujące książki, notatki, itd. To właśnie dla tego typu obiektów z reguły dostarcza się metodę ```equals```.

Poprawność implementacji tytułowej metody można rozpatrywać dwojako:
1. Obiekty powinny być sobie równe gdy w modelowanym świecie byłyby równe. Przykładowo, dwie książki możemy uznać za równe o ile mają taki sam numer ISBN.
2. Metoda ```equals``` musi spełniać tzw. kontrakty, które są wymagane przez standard języka Java i których przestrzeganie jest konieczne dla poprawnego zachowania się niektórych struktur danych.

Zanim przejdziemy do analizy wyżej wspomnianych kontraktów, spójrzmy na prostą hierarchię klas, do której będziemy się dalej odwoływać.

```
class Book {
String isbn;
}

class Ebook extends Book {
String format;
}
```

### Kontrakty względem equals

Standard języka wymaga od implementacji equals zachowania następujących niezmienników:
* zwrotność, czyli obiekt jest sam sobie równy. Inaczej mówiąc, dla wszystkich obiektu o prawdą jest, iż ```o.equals(o) == true```,
* symetryczność, czyli o ile pierwszy obiekt jest równy drugiemu, to drugi też jest równy pierwszemu. Oznacza to tyle, iż o ile ```o1.equals(o2)``` zwraca prawdę (fałsz) to ```o2.equals(o1)``` też musi zwrócić prawdę (fałsz),
* spójność, czyli dla każdych dwóch obiektów metoda ```o1.equals(o2)``` powinna zawsze zwracać tę samą wartość, o ile nie zaszły żadne zmiany w obiektach,
* przechodniość to warunek, który zapewnia o tym, iż wynik operacji ```equals``` jest przechodni, tj. o ile mamy trzy obiekty ```o1```, ```o2```, ```o3```, i o ile o1 jest równy ```o2```, a ```o2``` jest równy ```o3``` to wtedy ```o1``` jest równy ```o3```,
* porównanie obiektu i wartości ```null``` zawsze zwraca ```false```.

### Poprawna implementacja metody equals

Skupmy się teraz na poprawnej implementacji metody ```equals```, tj. takiej, która zachowuje wszystkie obwarowania wprowadzone przez standard języka. Generalnie, o ile rozważamy porównywanie obiektów dokładnie takiego samego typu, to sytuacja jest prosta i standardowe implementacje, bazujące na porównywaniu pól obiektów, są poprawne i wystarczające. Sytuacja jednak nie jest tak prosta, ponieważ ```equals``` przyjmuje w argumentach parametr typu ```Object```:

```
public boolean equals(Object o)
```

W konsekwencji, do instancji naszej klasy może zostać porównany obiekt każdego innego typu. I, o ile oczywistym jest, iż obiekty z różnych hierarchii klas są po prostu różne, to, równość obiektów pozostających w jednej hierarchii może być już przedmiotem rozważań.

Skupmy się teraz na klasach przedstawionych powyżej - książki i ebooka. Ustalmy sobie, iż chcielibyśmy takiej sytuacji, w której ebook i książka może być równa. Rozważmy prostą implementację:

```
class Book {
public boolean equals(Object o){
if(!(o instanceof Book)) {
return false;
}
return this.isbn == ((Book)o).isbn;
}
}
```

Ten sposób implementacji uznaje, iż dwie książki (oraz ich pochodne - ebooki) są równe, gdy ich numery ISBN są równe. Rozsądna implementacja dla klasy ```Ebook``` mogłaby wyglądać tak:

```

class Ebook {
public boolean equals(Object o) {
if ((o instanceof Ebook)) {
return format.equals(((Ebook) o).format) && super.equals(o);
} else if ((o instanceof Book)) {
return super.equals(o);
}
return false;
}
}
```

Implementacja ```Ebook.equals``` rozważa dwa przypadki:
1. Porównywany obiekt ma typ ```Ebook```. Sytuacja jest prosta - porównujemy obiekty tego samego typu.
3. Instancję Ebook porównujemy z instancją ```Book```. W tym celu wywołujemy metodę z nadklasy, żeby porównać tę część, którą da się porównać - tylko kod ISBN.

Nie trudno zaobserwować, iż obydwie metody dostarczają zwrotność, symetryczność i spójność. Spójrzmy jednak, jak sytuacja wygląda z przechodniością. Otóż tak zaimplementowany ```equals``` nie jest przechodni. Żeby się o tym przekonać, wystarczy przeanalizować następujący przypadek:

```
Book b1 = new Book("1");
Ebook e1 = new Ebook("1", "mobi"), e2 = new Ebook("1", "epub");
e1.equals(b1) -> true (1)
b1.equals(e2) -> true (2)
e1.equals(e2) -> false (3)
```

Jak widzimy, operacja (3) zwraca ```false```, wbrew temu, co byłoby oczekiwane od przechodniego operatora.

### Co z tą przechodniością?

Zauważmy, iż z przedstawionego przykładu można wysnuć natychmiastowy, bardziej ogólny wniosek. Mianowicie, iż nie da się zaimplementować metody ```equals```, która obejmowałaby porównywanie obiektów w relacji rodzic-dziecko i jednocześnie będącej przechodnią. Taki stan rzeczy nie wynika z ograniczeń języka, a jest po prostu bezpośrednią konsekwencją dziedziczenia. Otóż klasa będąca wyżej w hierarchii klas nie ma pojęcia o polach, które znajdują się w klasie będącej niżej w hierarchii.
W naszym przykładzie książka wie jedynie o numerze ISBN i może co najwyżej tego numeru się spodziewać w klasach pochodnych. Innymi słowy, podczas porównywania książki i ebooka musi dojść do tzw. logicznego *object sliceing’u*, tj. potraktowania ebooka tak jakby był zwykłą książką. Jest to - niech wybrzmi to raz jeszcze - naturalna konsekwencja porównywania bytów różnego typu, zarówno w sensie świata rzeczywistego jak i obiektowego.

Jak w takim razie można rozwiązać taki problem? W takim przypadku można zrobić dwie rzeczy:
1. Uniemożliwić dziedziczenie klas, które są tzw. *value classes* (klasy reprezentujące wartości czy też obiekty świata rzeczywistego). W gruncie rzeczy jest to całkiem rozsądne zarówno z punktu widzenia modelowania świata rzeczywistego jak i technicznego - taki zabieg bardzo upraszcza implementację ```equals```.
2. o ile z jakichś powodów nasza klasa musi być otwarta na ewentualne dziedziczenie, to możemy uznać, iż obiekty różnych typów nigdy nie są sobie równe. Wtedy implementacja jest również bardzo prosta. Szablon metody przy takim podejściu mógłby wyglądać tak:

```
public boolean equals(Object o) {
if(o == null) return false;
if(o.getClass() != this.getClass()) return false;
... // just compare fields
}
```
W takim podejściu należy pamiętać, iż taka metoda nie zachowuje się poprawnie dla klas pochodnych.

Wróćmy jeszcze na moment do źródłowego problemu. Czy naprawdę nie da się zaimplementować metody ```equals``` tak, żeby spełniała wszystkie wymagania i jednocześnie była w stanie porównywać obiekty z różnych poziomów w hierarchii? Generalnie, istnieją techniki, które umożliwiają poprawną implementację. Pozostaje jednak wtedy przez cały czas pytanie, czy rzeczywiście rozsądne jest, aby obiekty różnych typów mogły być równe? Po drugie, takie rozwiązania są najczęściej skomplikowane i dużo trudniejsze w implementacji niż możnaby się spodziewać po ```equals```. W związku z tym najbardziej sensownym wydaje się jednak uznanie, iż klasy przenoszące wartości powinny być klasami zamkniętymi na rozszerzanie.

### Podsumowanie

Implementacja metody ```equals``` wydaje się być bardzo prosta. Należy jednak zwrócić szczególną uwagę na jej adekwatną implementację, gdyż niespełnienie jej wymagań może powodować błędy, które niekoniecznie będą widoczne na pierwszy rzut oka. Programista dostarczający implementację ```equals``` powinien dokładnie przeanalizować czy jego funkcja przestrzega wszystkich wymaganych obostrzeń.
Idź do oryginalnego materiału