Effective implementation of equals method |
---|
|
|
|
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.
|
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
|