Odpowiedź Kiliana Fotha jest doskonała. Chciałbym tylko dodać kanoniczny przykład *, dlaczego jest to problem. Wyobraź sobie całkowitą klasę Point:
class Point2D {
public int x;
public int y;
// constructor
public Point2D(int theX, int theY) { x = theX; y = theY; }
public int hashCode() { return x + y; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point2D) ) { return false; }
Point2D that = (Point2D) o;
return (x == that.x) &&
(y == that.y);
}
}
Podklasujmy teraz, aby był punktem 3D.
class Point3D extends Point2D {
public int z;
// constructor
public Point3D(int theX, int theY, int theZ) {
super(x, y); z = theZ;
}
public int hashCode() { return super.hashCode() + z; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point3D) ) { return false; }
Point3D that = (Point3D) o;
return super.equals(that) &&
(z == that.z);
}
}
Super proste! Wykorzystajmy nasze punkty:
Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);
p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false
Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);
p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false
Prawdopodobnie zastanawiasz się, dlaczego zamieszczam tak łatwy przykład. Oto haczyk:
p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!
Kiedy porównujemy punkt 2D z równoważnym punktem 3D, stajemy się prawdą, ale kiedy odwracamy porównanie, otrzymujemy fałsz (ponieważ p2a zawodzi instanceof Point3D
).
Wniosek
Zazwyczaj możliwe jest zaimplementowanie metody w podklasie w taki sposób, aby nie była już zgodna z tym, jak superklasa oczekuje, że zadziała.
Zasadniczo niemożliwe jest zaimplementowanie funkcji equals () w znacznie innej podklasie w sposób zgodny z jej klasą nadrzędną.
Kiedy piszesz klasę, którą zamierzasz pozwolić ludziom na podklasę, naprawdę dobrym pomysłem jest napisanie kontraktu dotyczącego tego, jak każda metoda powinna się zachowywać. Jeszcze lepszy byłby zestaw testów jednostkowych, które ludzie mogliby przeprowadzić przeciwko wdrożeniu zastąpionych metod, aby udowodnić, że nie naruszają umowy. Prawie nikt tego nie robi, bo to za dużo pracy. Ale jeśli cię to obchodzi, to jest to, co należy zrobić.
Doskonałym przykładem dobrze napisanej umowy jest komparator . Po prostu zignoruj to, o czym mówi .equals()
z powodów opisanych powyżej. Oto przykład tego, jak Komparator może robić rzeczy, .equals()
których nie może .
Notatki
Źródłem tego przykładu był „Efektywna Java” Josh Blocha, pozycja 8, ale Bloch używa ColorPoint, który dodaje kolor zamiast trzeciej osi i używa podwójnych zamiast ints. Przykład Java Blocha jest w zasadzie powielony przez Odersky / Spoon / Venners, którzy udostępnili swój przykład online.
Kilka osób sprzeciwiło się temu przykładowi, ponieważ jeśli poinformujesz klasę nadrzędną o podklasie, możesz rozwiązać ten problem. To prawda, jeśli istnieje wystarczająco mała liczba podklas i jeśli rodzic wie o nich wszystkich. Ale pierwotne pytanie dotyczyło stworzenia interfejsu API, dla którego ktoś inny napisze podklasy. W takim przypadku generalnie nie można zaktualizować implementacji nadrzędnej, aby była zgodna z podklasami.
Premia
Komparator jest również interesujący, ponieważ działa poprawnie w kwestii poprawnego implementowania funkcji equals (). Co więcej, jest to zgodne ze schematem naprawiania tego rodzaju problemu spadkowego: wzorzec projektowania strategii. Typy, którymi podekscytowani są ludzie Haskell i Scala, są również wzorem strategii. Dziedziczenie nie jest złe ani złe, jest po prostu trudne. Więcej informacji można znaleźć w artykule Philipa Wadlera Jak uczynić polimorfizm ad hoc mniej ad hoc