Tuesday, January 26, 2016

Effective Java - Item 8 : Obey the general contract when overriding equals

In this item and the following 4 items the author talks about the non-final methods of the Object class - equals, hashcode, toString, clone. In particular the best ways to override them: when? and how?

The default implementation of the equals() method uses the "==" relation to compare two objects. Java docs states that "for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true)."

Let us look at the following scenario. I created a custom String class MyString which accepts a String as a parameter. I also created 2 instances of this MyString and passed the same string literal "hello" and compared them using the equals method.


What do you think will be the output?

From what we have learnt about the equals method it is obvious that the output will be false.
Then we might as well have used the "==" operator to check their equivalence.

But what if we wanted to check the logical equivalence of the two strings. In that case both of them should be equal. This is where the equals method comes into picture. You can override the equals method and give your own implementation to check the logical equivalence of the MyString objects.

I gave this example of providing a custom wrapper around the String class just to illustrate the application of overriding the equals method.

So, now that we learnt in what scenario we would want to override the equals method the next step is to learn how to do it.
The author says that there is a written contract with a set of four rules and one must adhere to it when overriding the equals method.

1.     Reflexivity

The first requirement says that the object whose equal method you are overriding must maintain the reflexive property, i.e. the object must be equal to itself.
Considering the MyString class from our previous example, the following code must output true according to this requirement.
1
2
MyString a = new MyString("Hello");
System.out.println(a.equals(a));

     2.     Symmetry

The second requirement says that for two non-null references the equal method of either of the two references should return true iff the equals method of both the references return true. To better understand what this exactly means let us build upon our MyString class and consider that we override the equals method to make it case-insensitive. The following code should output true.
1
2
3
MyString a = new MyString("Hello");
MyString b = new MyString("hello");
System.out.print(a.equals(b));
But what about in this scenario?
1
2
3
MyString a = new MyString("Hello");
String b = new String("hello");
System.out.print(a.equals(b));
It should output false else it would violate the symmetric property, because b.equals(a) will be false.
And why is that so, you ask?
Think about it, we are only overriding the equals method of MyString class. The String class does not have a clue that we are trying to compare two strings irrespective of their case.

    3.     Transitivity

The third requirement says the equals method should uphold the transitive property i.e. if any two one object is equal to a second object and the second is equal to a third object then the first object should also be equal to the third object. If the first object is not equal to the third then the equivalence of the first and the second should return false.
The author says that this requirement can usually be violated when an instantiable class is extended and the sub-class adds a new property.
So let us extend our MyString class and add a color property.
1
2
3
4
5
6
7
class MyColorString extends MyString {
    private final Color strColor;
    public MyColorString(String string, Color color) {
      super(string);
      this.strColor = color;
    }
}
Now consider the following three objects
1
2
3
MyColorString str1 = new MyColorString("hello", Color.RED);
MyString str2 = new MyString("hello");
MyColorString str3 = new MyColorString("hello", Color.BLUE);
These three strings can be compared using the equals method since it should use the instanceof operator (we will see this later  in the conclusion) and str1 is an instance of the parent class MyString.
So if we do a comparison of the first 2 instances and second and third as:
1
2
str1.equals(str2);
str2.equals(str3);
Both of them will return true as they do not consider the color property of the sub-class. But str1.equals(str3) will return false as now the color property comes into account. This clearly violates the transitivity contract.
A workaround to this problem could be to use getClass() method instead of instanceof operator in the equals method to ensure equating objects of the same implementation class.
But is this a good solution? Well, according to the author it is not since it violates the Liskov substitution principle and that is an entire topic in itself. The author says that: "There is no way to extend and instantiable class and add a value component while preserving the equals contract". 

    4.     Consistency

The condition of this rule is quite simple. It says that the equality of any two objects should be consistent i.e. their equality should hold true unless one of them is modified. Here we need to consider if the object we are dealing with is mutable or immutable, because mutable objects can be modified and so they can be equal to different objects at different times but that is not the case with immutable objects.
The author brings up an important and interesting question here, whether to make a class mutable or immutable? He asks the reader to think hard about this question. Infact he has an item dedicated to this question further in the book.
Another important point we need to keep in mind when overriding the equals method is not to depend on unreliable sources such as network access. A good example of this is the equals method of java.net.URL

    5.     Non-nullity

The name of this rule is actually devised by the author. As the name suggests according to this requirement for any object x the following should not return true: x.equals(null) .

To conclude you can use the following as a recipe for a well implemented equals method:


So here is an implementation of our MyString class with the equals method


No comments:

Post a Comment