Jak nie korzystać z (pattern matching with) instanceof
Można, nie znaczy trzeba (a wręcz nie powinno się)
(Ten wpis został zaktualizowany. Dodatek jest na końcu.)
W pogoni za nietrywialnymi przykładami do zilustrowania zastosowań JEPa 305 (opisanego przeze mnie tutaj) niektóre osoby mogły zapędzić się za daleko. W szczególności w trudniejszy teren metody equals()
obecnej w każdym obiekcie w Javie. Dokonajmy teraz aktu, który pewnie będzie budził zgorszenie wielu braci programistów i sióstr programistek: zajrzyjmy do dokumentacji metody Object.equals()
. Można tam wyczytać jakie warunki musi spełniać implementacja equals()
, żeby była poprawna. Jednym z nich jest zasada symetryczności: jeśli mamy dwa dowolne obiekty x
i y
, to wywołania x.equals(y)
oraz y.equals(x)
muszą dać taki sam wynik. Oba muszą dać albo true
, albo false
. Jeśli jedno daje true
a drugie false
, to metoda jest źle zaimplementowana. Handlujcie z tym.
Jednym z popularnych mitów krążących w światku programowania jest to, że “dziedziczenie to ZŁO”. (Jeśli jest dość czasu to mówię o tym w CONTEXTVS, STVLTE!)
Dziedziczenie i hierarchie typów nie są złe same z siebie. Nie wiem, jak ktoś może programować całkowicie bez dziedziczenia i nazywać to “programowaniem zorientowanym na obiekty”. Złe jest ich bezrozumne stosowanie w tych wypadkach, gdy nie powinny być stosowane, bo np. łamią zasadę podstawienia Barbary Liskov. Okraszone bezrefleksyjnym stosowaniem JEPa 305 daje ono iście wybuchową mieszankę.
Dobra, ale o co chodzi?
Popatrzmy na taki kod (którego całość dostępna jest tutaj):
1class Point {
2 public final int x;
3 public final int y;
4
5 @Override
6 public boolean equals(Object o) {
7 if (this == o) return true;
8 if (o instanceof Point that) {
9 return this.x == that.x && this.y == that.y;
10 }
11 return false;
12 }
13 // ...
14}
Wygląda niewinnie, prawda? Można wypychać? Po paru tygodniach wpada nieświadomy junior, który “poczebuje zaimplementować Punkt3D”:
1class Point3D extends Point {
2 public final int z;
3
4 @Override
5 public boolean equals(Object o) {
6 if (this == o) return true;
7 if (o instanceof Point3D that) {
8 if (!super.equals(o))
9 return false;
10 return z == that.z;
11 }
12 return false;
13 }
14 //...
15}
Na pierwszy rzut oka kod wygląda niezgorzej. Jednak uruchomienie prostego sprawdzenia poprawności implementacji metod equals()
:
1var point = new Point (1,1);
2var point3D = new Point3D(1,1,1);
3System.out.println(point.equals(point3D));
4System.out.println(point3D.equals(point));
daje nam następujący wynik:
true
false
Wniosek jest prosty: implementacja metod equals()
nie jest symetryczna. A dlaczegóż to, laboga, dlaczegóż to? Ano, z JEPem 305 jest jak z każdym innym napaleniem się na bajery jak szczerbaty na suchary: nie zadziała w tym wypadku. Błąd tkwi w linii 8. Ale jak to, przecież tam jest dopasowanie wzorca z instanceof, po to to zrobili, nie? Myślę, że wątpię. Punkt3D to nie jest “taki, rozumisz, zwykły Punkt, tylko z jeszcze jedną osią” oraz nie można w equals
z punktem przyrównywać wszystkiego, co tylko jest punktem. Tak, instanceof
zwraca true
nie tylko dla dokładnie tego typu, ale także każdego typu, który dziedziczy/rozszerza. Tak, wiem, truzim. Tylko że przeglądy kodu świadczą o tym, że jakoś ów truizm nie przyjął się w powszechnej świadomości…
Bez zbędnej spiny, proszę
W życiu trzeba też być pragmatycznym programistą. Czasami zaimplementowanie equals()
z wykorzystaniem instanceof
razem dobrodziejstwem JEPa 305 nie jest złe, jeśli wszyscy jesteśmy świadomi konsekwencji, nie będziemy po typie dziedziczyć, a taki rodzaj sprawdzania identyczności jest potrzebny, bo jakieś frameworki tworzą proxy dziedziczące np. z encji.
Stąd taka moja mała prośba: jeśli widzisz gdzieś w kodzie (produkcyjnym czy na blogu) instanceof
wewnątrz equals()
to wiedz, że coś się dzieje. I nie kopiuj kodu z internetu na ślepo. Kontekst, głupcze!
Aktualizacja
Aby wszystko było jasne i by uniknąć nieporozumień, postanowiłem skorzystać z porady Nicolai’a Parloga i zaktualizować ten wpis w kwietniu 2022 roku.
Używanie instanceof
w equals()
samo w sobie nie jest problematyczne. Również używanie dziedziczenia w Javie samo w sobie nie jest problematyczne. Tak jak używanie młotka nie jest. Ale nadużycie już tak.
Kłopoty pojawiają się, gdy są one niewłaściwie używane, zwłaszcza razem: equals()
polegająca na instanceof
oraz klasy potomne.
Rozwiązanie tego problemu jest dość proste: najlepiej, aby klasa była final
. Point3D
to nie jest Point
z jeszcze jednym dodatkowym wymiarem/właściwością. Przypuszczam, że klasa, która jest sealed
wraz z dobrze napisanymi equals
w całej rodzinie też by wystarczyła.
Jeśli nie możesz sprawić, by klasa była final
, to: public final boolean equals(Object o)
też jest opcją. Wrzucając final
do equals()
przynajmniej upewniamy się, że nie będzie asymetrycznego zachowania equals
.
- Chwileczkę, ale wtedy mój
Point3D(1, 1, 1)
będzie równyPoint(1, 1)
, nie o to mi chodzi, ponieważ ten point3D nie jest równy point!!11jeden" - Otóż to, czy nie mówiłem, że
Point3D
to nie jestPoint
z tylko jednym dodatkowym wymiarem/właściwością"? ;-)
TL;DR: final
izacja powoduje, że instanceof
w equals()
jest bezpieczne
TL;DR2: Nie kopiuj i nie wklejaj losowych fragmentów z internetu bez zrozumienia kontekstu.