Kilian Foth's answer is excellent. I'd just like to add the canonical example* of why this is a problem. Imagine an integer Point class:
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);
}
}
Now let's sub-class it to be a 3D point.
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 simple! Let's use our points:
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
You are probably wondering why I'm posting such an easy example. Here's the catch:
p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!
When we compare the 2D point to the equivalent 3D point, we get true, but when we reverse the comparason, we get false (because p2a fails instanceof Point3D
).
Conclusion
It is usually possible to implement a method in a subclass in such a way that it's no-longer compatible with how the super-class expects it to work.
It is generally impossible to implement equals() on a significantly different subclass in a way that is compatible with it's parent class.
When you write a class that you intend to allow people to subclass, it's a really good idea to write out a contract for how each method should behave. Even better would be a set of unit tests people could run against their implementations of overridden methods to prove that they do not violate the contract. Almost nobody does that because it's too much work. But if you care, that's the thing to do.
A great example of a well-spelled out contract is Comparator. Just ignore what it says about .equals()
for the reasons described above. Here's an example of how Comparator can do things .equals()
can't.
Notes
Josh Bloch's "Effective Java" Item 8 was the source of this example, but Bloch uses a ColorPoint which adds a color instead of a third axis and uses doubles instead of ints. Bloch's Java example is basically duplicated by Odersky/Spoon/Venners who made their example available online.
Several people have objected to this example because if you let the parent class know about the sub-class, you can fix this issue. That's true if there are a small enough number of sub-classes and if the parent knows about them all. But the original question was about making an API which someone else will write sub-classes for. In that case, you generally can't update the parent implementation to be compatible with the sub-classes.
Bonus
Comparator is also interesting because it works around the issue of implementing equals() correctly. Better yet, it follows a pattern for fixing this type of inheritance issue: the Strategy design pattern. The Typeclasses that Haskell and Scala people get excited about are also the Strategy pattern. Inheritance isn't bad or wrong, it's just tricky. For further reading, check out Philip Wadler's paper How to make ad-hoc polymorphism less ad hoc