Overriding .equals method for inheriting classes in Java
According to item #10 in Effective Java, when you override equals method for classes, you should adhere to its general contract as follows:
- Reflexive
- Symmetric
- Transitive
- Consistent
- Always false when compared to null.
Of these properties, the trickiest property could be attaining transitivity which means if :
- A is equal to B, and
- B is equal to C, then
- A is equal to C
We can easily violate this property when using inheritance, and I will demonstrate how by using the same example as in Effective Java book.
Suppose you have a class Point as follows:
@AllArgsConstructor
@Getter
public class Point {
private int x;
private int y;
}
And class ColorPoint as follows:
@Getter
public class ColorPoint extends Point{
private String color;
public ColorPoint(int x, int y, String color) {
super(x, y);
this.color = color;
}
}
Note here that ColorPoint is a Point. Then a Point instance can be compared to a ColorPoint instance and vice-versa.
Lets add equals method for Point class:
@Override
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
}
And now comes the interesting part. Let’s try overriding ColorPoint class with the first possible implementation, which is as follows:
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color.equals(color);
}
The above method instanceof in ColorPoint class will return true if the object is an instance of either a ColorPoint or a Point class.
Then what happens when we compare a Point to a Color Point?
public static void main(String[] args) {
Point point = new Point(1, 2);
ColorPoint colorPoint = new ColorPoint(1, 2, "red");
System.out.println(point.equals(colorPoint));
System.out.println(colorPoint.equals(point));
}Output:
true
false
We first compare point to colorPoint, then colorPoint to point. We receive different outputs, because colorPoint’s equals method expects a ColorPoint but receive a Point. To remind you a ColorPoint is a Point but a Point is not a ColorPoint. Here we are breaking one of the properties of equals method. That is, if A equal to B, then B is equal to A.
How do we resolve this issue? Let’s do color-blind comparison when comparing mixed classes (i.e. comparing Point to ColorPoint)
Here is the new equals implementation of ColorPoint:
@Override
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);
/* If o is a ColorPoint; do a full comparison */
return super.equals(o) && ((ColorPoint) o).color.equals(color);
}
Now lets try it out:
public static void main(String[] args) {
Point point = new Point(1, 2);
ColorPoint colorPoint = new ColorPoint(1, 2, "red");
System.out.println(point.equals(colorPoint));
System.out.println(colorPoint.equals(point));
}Output:
true
true
Beautiful! Now these instances are equal to each other. But what if we try comparing the same point to another colorPoint.
public static void main(String[] args) {
Point point = new Point(1, 2);
ColorPoint redPoint = new ColorPoint(1, 2, "red");
ColorPoint bluePoint = new ColorPoint(1, 2, "blue");
System.out.println(point.equals(redPoint));
System.out.println(point.equals(bluePoint));
System.out.println(redPoint.equals(bluePoint));
}Output:
true
true
false
Here we violate rule of transitivity. Since “point” is equal to “redPoint” and “point” is equal to “bluePoint” but “redPoint” is not equal to “bluePoint”.
Solution: favor Composition over Inheritance!
Instead of inheriting from Point, let ColorPoint be an independent class which is contains a Point field. E.g.:
@Getter
public class ColorPoint{
private String color;
private Point point;
public ColorPoint(int x, int y, String color) {
this.point = new Point(x, y);
this.color = color;
}
public Point asPoint() {
return point;
}
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
Notice that we create a new Point object in the constructor and we have an asPoint() method to return the point object and the inheritance is removed. This way, comparing a Point to a ColorPoint is always false since ColorPoint no longer extends Point. We compare ColorPoint to ColorPoint instances and Point to Point instance only. E.g.:
public static void main(String[] args) {
Point point1 = new Point(1, 2);
Point point2 = new Point(1, 2);
Point point3 = new Point(1, 3);
ColorPoint redPoint = new ColorPoint(1, 2, "red");
ColorPoint bluePoint = new ColorPoint(1, 2, "blue");
ColorPoint anotherBluePoint = new ColorPoint(1, 2, "blue");
// comparing Point to Point
System.out.println(point1.equals(point2)); // true
System.out.println(point2.equals(point1)); // true
System.out.println(point1.equals(point3)); // false
// comparing Point to ColorPoint
System.out.println(point1.equals(redPoint)); // false
// comparing Point to ColorPoint as Point
System.out.println(point1.equals(redPoint.asPoint())); // true
//comparing ColorPoint to ColorPoint
System.out.println(bluePoint.equals(redPoint)); // false
//comparing ColorPoint to ColorPoint
System.out.println(bluePoint.equals(anotherBluePoint)); // true
}
Now we can safely and accurately override equals method without violating the transitivity property.
Thank you for reading! You can find the code in my Github repository.