Sunday, February 24, 2008

It is not true that there is simply no way to extend an instantiable class and add an aspect while preserving the equals contract.

In Effective Java, Item 7 is "Obey the general contract when overriding equals". There is one sentence "There is simply no way to extend an instantiable class and add an aspect while preserving the equals contract." Josh uses the following classes to illustrate Transitivity. The following text is excerpted from the book.
The Point class.

public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
}
... // Remainder omitted
}

The ColorPoint class.

public class ColorPoint extends Point {
private Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}

//Broken - violates transitivity.
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
// If o is a normal Point, do a color-blind
// comparison
if (!(o instanceof ColorPoint))
return o.equals(this);
// o is a ColorPoint; do a full comparison
ColorPoint cp = (ColorPoint)o;
return super.equals(o) && cp.color == color;
}

... // Remainder omitted
}


This approach does provide symmetry, but at the expense of transitivity:

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

At this point, p1.equals(p2) and p2.equals(p3) return true, while p1.equals(p3) returns false, a clear violation of transitivity. The first two comparisons are “color-blind,” while the third takes color into account.

So what's the solution? It turns out that this is a fundamental problem of equivalence relations in object-oriented languages. There is simply no way to extend an instantiable class and add an aspect while preserving the equals contract.

Then Josh gives a a workaround where ColorPoint does not extend Point. I thinks that we can allow ColorPoint to extend Point and preserve the equals contract. Here is my solution.
The Point class.

public class Point {
private final int x;
private final int y;

public Point( int x, int y ) {
this.x = x;
this.y = y;
}

public boolean equals( Object o ) {
if( this == o )
return true;
if( !(this.getClass() == o.getClass()) )
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}

... // Remainder omitted

}

The ColorPont class.

public class ColorPoint extends Point {
private Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}

public boolean equals( Object o ) {
if( this == o )
return true;
if( o == null )
return false;
if( !(this.getClass() == o.getClass()) )
return false;
ColorPoint cp = (ColorPoint) o;
return super.equals( o ) && cp.color == color;
}
... // Remainder omitted
}

First, equals method use the == operator to check if the argument is a reference to this object. Secondly, it check whether the argument and this object are of the same type. I have tested this piece of code. It works. But I think that there may be some situations I have not taken into account.

Any comments are welcome.