Overriding .equals method for inheriting classes in Java

Hussein Maziad
4 min readFeb 13, 2022

--

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 :

  1. A is equal to B, and
  2. B is equal to C, then
  3. 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.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Hussein Maziad
Hussein Maziad

No responses yet