Monday, January 18, 2010

Java equals() Method

According to the Javadocs, the equals method must be:
  • reflexive: for any non-null reference value x, x.equals(x) should return true.
  • symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • 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) should return true.
  • 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 used in equals comparisons on the objects is modified.
  • return false for x.equals(null), for any non-null reference value x.
In other words, the following tests must pass:
@Test
public void testEquals() {

  // reflexive
  assertTrue(x.equals(x));

  // symmetric
  assertTrue(x.equals(y) == y.equals(x));

  // transitive
  if (x.equals(y) && y.equals(z)) {
    assertTrue(x.equals(z));
  }

  // consistent
  assertTrue(x.equals(y) == x.equals(y));

  // null check
  assertFalse(x.equals(null));
}
Implementing Equals(): Approach 1
The steps are:
  1. Check reference equality (good optimisation step)
  2. Check correct class type using instanceof
  3. Cast to correct type
  4. Compare objects
@Override
public boolean equals(Object obj) {

  // check reference equality
  if (this == obj) {
    return true;
  }

  // check correct arg type
  if (!(obj instanceof Rectangle)) {
    return false;
  }

  // cast the object to the correct type
  Rectangle other = (Rectangle) obj;

  // compare fields
  return other.getLength() == length &&
         other.getWidth()  == width;
}
But what happens if you extend this class by adding another field? For example, a Rectangle has a length and width. But what if we create a Cuboid that extends Rectangle with an additional depth attribute?

The problem occurs when you mix objects of Rectangle and Cuboid because Rectangle instanceof Cuboid returns false, whereas Cuboid instanceof Rectangle returns true. This breaks the symmetric rule because rectangle.equals(cuboid) is true, but cuboid.equals(rectangle) is false. In order to preserve symmetry, we can change our equals method as follows:

Implementing Equals(): Approach 2

  1. Check if null
  2. Check correct class type using getClass
  3. Cast to correct type
  4. Compare objects
@Override
public boolean equals(Object obj) {

  // check null
  if (obj == null) {
      return false;
  }

  //check correct type
  if(getClass() != obj.getClass()) {
      return false;
  }
  // cast the object to the correct type
  Rectangle other = (Rectangle) obj;

  // compare fields
  return other.getLength() == length &&
         other.getWidth()  == width;
}
The getClass method will always return false if the parameter is not the exact same type as the object class.

In most cases, you should use Approach 2 so that you obey the equals contract, unless you want to compare subclasses with their base types.

No comments:

Post a Comment