Implementing equals(), hashCode(), toString() & compareTo() using reflection

In my previous posts, I wrote about the various utility classes provided by Apache’s commons lang library for implementing commonly used methods (equals(), hashCode() etc.). All of these classes also provide the option of implementing these methods using reflection. That reduces your code for implementing any of these methods to just writing one line! For example, to implement toString() method using reflection, use the reflectionToString() static method of the ToStringBuilder class. Here’s the implementation of all the above methods using reflection in a sample class:

public class Account implements Comparable {
  private long id;
  private String firstName;
  private String lastName;
  private String emailAddress;
  private Date creationDate;

  public boolean equals(Object obj) {
    return EqualsBuilder.reflectionEquals(this, obj);
  }

  public int hashCode() {
    return HashCodeBuilder.reflectionHashCode(this);
  }

  public int compareTo(Account account) {
    return CompareToBuilder.reflectionCompare(this, account);
  }

  public String toString() {
    return ToStringBuilder.reflectionToString(this);
  }
}

However, these methods use AccessibleObject.setAccessible() method to change the visibility of class fields (since generally these fields are private). This will not work under a security manager, unless appropriate permissions are set up correctly. Also, these methods are slower than testing explicitly.

Using EqualsBuilder to create equals() method

While writing you own classes, you generally need to implement equals() and hashCode() methods. Also, you need to implement the methods such that objects which are equal must generate the same hashCode. The contract for equals() method (from the documentation for Object class) states that:

The equals method implements an equivalence relation on non-null object references:

  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  • It is 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.
  • It is 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.
  • It is 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.
  • For any non-null reference value x, x.equals(null) should return false.

So, in your implementation, for all the relevant fields you need to do the following:

  1. Check that the field is not null
  2. Compare the fields in the two objects properly:
    • If field is of primitive type, use '=='
    • If it is of some class type, use 'equals()'
    • If it is of array type, use 'Arrays.equal()'

Writing all this is not hard, but it is tedious. The EqualsBuilder class from the Apache’s Commons Lang project makes it quite easy to properly implement the equals() method in your class. Typical use is as follows:

public boolean equals(Object obj) {
   if (obj == null) { return false; }
   if (obj == this) { return true; }
   if (obj.getClass() != getClass()) {
     return false;
   }
   MyClass rhs = (MyClass) obj;
   return new EqualsBuilder()
                 .appendSuper(super.equals(obj))
                 .append(field1, rhs.field1)
                 .append(field2, rhs.field2)
                 .append(field3, rhs.field3)
                 .isEquals();
}

We create an instance of the EqualsBuilder class and we supply it the pairs of fields that we want to compare (e.g. field1 & rhs.field1). The append()/appendSuper() methods return an instance of EqualsBuilder itself, so calls to multiple append methods can be chained as shown above. Finally, we return the value from the isEquals() method of the last instance in the chain.

A complete example follows:

public class Account {
  private long id;
  private String firstName;
  private String lastName;
  private String emailAddress;
  private Date creationDate;

  public boolean equals(Object obj) {
    if(this == obj) {
      return true;
    }
    if(obj == null || this.getClass() != obj.getClass()) {
      return false;
    }
  
    Account account = (Account) obj;
    return new EqualsBuilder().append(this.id, account.id)
      .append(this.firstName, account.firstName)
      .append(this.lastName, account.lastName)
      .append(this.emailAddress, account.emailAddress)
      .append(this.creationDate, account.creationDate)
      .isEquals();
  }
}