Skip to main content
added 3 characters in body
Source Link
kaya3
  • 20.8k
  • 46
  • 126
class Point {
    // ...
    
    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result = (this.getX() == that.getX() && this.getY() == that.getY()
                    && this.getClass().equals(that.getClass()));
        }
        return result;
    }
}

class ColoredPoint extends Point {
    // ...
    
    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }
}
class Point {
    // ...
    
    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result = (this.getX() == that.getX() && this.getY() == that.getY()
                    && this.getClass().equals(that.getClass()));
        }
        return result;
    }

class ColoredPoint extends Point {
    // ...
    
    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }
}
class Point {
    // ...
    
    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result = (this.getX() == that.getX() && this.getY() == that.getY()
                    && this.getClass().equals(that.getClass()));
        }
        return result;
    }
}

class ColoredPoint extends Point {
    // ...
    
    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }
}
Source Link
kaya3
  • 20.8k
  • 46
  • 126

The ways shown in the post to fix this require class ID or runtime type checks (like instanceof), and even then they break substitutability (e.g. a value of ColoredPoint cannot be passed where Point is expected as it behaves differently).

I think this is a misinterpretation. The linked article does provide sound implementations of equals for the two classes Point and ColoredPoint:

class Point {
    // ...
    
    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result = (this.getX() == that.getX() && this.getY() == that.getY()
                    && this.getClass().equals(that.getClass()));
        }
        return result;
    }

class ColoredPoint extends Point {
    // ...
    
    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }
}

The linked article calls this "technically valid, but unsatisfying". But it indeed defines an equivalence relation, so the implementation is sound. And I would argue that it is not really "unsatisfying".

To be clear, this implementation does not break substitutability ─ yes, new ColoredPoint(1, 2, "red") behaves differently to new Point(1, 2) when using the .equals() method ─ but only because it is supposed to behave differently; they are equal to different things, so of course their .equals() methods give different answers. This doesn't violate substitutability any more than any subclass overriding any method, changing its behaviour, does. (That is, you're free to think this is a problem, and it kind of is, but then you must accept that inheritance itself is a problem.)

The only trouble with the above implementation is that it's not completely extensible: you cannot define a new subclass of Point such that new MyPoint(1, 2).equals(new Point(1, 2)) is true without breaking symmetry. This is only a problem in a quite specific set of circumstances:

  • Your language allows inheritance, particularly open inheritance such that first-party classes may be extended by third-party subclasses,
  • You want an equivalence relation which is extensible to third-party subclasses,
  • You want those third-party classes to be able to choose how they are related to first-party classes or other third-party classes.

Even if the first two hold, the third is quite an unusual requirement, and (in my opinion) indicates a design problem in the user's code. The problem is that if Alice and Bob both extend this class, which of them gets to decide whether new AlicePoint(1, 2) is equal to new BobPoint(1, 2)?

Suppose Alice says that an AlicePoint equals any point with the same x, y coordinates, and Bob says a BobPoint only equals another BobPoint. Whose definition should win? Fundamentally, by promising this kind of extensibility to your users, the promise you make to Alice (that she can decide which objects hers are equal to) is incompatible with the promise you make to Bob (that he can decide the same for his objects). That is, the promise you want to make is inconsistent with itself.

The only way you can make an equivalence relation extensible, therefore, is by saying that two objects can only be equal if both classes are defined in such a way as to allow them to equal objects of the other class; i.e. both classes explicitly cooperate in order to have equal objects. Then it is up to the implementor of those two classes to ensure that they properly implement an equivalence relation. Then across module boundaries, equals necessarily returns false; and that's fine.

Note that this is a non-issue if your language only has closed inheritance (i.e. users can only extend their own classes). Closed inheritance still allows for extensibility, just through other means (e.g. composition, generics, callback functions, etc.).