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.

5 comments:

zhaokeqiang said...
This comment has been removed by the author.
zhaokeqiang said...

Your equals method will encounter problem when compare following object:

ColorPoint cp1 = new ColorPoint(1, 2);
ColorPoint cp2 = new ColorPoint(1, 2);
System.out.println( cp1.equals(cp2) ); //should be true while the real output is false

The problem lies in the last line of equals method.
return super.equals( o ) && cp.color == color;

Finally, this will call the equals of Object. Let 's see how Object#equals deal:
"
Compares the argument to the receiver, and answers true
* if they represent the same object using a class
* specific comparison. The implementation in Object answers
* true only if the argument is the exact same object as the
* receiver (==).
"
So, the equals method of Object will simply return whether the receiver and argument point to the same object.

If the equals method of Object just simply does nothing, then your equals method will work well.

Unknown said...

Hi, Ke Qiang.
ColorPoint's equals method does not call Object's equals method. It will call Point's equals method. And Point's equals does not call Object's equals method. ColorPoint extends Point.

zhaokeqiang said...

I had written the equals method of Point following that in ColorPoint.

I think you may give some instructions on implementation of equals of Point. Is it the same with ColorPoint or what rules it should follow?

Unknown said...

The base class Point does not call Object's equals method. And I have added the code for my Point implementation in this post. Thanks for your input.