Friday, January 13, 2012

Overriding equals()


Maximum of us know how to override equals() and what is the use of it.  It’s easy, but the most important part where we fail is to understand when to override. We tend to confuse whether to override an equals() or not.  It’s better to avoid equals() whenever possible.  But how come we would know that ‘whenever’. Let me list out the ‘whenever’ conditions where we should avoid overriding equals().
·        When you know that each instance of the class is unique.  For e.g. Thread, each instance has to be unique as it represents active entity.  So equals() implemented in Object class is perfect for it.
·        You are not sure whether the objects should be logical equal.  For e.g. java.util.Random could have overridden equals(), but the designers must have not be sure that the client(the end programmer) would need to know that the produced random numbers from two different instances of Random are same or not.
·        Superclass has already overridden equals() and it fits the requirement with the subclass.  For e.g. Set overrides equals() and the same implementation is used for its subclasses like HashSet. 
·        It is a private class or a package-private class and it is sure that the equals() will never be called.  Though we can override the equals() here incase accidently equals get called.  In that case we can just throw a AssertionError() in the body of equals().
Hopefully by now it is clear as to when not to override equals().
Let us discuss on how to override equals(), rather we would discuss what are the points which should be considered while overriding equals().
·        Reflexive:  Mathematically it means, for any non-null reference value x, x.equals(x) must return true.  It’s hard to violate this rule.
·        Symmetric:  This means for any non-null reference values x and y, x.equals(y) must return true if and only if y.equals(x) returns true
Let us see how an equals() overriding can violate this rule.
public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        if (s == null) {
            throw new NullPointerException();
        }
        this.s = s;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof CaseInsensitiveString) {
            return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
        }
        if (obj instanceof String) {
            return s.equalsIgnoreCase((String) obj);
        }
        return true;
    }
}

Now if we try to run the below line of code,
CaseInsensitiveString c = new CaseInsensitiveString("Test");
      String s = "test";

      System.out.println("c.equals(s):" + c.equals(s));
System.out.println("s.equals(c):" + s.equals(c));

The result will be:
c.equals(s):true
s.equals(c):false

This clearly shows how it violates rule of symmetry.  This is because String class equals() is not aware of case-insensitive strings.  In order to make this symmetric we need to do something like this:

@Override
    public boolean equals(Object obj) {

return obj instanceof CaseInsensitiveString && ((CaseInsensitiveString) obj).s.equalsIgnoreCase(s);
    }
·        Transitive:  For any non-null reference values x, y and z if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) must return true.
Let us consider a case of a superclass and a subclass.  Superclass has some properties and overrides equals. Subclass adds some more properties and try to override equals()
public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) {
            return false;
        }
        Point p = (Point) obj;
        return p.x == x && p.y == y;
    }
}

public class ColourPoint extends Point {
    private final int colour;

    public ColourPoint(int x, int y, int colour) {
        super(x, y);
        this.colour = colour;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ColourPoint)) {
            return false;
        }
        return super.equals(obj) && ((ColourPoint) obj).colour == colour;
    }
}

When we do,
Point p = new Point(2, 3);
     ColourPoint cp = new ColourPoint(2, 3, 8);

     System.out.println("p.equals(cp):" + p.equals(cp));
System.out.println("cp.equals(p):" + cp.equals(p));

We will get,
p.equals(cp):true
cp.equals(p):false

This pathetically fails for even symmetry.  The reason is clear.  ColourPoint’s equals() is trying to call Point’s equals() which is returning true here, thus violating the rule.  Let us see another approach,
@Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) {
            return false;
        }
        if (!(obj instanceof ColourPoint)) {
            return obj.equals(this);
        }
        return super.equals(obj) && ((ColourPoint) obj).colour == colour;
    }
This overriding in ColourPoint will solve symmetry but not transitivity.  How??  This is how:
Point p1 = new Point(2, 3);
      ColourPoint cp1 = new ColourPoint(2, 3, 8);
      ColourPoint cp2 = new ColourPoint(2, 3, 9);

      System.out.println("cp1.equals(p1):" + cp1.equals(p1));
      System.out.println("p1.equals(cp2):" + p1.equals(cp2));
System.out.println("cp2.equals(cp1):" + cp2.equals(cp1));

The result would be,
cp1.equals(p1):true
p1.equals(cp2):true
cp2.equals(cp1):false

Sadly, 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 benfits of object oriented-programming. 
But there can be a workaround.  Instead of having ColourPoint extend Point keep a private Point field in ColourPoint.
public class ColourPoint {
    private final Integer colour;
    private final Point point;

    public Point asPoint() {
        return point;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ColourPoint)) {
            return false;
        }
        ColourPoint cp = (ColourPoint) obj;
        return cp.point.equals(point) && cp.colour.equals(colour);
    }
}
Note: java.sql.Timestamp extends java.util.Date and adds a nanoseconds fieldThe equals() implementation for Timestamp does violates symmetry.  It has disclaimer stating this.
·        Consistent:  For any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information is changed on objects.  Best way to keep up to this is to avoid writing equals() which depends on unreliable resources.
·        For any non-null reference x.x equals(null) must return false.
We can do something like this,
@Override
public boolean equals(Object obj) {
if(obj == null) {
          return false;
      }
      ...
}

            Or this,
      @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) {
            return false;
        }
        .......
    }
    
While executing this, it will cast the object  and can throw ClassCastException, but it won’t.  Because instanceof operator will return false if it throws exception.

Now some important points to be remembered while implementing equals().
1.       Use == operator to check if the argument is a reference to this object.  This is for performance optimization.
2.      Use the instanceof operator to check if the argument has the correct type.
3.      Cast the argument to the correct type.  No need to worry about ClassCastException, because it will be preceeded by instanceof so it has to be a success.
4.      For each “significant” field in the class, check if that field of the argument matches the corresponding field of this object.
(field == null ? obj.feild == null : field.equals(o.field))

5.      Follow the rules of symmetric, transitivity and consistency.

Some Points to remember:
·        Always override hashCode when you override equals()
·        Don’t substitute another type for Object in the equals declaration.
public boolean equals(MyClass obj)                                                                                                                                                  
·        Don’t try to exaggerate.  That means only checking fields equality will do the job; don’t try hard on other things which can lead you into trouble.

**Note: You can use org.apache.commons.lang.builder.EqualsBuilder for writing a good equals().  For more information you can see http://commons.apache.org/lang/api-2.5/org/apache/commons/lang/builder/EqualsBuilder.html


{Courtesy: Effective Java - Joshua Bloch} 

No comments:

Post a Comment