Kilian Foths Antwort ist ausgezeichnet. Ich möchte nur das kanonische Beispiel * hinzufügen, warum dies ein Problem ist. Stellen Sie sich eine Integer-Point-Klasse vor:
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);
}
}
Lassen Sie es uns als 3D-Punkt unterteilen.
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 einfach! Verwenden wir unsere Punkte:
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
Sie wundern sich wahrscheinlich, warum ich ein so einfaches Beispiel poste. Hier ist der Haken:
p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!
Wenn wir den 2D-Punkt mit dem äquivalenten 3D-Punkt vergleichen, erhalten wir Wahr, aber wenn wir den Vergleich umkehren, erhalten wir Falsch (weil p2a fehlschlägt instanceof Point3D
).
Fazit
Es ist normalerweise möglich, eine Methode in einer Unterklasse so zu implementieren, dass sie nicht mehr mit den Erwartungen der Superklasse kompatibel ist.
Es ist im Allgemeinen unmöglich, equals () in einer signifikant anderen Unterklasse auf eine Weise zu implementieren, die mit der übergeordneten Klasse kompatibel ist.
Wenn Sie eine Klasse schreiben, deren Unterklassen Sie zulassen möchten, empfiehlt es sich, einen Vertrag für das Verhalten der einzelnen Methoden zu erstellen. Noch besser wäre eine Reihe von Unit-Tests, mit denen die Leute ihre Implementierung überschriebener Methoden testen könnten, um zu beweisen, dass sie nicht gegen den Vertrag verstoßen. Fast niemand macht das, weil es zu viel Arbeit ist. Aber wenn es dich interessiert, ist es das, was du tun musst.
Ein gutes Beispiel für einen gut formulierten Vertrag ist Comparator . Ignorieren Sie einfach, worüber es .equals()
aus den oben beschriebenen Gründen sagt . Hier ist ein Beispiel dafür, wie Comparator das nicht .equals()
kann .
Anmerkungen
Josh Blochs "Effective Java" Item 8 war die Quelle dieses Beispiels, aber Bloch verwendet einen ColorPoint, der anstelle einer dritten Achse eine Farbe hinzufügt und anstelle von Ints Doubles verwendet. Das Java-Beispiel von Bloch wird im Wesentlichen von Odersky / Spoon / Venners dupliziert , die ihr Beispiel online zur Verfügung gestellt haben.
Mehrere Personen haben Einwände gegen dieses Beispiel erhoben, da Sie dieses Problem beheben können, wenn Sie die übergeordnete Klasse über die Unterklasse informieren. Das ist wahr, wenn es eine ausreichende Anzahl von Unterklassen gibt und wenn die Eltern über alle Bescheid wissen. Die ursprüngliche Frage war jedoch, eine API zu erstellen, für die jemand anderes Unterklassen schreibt. In diesem Fall können Sie die übergeordnete Implementierung im Allgemeinen nicht so aktualisieren, dass sie mit den Unterklassen kompatibel ist.
Bonus
Comparator ist auch deshalb interessant, weil es darum geht, equals () korrekt zu implementieren. Besser noch, es folgt einem Muster zur Behebung dieser Art von Vererbungsproblemen: dem Strategieentwurfsmuster. Die Typeclasses, auf die sich Haskell und Scala freuen, sind auch das Strategiemuster. Vererbung ist nicht schlecht oder falsch, sie ist nur knifflig. Für weitere Lesung Besuche Philip Wadler Papier Wie Ad-hoc - Polymorphismus weniger ad hoc machen