Czy instancja może być równa innej instancji bardziej konkretnego typu?

25

Przeczytałem ten artykuł: Jak napisać metodę równości w Javie .

Zasadniczo zapewnia rozwiązanie dla metody equals (), która obsługuje dziedziczenie:

Point2D twoD   = new Point2D(10, 20);
Point3D threeD = new Point3D(10, 20, 50);
twoD.equals(threeD); // true
threeD.equals(twoD); // true

Ale czy to dobry pomysł? te dwa wystąpienia wydają się być równe, ale mogą mieć dwa różne kody skrótu. Czy to nie jest trochę złe?

Wierzę, że lepiej by to osiągnąć, zamiast tego rzucając operandami.

Wes
źródło
1
Przykład z kolorowymi punktami podany w łączu ma dla mnie większy sens. Rozważałbym, że punkt 2D (x, y) może być postrzegany jako punkt 3D z zerowym składnikiem Z (x, y, 0) i chciałbym, aby równość zwróciła w twoim przypadku fałsz. W rzeczywistości w artykule wyraźnie stwierdzono, że kolorowy punkt różni się od punktu i zawsze zwraca wartość false.
coredump
10
Nic gorszego niż samouczki, które łamią powszechne konwencje ... Potrzeba lat, aby zerwać z programistami tego rodzaju nawyki.
corsiKa
3
@coredump Traktowanie punktu 2D jako zwspółrzędnej zerowej może być przydatną konwencją dla niektórych aplikacji (przychodzą na myśl wczesne systemy CAD obsługujące starsze dane). Ale to konwencja arbitralna. Płaszczyzny w przestrzeniach o 3 lub więcej wymiarach mogą mieć dowolną orientację ... to sprawia, że ​​interesujące problemy są interesujące.
ben rudgers
2
To więcej niż trochę źle .
Kevin Krumwiede

Odpowiedzi:

71

Nie powinna to być równość, ponieważ psuje przechodniość . Rozważ te dwa wyrażenia:

new Point3D(10, 20, 50).equals(new Point2D(10, 20)) // true
new Point2D(10, 20).equals(new Point3D(10, 20, 60)) // true

Ponieważ równość jest przechodnia, powinno to oznaczać, że prawdziwe jest również następujące wyrażenie:

new Point3D(10, 20, 50).equals(new Point3D(10, 20, 60))

Ale oczywiście - nie jest.

Twój pomysł na rzutowanie jest poprawny - spodziewaj się, że w Javie rzutowanie oznacza po prostu rzutowanie typu odwołania. To, czego naprawdę chcesz, to metoda konwersji, która utworzy nowy Point2Dobiekt z Point3Dobiektu. To sprawiłoby, że wyrażenie to byłoby bardziej znaczące:

twoD.equals(threeD.projectXY())
Idan Arye
źródło
1
W tym artykule opisano implementacje, które łamią przechodniość i oferuje szereg obejść. W dziedzinie, w której zezwalamy na punkty 2D, już zdecydowaliśmy, że trzeci wymiar nie ma znaczenia. i więc (10, 20, 50)równa się (10, 20, 60)jest w porządku. Dbamy tylko o 10i 20.
ben rudgers
1
Czy powinna Point2Distnieć projectXYZ()metoda zapewnienia Point3Dreprezentacji samego siebie? Innymi słowy, czy wdrożenia powinny się znać?
hjk
4
@hjk Pozbycie się Point2Dwydaje się prostsze, ponieważ rzutowanie punktów 2D wymaga najpierw zdefiniowania ich płaszczyzny w przestrzeni 3D. Jeśli punkt 2D wie, że jest to płaszczyzna, to jest to już punkt 3D. Jeśli nie, to nie może wyświetlić. Przypomina mi się Flatland Abbotta .
ben rudgers
@benrudgers Możesz jednak zdefiniować Plane3Dobiekt, który zdefiniuje płaszczyznę w przestrzeni 3D, ta płaszczyzna może mieć liftmetodę (2D-> 3D podnosi, nie rzutuje), która zaakceptuje a Point2Di liczbę dla „trzeciej osi „- odległość od płaszczyzny wzdłuż płaszczyzny normalnej. Aby ułatwić korzystanie, możesz zdefiniować wspólne płaszczyzny jako stałe statyczne, abyś mógł robić rzeczy takie jakPlane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50))
Idan Arye
@IdanArye Skomentowałem sugestię, że punkty 2D powinny mieć metodę projekcji. Jeśli chodzi o płaszczyzny z metodami podnoszenia, myślę, że potrzebne byłyby dwa argumenty: punkt 2D i płaszczyzna, na której zakłada się, że jest na nim, tj. Naprawdę musi to być rzut, jeśli nie jest właścicielem punktu ... a jeśli jest właścicielem punktu, dlaczego po prostu nie ma punktu 3D i pozbyć się problematycznego typu danych i zapachu zakłóconej metody? YMMV.
ben rudgers
10

Odchodzę od czytania artykułu, myśląc o mądrości Alana J. Perlisa:

Epigram 9. Lepiej jest mieć 100 funkcji działających na jednej strukturze danych niż 10 funkcji na 10 strukturach danych.

Fakt, że poprawność „równości” jest rodzajem problemu, który utrzymuje Martina Orderky'ego, wynalazcę Scali w nocy, powinien przerwać, czy przesłonięcieequals drzewa spadkowego jest dobrym pomysłem.

To, co się dzieje, gdy nie mamy szczęścia, to ColoredPointto, że nasza geometria zawodzi, ponieważ użyliśmy dziedziczenia do rozprzestrzeniania typów danych, zamiast tworzyć jeden dobry. Dzieje się tak pomimo konieczności cofnięcia się i zmodyfikowania węzła głównego drzewa dziedziczenia, aby equalsdziałał. Dlaczego nie dodać po prostu zai colordo Point?

Dobry powód by to działał Pointi ColoredPointdziałał w różnych domenach ... przynajmniej jeśli te domeny nigdy się nie zmieszały. Jednak w takim przypadku nie musimy nadpisywać equals. Porównywanie ColoredPointi Pointdla równości ma sens tylko w trzeciej domenie, w której mogą się mieszać. I w takim przypadku prawdopodobnie lepiej jest, aby „równość” była dostosowana do tej trzeciej domeny, niż próbować zastosować semantykę równości z jednej lub drugiej lub obu niezmienionych domen. Innymi słowy, „równość” powinna być zdefiniowana lokalnie w miejscu, w którym płynie błoto z obu stron, ponieważ możemy nie chcieć ColoredPoint.equals(pt)zawieść przeciwko przypadkom, Pointnawet jeśli autor ColoredPointuważał, że to dobry pomysł sześć miesięcy temu o 2 nad ranem .

Ben Rudgers
źródło
6

Kiedy dawni bogowie programowania wymyślali programowanie obiektowe za pomocą klas, zdecydowali, kiedy chodzi o kompozycję i dziedziczenie, aby mieć dwa relacje dla obiektu: „jest” i „ma”.
To częściowo rozwiązało problem, że podklasy różnią się od klas nadrzędnych, ale uczyniły je użytecznymi bez łamania kodu. Ponieważ instancja podklasy „jest” obiektem nadklasy i można go bezpośrednio zastąpić, nawet jeśli podklasa ma więcej funkcji składowych lub elementów danych, „ma” gwarantuje, że wykona wszystkie funkcje elementu nadrzędnego i będzie mieć wszystkie członkowie. Można więc powiedzieć, że Point3D ”to„ Point, a Point2D ”to„ Point, jeśli oba dziedziczą po Point. Dodatkowo Point3D może być podklasą Point2D.

Równość między klasami jest jednak specyficzna dla dziedziny, a powyższy przykład jest dwuznaczny co do tego, czego programista potrzebuje, aby program działał poprawnie. Zasadniczo przestrzegane są reguły domeny matematycznej, a wartości danych generowałyby równość, jeśli ograniczysz zakres porównania tylko w tym przypadku do dwóch wymiarów, ale nie w przypadku porównania wszystkich elementów danych.

Otrzymujesz więc tabelę zawężających się równości:

Both objects have same values, limited to subset of shared members

Child classes can be equal to parent classes if parent and childs
data members are the same.

Both objects entire data members are the same.

Objects must have all same values and be similar classes. 

Objects must have all same values and be the same class type. 

Equality is determined by specific logical conditions in the domain.

Only Objects that both point to same instance are equal. 

Zazwyczaj wybierasz najsurowsze reguły, które możesz nadal wykonywać wszystkie niezbędne funkcje w domenie problemowej. Wbudowane testy równości dla liczb są tak restrykcyjne, jak mogą być do celów matematycznych, ale programista ma wiele sposobów na to, jeśli to nie jest cel, w tym zaokrąglanie w górę / w dół, obcinanie, gt, lt itp. . Obiekty ze znacznikami czasu są często porównywane według czasu ich wygenerowania, dlatego każde wystąpienie musi być unikalne, aby porównania były bardzo szczegółowe.

Czynnikiem projektowym w tym przypadku jest określenie skutecznych sposobów porównywania obiektów. Czasami musisz wykonać rekurencyjne porównanie wszystkich elementów danych z elementami, co może być bardzo kosztowne, jeśli masz wiele obiektów z dużą liczbą elementów danych. Alternatywą jest porównywanie tylko odpowiednich wartości danych lub sprawienie, by obiekt wygenerował wartość skrótu danych elementów, których to dotyczy, w celu szybkiego porównania z innymi podobnymi obiektami, utrzymywania kolekcji posortowanych i przycinanych, aby porównania były szybsze i mniej obciążające procesorem, a być może zezwalać obiektom, które są identyczne pod względem danych, które mają zostać ubite, a na ich miejsce należy umieścić zduplikowany wskaźnik do pojedynczego obiektu.

Chris Reid
źródło
2

Zasada jest taka, że ​​za każdym razem, gdy nadpisujesz hashcode(), nadpisujesz equals()i vice versa. To, czy jest to dobry pomysł, czy nie, zależy od zamierzonego zastosowania. Osobiście wybrałbym inną metodę ( isLike()lub podobną), aby osiągnąć ten sam efekt.

TMN
źródło
1
Można przesłonić hashCode bez zastępowania równości. Na przykład można to zrobić, aby przetestować inny algorytm mieszający dla tego samego warunku równości.
Patricia Shanahan
1

Często przydatne jest, aby klasy niepubliczne miały metodę testowania równoważności, która pozwala obiektom różnych typów uważać się za „równe”, jeśli reprezentują te same informacje, ale ponieważ Java nie pozwala, w jaki sposób klasy mogą się podszywać pod siebie inne często dobrze jest mieć jeden typ opakowania publicznego, w przypadkach, w których możliwe jest posiadanie równoważnych obiektów o różnych reprezentacjach.

Rozważmy na przykład klasę enkapsulującą niezmienną macierz 2D doublewartości. Jeśli jedna metoda zewnętrzna prosi o matrycę tożsamości o wielkości 1000, druga prosi o matrycę diagonalną i przekazuje tablicę zawierającą 1000, a trzecia pyta o matrycę 2D i przekazuje macierz 1000 x 1000, w której wszystkie elementy na pierwotnej przekątnej wynoszą 1,0 a wszystkie inne są zerowe, obiekty podane wszystkim trzem klasom mogą wewnętrznie korzystać z różnych magazynów zaplecza [pierwszy z pojedynczym polem wielkości, drugi z tablicą tysiąca elementów, a trzeci z tysiącem tablic 1000 elementów], ale powinny zgłaszać się nawzajem jako równoważne [ponieważ wszystkie trzy enkapsulują niezmienną macierz 1000 x 1000 z jednymi na przekątnej i zerami wszędzie indziej].

Poza faktem, że ukrywa istnienie odrębnych typów zaplecza sklepowego, opakowanie jest również przydatne do ułatwienia porównań, ponieważ sprawdzanie równoważności elementów będzie zasadniczo procesem wieloetapowym. Zapytaj pierwszy element, czy wie, czy jest równy drugiemu; jeśli nie wie, zapytaj drugiego, czy wie, czy jest równy pierwszemu. Jeśli żaden obiekt nie wie, zapytaj każdą tablicę o zawartość jej poszczególnych elementów [można dodać inne kontrole przed podjęciem decyzji o długim powolnym porównywaniu poszczególnych elementów].

Zauważ, że metoda testu równoważności dla każdego obiektu w tym scenariuszu musiałaby zwrócić wartość trójstanową („Tak, jestem równoważny”, „Nie, nie jestem równoważny” lub „Nie wiem”), więc normalna metoda „równa się” nie byłaby odpowiednia. Podczas gdy jakikolwiek obiekt może po prostu odpowiedzieć „nie wiem”, gdy zostanie o nie zapytany, dodanie logiki do np. Macierzy diagonalnej, która nie zawracałaby sobie głowy pytaniem o dowolną matrycę tożsamości lub macierz diagonalną o dowolne elementy poza główną przekątną, znacznie przyspieszyłoby porównania między takimi typy.

supercat
źródło