August 20, 2009

Building equals(), hashCode(), compareTo() and toString() with ease

Implementing equals(), hashCode(), compareTo() and toString() is a common task for java programmers. If you're unfamiliar with this topic, check out Joshua Blochs best practices on how to override hashCode().
Modern IDEs like Eclipse offer commands to auto-generate these methods for particular classes. While this is ok in terms of time saving, the generated code is verbose and therefor hardly maintainable.



Let's take a look at what Eclipse Galileo does with an example class Customer containing a few members:
public class Customer {
private String firstName;
private String lastName;
private Date birthday;
private String street;
private String city;
private String zipcode;

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((birthday == null) ? 0 : birthday.hashCode());
result = prime * result + ((city == null) ? 0 : city.hashCode());
result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
result = prime * result + ((street == null) ? 0 : street.hashCode());
result = prime * result + ((zipcode == null) ? 0 : zipcode.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Customer other = (Customer) obj;
if (birthday == null) {
if (other.birthday != null) {
return false;
}
}
else if (!birthday.equals(other.birthday)) {
return false;
}
if (city == null) {
if (other.city != null) {
return false;
}
}
else if (!city.equals(other.city)) {
return false;
}
if (firstName == null) {
if (other.firstName != null) {
return false;
}
}
else if (!firstName.equals(other.firstName)) {
return false;
}
if (lastName == null) {
if (other.lastName != null) {
return false;
}
}
else if (!lastName.equals(other.lastName)) {
return false;
}
if (street == null) {
if (other.street != null) {
return false;
}
}
else if (!street.equals(other.street)) {
return false;
}
if (zipcode == null) {
if (other.zipcode != null) {
return false;
}
}
else if (!zipcode.equals(other.zipcode)) {
return false;
}
return true;
}
}

The methods are generated by using the Eclipse Galileo command Menu -> Source -> Generate equals() and hashCode(). As you can see the generated equals() method is damn long. At first glance you can hardly see which members are included in the equals-logic. What happens if you add another member to the class which has to be involved into equals/hashcode? I for myself would delete and re-generate the methods. But I'm no fan of such boiler plate code so let me introduce another approach to implement these methods.

Instead of letting the IDE generate those methods I prefer to use the utility classes from Apache Commons Lang. The library contains convient classes like EqualsBuilder, HashCodeBuilder, CompareToBuilder and ToStringBuilder to build the appropriate methods.
Let's see how the above example would look like using the apache commons:
import java.util.Date;

import org.apache.commons.lang.builder.CompareToBuilder;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;

public class Customer implements Comparable<Customer> {
private String firstName;
private String lastName;
private Date birthday;
private String street;
private String city;
private String zipcode;

@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(firstName)
.append(lastName)
.append(birthday)
.append(street)
.append(city)
.append(zipcode)
.toHashCode();
}

@Override
public boolean equals(Object obj) {
if (obj == null) { 
return false; 
}
if (obj == this) { 
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
Customer rhs = (Customer) obj;
return new EqualsBuilder()
.append(firstName, rhs.firstName)
.append(lastName, rhs.lastName)
.append(birthday, rhs.birthday)
.append(street, rhs.street)
.append(city, rhs.city)
.append(zipcode, rhs.zipcode)
.isEquals();
}

@Override
public String toString() {
return new ToStringBuilder(this)
.append("firstName", firstName)
.append("lastName", lastName)
.append("birthday", birthday)
.append("street", street)
.append("city", city)
.append("zipcode", zipcode)
.toString();
}

public int compareTo(Customer rhs) {
return new CompareToBuilder()
.append(firstName, rhs.firstName)
.append(lastName, rhs.lastName)
.append(birthday, rhs.birthday)
.append(street, rhs.street)
.append(city, rhs.city)
.append(zipcode, rhs.zipcode)
.toComparison();
}
}

The implementation is self-explanatory. At first glance you can see which members are involved into the method. Also you can easily add or remove members from these methods if requirements are changing.

See also:

5 comments:

Developer Dude said...

I use the Commons equals builder and hash code builder, but I do not compare for an instance of the base class. Here is why: let's say you have the base class Shape, and you have a subclass Circle and a subclass Square, both extending Shape. Does a Circle equal a Square? No, not in most circumstances.

Granted, you are checking for inequality, and that would probably work in most cases, but almost always I check using the following logic:

if (rhs_.getClass() != getClass()) return false;
// don't compare by interface or base class, compare by runtime class.

This is more explicit and IMO more readable.

Sometimes I also compare empty lists contained in the object to nulls because in some use cases, for my purposes, they are equal.

But yes, Apache Commons builder classes are IMO much cleaner and probably better tested than the ugly boiler plate Eclipse generates.

Ashitkin said...

you didn't cover a couple of important things -
ToStringStyle class, object pooling while using these classes and probable performance issues, especially for proxied objects>you need to preload fields before comparison, although actually you could finish on the very first statement, like this==that.

By the way:
you didn't override equals properly. Also it's a good practice to put ==this and ==null before instanceof or getClass, because they are less expensive operations.

FYI -Intellij IDEA generates toString with toStringBuilder

P.S. i've seen a couple of times things like
if (true==true) return true
you also fan of such style? ;-)

Benjamin Winterberg said...

Thanks for your input. I apologize that I wasn't 100% correct on implementing equals() in this example. Shame on me! :)
I've corrected the sample code with your suggestions.

breun said...

And what about using Project Lombok? http://projectlombok.org/

Ned said...

You don't mention whether these properties of Customer are mutable. They are not final, so that would be the first hint. The problem with using mutables in hashCode() is that if the object is placed in a collection, then an attribute is altered, you will no longer find the object using contains(object). Check out Josh Bloch's recommendations against using mutables.

Also, not 100% sure but I think your comparison of concrete types will still allow for scenarios that break symmetry or transitivity. Josh Block recommends using a "canEqual" method and claims it's the only way to completely satisfy the contract for polymorphic collections.