Effective implementation of equals method
Venkat Subramaniam
[email protected]
http://www.durasoftcorp.com
Abstract
"Effective Java Programming Language Guide" by Joshua Bloch presents fifty seven items for a Java developer to keep in mind while developing quality code. There are a number of very interesting suggestions and gochas raised in it, including issues with cloning, problems with automatic garbage collection, benefits and usage of immutable objects, when to avoid inheritance, to mention a few. This is a very good book to read and we strongly recommend it to any serious Java developer. In this article, we bring to you Johua's discussions about the effective implementation of the equals method (Item #7). While we agree with the problems he has raised, we don't agree with his final conclusion. We go further to present how the concerns could be easily addressed.
The equals method
Objects may be compared by value or for their identity. To compare the identity, you simply use the == operator. If you want to compare objects by value, you check if two objects are equal based on their current state or values of their attributes/data. The Object base class provides the equal method, which by default returns true if the argument reference is identical to the reference on which equals is invoked. Traditionally, one would override the equals method to provide meaningful comparison of two objects.
Rules to follow in Overriding equals
The Java language specifications 1, 2, 3 describes the equals method as a method that indicates if an object is equals to another and it implements the equalance relation. The equals method should be reflexive, symmetric, transitive, consistent and should return a false if the reference argument is null.
  • Reflexivity requires that for any reference x, x.equals(x) should return true.
  • Symmetry requires that for any referneces x and y, x.equals(y) should return true if and only if y.equals(x) returns true. 
  • Transitivity requires that for any references x, y and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • Consistency requires that repeated calls to x.equals(y) should consistenly return a true or consistently return a false, if no data /state has changed in either object.
  • Non-nullity requires that the for any non-null reference x, x.equals(null) should return false.
Problem with maintaining Symmetry and Transitivity

Here we discuss the issues with symmetry and transitivity with examples presented in Effective Java1. Let us consider a class Point as shown below:

public class Point
{
	private final int x;
	private final int y;
	public Point(int px, int py)
	{
		x = px;
		y = py;
	}
	public boolean equals(Object o)
	{
		if (!(o instanceof Point))
			return false;
		Point p = (Point) o;
		return p.x == x && p.y == y;
	}
}
	
Now consider a class ColorPoint that extends Point as shown below:
public class ColorPoint extends Point
{
	private Color color;
	
	public ColorPoint(int px, int py, Color clr)
	{
		super(px, py);
		color = clr;
	}
	
	public boolean equals(Object o)
	{
		if (!(o instanceof ColorPoint))
			return false;
		ColorPoint cp = (ColorPoint) o;
		//return super.equals(o) && cp.color == color;
		// The above commented line is from1. We have modified it
		// as follows:
		return super.equals(o) && color.equals(cp.color);
	}
}
	
Now, if we try the following scenario: 
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.red);
p.equals(cp) returns a true, however, cp.equals(p) returns a false. This fails the symmetry. One way to fix this as suggested in1 is:
	// ColorPoint's equals method
	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);
		
		// o is a ColorPoint; do a full compoarison
		ColorPoint cp = (ColorPoint) o;
		//return super.equals(o) && cp.color == color;
		// The above commented line is from1. We have modified it
		// as follows:
		return super.equals(o) && color.equals(cp.color);
	}
	
While this solves the symmetry problem, it fails trasitivity. Consider:
	ColorPoint p1 = new ColorPoint(1, 2, Color.red);
	Point p2 = new Point(1, 2);
	ColorPoint p3 = new ColorPoint(1, 2, Color.blue);
	
Now, while p1.equals(p2) returns a true and p2.equals(p3) returns a true, p1.equals(p3) returns a false. The first two performed a color-blind comparison, while the third considered the color in the comparison.
Concerns and recommendations from Effective Java
The following are the recommendations from "Effective Java."1
"So what's the solution? It turns out that this is a fundamental problem of equivalence relations in object-oriented languages. There is simply no way to extend an instantiable class and add an aspect while preserving the equals contract. There is, however, a fine workaround. Follow the advice of Item 14, 'Favor composition over inheritance.' Instead of having ColorPoint extend Point, give ColorPoint a private Point field and a public view method (Item 4) that returns the point at the same position as this color point:"
Easy fix!
While I separately agree with the argument of using composition over inheritance, in this particular problem, there is actually an easier work around. In the equals method, we want to compare further only if the objects are of the same type. If the objects are of different types, we can decide to return a false. Now, how do we decide if the objects are of different types? In the original implementation of ColorPoint's equals method we tried this, and failed. It failed symmetry. Let's try again. Let's revisit the equals of the ColorPoint and Point:
// Point's equals method
public boolean equals(Object o)
{
	if (!(o instanceof Point))
		return false;
	Point p = (Point) o;
	return p.x == x && p.y == y;
}
// ColorPoint's equals method
public boolean equals(Object o)
{
	if (!(o instanceof ColorPoint))
		return false;
	ColorPoint cp = (ColorPoint) o;
	return super.equals(o) && color.equals(cp.color);
}
	
The ColorPoint checks to see if the given object is also a ColorPoint. If not, it returns false. This is good. However, to the Point's equals method, if we send a ColorPoint as an argument, the instanceof will identify this object as an instance of the Point class and so, it will perform a color-blind comparison. This is why symmetry was failing. This problem, however, can be eliminated as follows:
//Point's equals method
public boolean equals(Object o)
{
	if (!(o.getClass() == getClass()))
		return false;
	Point p = (Point) o;
	return p.x == x && p.y == y;	
}

//ColorPoints' equals method
public boolean equals(Object o)
{
	if (!(o.getClass() == getClass()))
		return false;
	ColorPoint cp = (ColorPoint) o;
	return super.equals(o) && color.equals(cp.color);
}
Each class can check to see if the object being pass in as argument is exactly the same type as the one on which equals is called. This way, if p.equals(cp) is called, where p is a reference to a Point object and cp to ColorPoint, the equals will return a false. Similarly, cp.equals(p) will return a false. However, cp1.equals(cp2), where both references cp1 and cp2 refer to object of ColorPoint, will return a true if the point values are equal and the colors are the same as well.
Conclusion
While polymorphism and substituitability (and overriding) are concepts that provide great extensibility in a system, we have to be very careful in implementing these concepts. It requires quite a bit of insight and analysis to get it done correct. The issues presented in "Effective Java"1 are not only interesting, but also very important. If you have not read through it, I hope you will soon. We will present a few other issues from that book in future issues and also discuss our opinion on what we may agree and some that we may not!
References
  1. Joshua Block, Effective Java Programming Language Guide. Addison-Wesley, Boston, MA, 2001. ISBN:0-201-31005-8.
  2. Ken Arnold, James Gosling, David Holmes, The Java Programming Language, Third Edition. Addison-Wesley, Boston, MA, 2000. ISBN:0-201-70433-1.
  3. Sun Microsystems, http://java.sun.com/j2se/1.4/docs/api/index.html, Method description for Object's equals method.