5 Rules for overriding equal method — Effective Java Notes

When we create a new class, most time we need to override the equal method. The book lists 5 rules.

  1. Reflexive: x.equals(x) == true , an object must equal to itself.
  2. Symmetry: if(x.equals(y)==true) then y.equals(x) == true.
  3. Transitive: if x.equals(y) and y.equals(z); then x.equals(z)
  4. Consistent: if x.equals(y)==true and no value is modified, then it’s always true for every call
  5. For any non-null object x, x.equals(null)==false

For consistent rule, suppose two objects equal. If the objects are immutable, then they should be always equal. But if the object is mutable, then we might modify it, so even two objects are equal before, they might not equal after some steps. When design the class, try to decide if the object is mutable or immutable base on the later use scenario. The book suggests that do not “write the equal method depends on the unreliable resources”.

Transitivity

Q: How could we violate this rule?

A: One class extends another class.

Example:

We have a Point class with two fields x and y. Then a new class ColorPoint which extends the Point and add a new value component color.

The point class

Now we have a new class ColorPoint

Then the next question is how to add the equal method? You may have your own answer. The following code block is my solution.

@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPointChild)) {
return false;
}
return super.equals(obj) && this.color.equals(((ColorPointChild) obj).color);
}

At first glance, it seems fine. If we have two ColorPointChild object, we are sure they will meet all requirements: symmetry, transitivity, consistent. That’s true. But wait, the ColorPointChild class is a little differnt — it is a child class.

We know that if A extends B , then a instanceOf B will return true, while b instanceOf A will return false.

Suppose we have two objects: a Point object and a ColorPointChild object. If you run the following unit test, will it pass?

@Test
public void testSymmetryPoint(){
Point p1 = new Point(1,2);
ColorPointChild cpc = new ColorPointChild(1,2,Color.ORANGE);
Assert.assertTrue(p1.equals(cpc));
Assert.assertTrue(cpc.equals(p1));
}

This test failed on the second assert. When we call cpc.equals(p1) method, it will first check if p1 is the instance of ColorPointChild. It is not. So failed.

OK, do we have better method to change it. Based on the previous failed test case, we can make a special case if the object is a Point Object.

@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
// if it is a Point object, we ignore the color
if (!(obj instanceof ColorPointChildSpecial)){
return obj.equals(this);
}

return super.equals(obj) && this.color.equals(((ColorPointChild) obj).color);
}

Then run the following unit test:

Luckily, it passed the symmetry test. But failed on the transitivity test.

“There is no way to extend an instantiable class and add a value component while preserving the equals contract, unless you are willing to forgo the benefits of object-oriented abstraction.”

Q:How to solve this problem?

A: Don’t extend the instantiable class if you want to override the equal method. In other word:

“Favor composition over inheritance.”

And all codes are available on github.

Reference: Effective JAVA