Thursday, September 17, 2009

Respect the Equals (and hashCode)

Another bug this week.

In just about every project I work on, one of the dozens of domain objects will have a bad, incomplete, or just plain problematic equals() or hashCode() method. The bug this time was in the equals() method. The related bug was noticed because multiple instances of what looked like the same object were allowed into a HashSet attached to another object which was then persisted (or at least attempted to be persisted) to the database via Hibernate. The resulting error during the execution of the Hibernate create() method was
org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session which can be a little hard to decipher what the actual problem is.

The equals and hashCode methods lie at the intersection of so many competing attributes. They are simultaneously:
  • Vitally Important (see Chapter 3 of Effective Java by Josh Bloch)
  • Error Prone (fields used in hashCode should be the same as those used in equals; it's easy to forget about null checking; == vs .equals() for fields, etc...)
  • Interesting (what does it mean for two Person objects to be equal? If using names, what about variations, nicknames, etc.)
  • Mind-numbing (a lot of repetitive code, nullchecks, etc.)
  • Confusing (what to do about superclass? what if different fields can be used to determine that two Person objects are supposed to be the same (i.e. SSN vs firstName+lastName+DOB+zipcode?)
  • Static (as a class evolves the methods need to constantly be evaluated to see if they remain current).
The equals and hashCode methods are usually so boring that the IDE has a built-in feature that will generate both of them automatically. (In Eclipse there's a menu option "Generate hashCode() and equals()..." under the Source menu; the user just clicks a checkbox list of which fields to include.) This feature is pretty slick, but in this case it indirectly led to the defect. Here's how it happened (root cause analysis):
  1. The class (Author.java) has two fields important for equals & hashCode; they are a primitive long "authorID" and a String "nomDePlume"
  2. We used Eclipse to automatically generate equals() and hashCode(). It worked perfectly.
  3. Sometime later the primitive long "authorID" was changed to an object using the wrapper class Long "authorID"
  4. The equals() method was still comparing the authorID's as if it was a primitive, i.e., == or !=
After making the change at step 3, we should have regenerated the equals() and hashCode().

So what can we do to make life easier so we don't make these common mistakes? Well, there's a good solution that I briefly touched upon in an earlier post about the Apache Commons-Lang EqualsBuilder. The bug this week didn't use it. If it had there wouldn't have been a problem when the field was changed from a primitive to a wrapper object.

Exercise for the reader: Compare the code below (which uses EqualsBuilder) to code that gets automatically generated by your favorite IDE. Which is more readable? Which would be easier to change as your domain model classes evolve?
public class Author {
private Long authorID;
private String nomDePlume;
// other fields, methods, and details omitted [...]
public boolean equals(Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (obj.getClass() != getClass()) return false;
EligIdId rhs = (EligIdId) obj;
return new EqualsBuilder()
.append(authorID, rhs.authorID)
.append(nomDePlume, rhs.nomDePlume)
.isEquals();
}
// TODO: Remember to implement hashCode()
}

0 comments:

Post a Comment