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.

Advertisements

Using CompareToBuilder to implement Comparable interface

Many a times, we need to implement the java.lang.Comparable interface. This interface has a single method int compareTo(Object o). Apache’s Commons Lang library provides a utility class – CompareToBuilder which makes it easy to write the compareTo() method. The method is consistent with with equals() and hashCode() methods built using EqualsBuilder and HashCodeBuilder (provided the same fields are used in all of them). A typical use of the class is as follows:

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

  public int compareTo(Account account) {
    return new CompareToBuilder()
        .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)
        .toComparison();
  }
}

We first create an instance of the CompareToBuilder. The append method takes two arguments – the two fields we want to compare. It returns an instance of CompareToBuilder so multiple calls to the append method can be chained (as in the above example). Finally we call the toComparison() method which returns the appropriate value to the caller.

Using ToStringBuilder to easily write toString() method

Another useful class in Apache’s Commons Lang library is the ToStringBuilder class. I’ve already covered the HashCodeBuilder and the EqualsBuilder classes in previous posts. The ToStringBuilder class provides you methods to easily write the toString() method in your class. The program below explains how to use it:

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

  public String toString() {
    return new ToStringBuilder(this)
        .append("id", this.id)
        .append("firstName", this.firstName)
        .append("lastName", this.lastName)
        .append("emailAddress", this.emailAddress)
        .append("creationDate", this.creationDate)
        .toString();
  }
}

The output is of the form Account@43d1[id=1, firstName=Parambir, lastName=Singh, emailAddress=test@parambir.com, creationDate=Mon Feb 22 10:54:29 IST 2010].

First we create an object of the ToStringBuilder class (and pass the ‘this’ reference to the constructor). Then we use the append method to add each field that we want to print in the toString() method. Append method takes two arguments: fieldName and fieldValue. e.g. builder.append("name", this.name). If the value of the field “name” was “Param”, it will be printed as “name=Param”. The append method returns an instance of the ToStringBuilder, so the calls to multiple append methods can be chained (as in the example above).

Some other useful methods of the class are:

  • append(value) – No field name is printed in this case, only the value is printed.
  • append(name, value) – The field will be printed in the form “name=value”.
  • appendSuper(String s) – Appends the toString from the super class.

The class also takes care of printing arrays properly!

Using HashCodeBuilder to implement hashCode()

Similar to overriding equals(), you may also need to override the hashCode() method in your custom class. The equals() contract states that two objects which compare equal must have the same hashCode. Moreover, for efficient storage in HashMap, non-equal objects should return different hashcodes.

Since equal objects should have equal hashcodes, you must compare the same fields as in the equals() method.

With all these concerns in mind, writing an effective hashCode() method might be difficult. Fortunately, the Apache Commons Lang library provides a utility class called HashCodeBuilder which makes it fairly easy to implement an efficient hashCode method.

Here’s how you use this class:

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

  public int hashCode() {
    return new HashCodeBuilder(11, 21)
        .append(this.id)
        .append(this.firstName)
        .append(this.lastName)
        .append(this.emailAddress)
        .append(this.creationDate)
        .toHashCode();
  }
}

First you need to initialize an object of the class HashCodeBuilder. The constructor takes two integers as arguments. You need to supply two random odd integers (preferable prime) as the arguments. You pass a relevant field of the class (that you want to be included while calculating the hashCode) to the append method. The method returns a HashCodeBuilder instance. So, you can chain the calls to the append methods together (as in the example above). Finally, you call the toHashCode() method which returns the computed hash code to the caller.

Some other useful constructors/methods in the class are:

  • appendSuper(int) – use this method if you want to append the hashCode of the super class object. E.g. o.appendSuper(super.hashCode());
  • HashCodeBuilder(int initialNonZeroOddNumber, int multiplierNonZeroOddNumber): These two arguments are used for calculating the hashCode for the object. Try to use different arguments for different classes.
  • HashCodeBuilder(): This uses two hard coded choices for the constants for calculating the hashCode.
  • hashCode(): returns the computed hash code (same as toHashCode()). Note: It doesn’t return the hashCode of the HashCodeBuilder object.

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();
  }
}