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żeli 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żeli 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, iż “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):
Wygląda niewinnie, prawda? Można wypychać? Po paru tygodniach wpada nieświadomy junior, który “poczebuje zaimplementować Punkt3D”:
Na pierwszy rzut oka kod wygląda niezgorzej. Jednak uruchomienie prostego sprawdzenia poprawności implementacji metod equals():
daje nam następujący wynik:
true falseWniosek 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ę, iż 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 iż przeglądy kodu świadczą o tym, iż 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żeli 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żeli widzisz gdzieś w kodzie (produkcyjnym czy na blogu) instanceof wewnątrz equals() to wiedz, iż 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, iż 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ę, iż nie będzie asymetrycznego zachowania equals.
- Chwileczkę, ale wtedy mój Point3D(1, 1, 1) będzie równy Point(1, 1), nie o to mi chodzi, ponieważ ten point3D nie jest równy point!!11jeden"
- Otóż to, czy nie mówiłem, iż Point3D to nie jest Point z tylko jednym dodatkowym wymiarem/właściwością"? ;-)
TL;DR: finalizacja powoduje, iż instanceof w equals() jest bezpieczne TL;DR2: Nie kopiuj i nie wklejaj losowych fragmentów z internetu bez zrozumienia kontekstu.