Monday, October 24, 2011

Guava's Objects Class: Equals, HashCode, and ToString

If you are fortunate enough to be using JDK 7, the newly available Objects class is the obvious (at least to me) choice for implementing the "common" Java object methods such as equals(Object) [with Objects.equals(Object,Object)], hashCode() [with Objects.hashCode(Object) or Objects.hash(Object...)], and toString() [with Objects.toString(Object)] to appropriately override the default Object implementations. I have written posts about using Objects class: JDK 7: The New Objects Class and Java 7 Objects-Powered Compact Equals.

If you're not yet using Java 7, your best choices might be the Apache Commons builders ToStringBuilder and EqualsBuilder and HashCodeBuilder (if you're using a version of Java prior to J2SE 5) or Guava (if you're using J2SE 5 or later). In this post, I look at using Guava's Objects class to implement the three common methods equals, hashCode, and toString().

Without Guava or other library to help, the three common methods discussed in this post are often highlighted as shown in the next code listing. These methods were generated with NetBeans 7.1 beta.

TraditionalEmployee
package dustin.examples;

import java.util.Calendar;

/**
 * Simple employee class using NetBeans-generated 'common' methods
 * implementations that are typical of many such implementations created
 * without Guava or other library.
 * 
 * @author Dustin
 */
public class TraditionalEmployee
{
   public enum Gender{ FEMALE, MALE };

   private final String lastName;
   private final String firstName;
   private final String employerName;
   private final Gender gender;

   /**
    * Create an instance of me.
    * 
    * @param newLastName The new last name my instance will have.
    * @param newFirstName The new first name my instance will have.
    * @param newEmployerName The employer name my instance will have.
    * @param newGender The gender of my instance.
    */
   public TraditionalEmployee(
      final String newLastName, final String newFirstName,
      final String newEmployerName, final Gender newGender)
   {
      this.lastName = newLastName;
      this.firstName = newFirstName;
      this.employerName = newEmployerName;
      this.gender = newGender;
   }

   public String getEmployerName()
   {
      return this.employerName;
   }

   public String getFirstName()
   {
      return this.firstName;
   }

   public Gender getGender()
   {
      return this.gender;
   }

   public String getLastName()
   {
      return this.lastName;
   }

   /**
    * NetBeans-generated method that compares provided object to me for equality.
    * 
    * @param obj Object to be compared to me for equality.
    * @return {@code true} if provided object is considered equal to me or
    *    {@code false} if provided object is not considered equal to me.
    */
   @Override
   public boolean equals(Object obj)
   {
      if (obj == null)
      {
         return false;
      }
      if (getClass() != obj.getClass())
      {
         return false;
      }
      final TraditionalEmployee other = (TraditionalEmployee) obj;
      if ((this.lastName == null) ? (other.lastName != null) : !this.lastName.equals(other.lastName))
      {
         return false;
      }
      if ((this.firstName == null) ? (other.firstName != null) : !this.firstName.equals(other.firstName))
      {
         return false;
      }
      if ((this.employerName == null) ? (other.employerName != null) : !this.employerName.equals(other.employerName))
      {
         return false;
      }
      if (this.gender != other.gender)
      {
         return false;
      }
      return true;
   }

   /**
    * NetBeans-generated method that provides hash code of this employee instance.
    * 
    * @return My hash code.
    */
   @Override
   public int hashCode()
   {
      int hash = 3;
      hash = 19 * hash + (this.lastName != null ? this.lastName.hashCode() : 0);
      hash = 19 * hash + (this.firstName != null ? this.firstName.hashCode() : 0);
      hash = 19 * hash + (this.employerName != null ? this.employerName.hashCode() : 0);
      hash = 19 * hash + (this.gender != null ? this.gender.hashCode() : 0);
      return hash;
   }

   /**
    * NetBeans-generated method that provides String representation of employee
    * instance.
    * 
    * @return My String representation.
    */
   @Override
   public String toString()
   {
      return  "TraditionalEmployee{" + "lastName=" + lastName + ", firstName=" + firstName
            + ", employerName=" + employerName + ", gender=" + gender +  '}';
   }
}

Although NetBeans 7.1 beta did the heavy lifting here, this code still must be maintained and can be made more readable. The next class is the same class, but with Guava-powered common methods instead of the NetBeans-generated 'typical' implementations shown above.

GuavaEmployee
package dustin.examples;

/**
 * Simple employee class using Guava-powered 'common' methods implementations.
 * 
 * I explicitly scope the com.google.common.base.Objects class here to avoid the
 * inherent name collision with the java.util.Objects class.
 * 
 * @author Dustin
 */
public class GuavaEmployee
{
   public enum Gender{ FEMALE, MALE };

   private final String lastName;
   private final String firstName;
   private final String employerName;
   private final TraditionalEmployee.Gender gender;

   /**
    * Create an instance of me.
    * 
    * @param newLastName The new last name my instance will have.
    * @param newFirstName The new first name my instance will have.
    * @param newEmployerName The employer name my instance will have.
    * @param newGender The gender of my instance.
    */
   public GuavaEmployee(
      final String newLastName, final String newFirstName,
      final String newEmployerName, final TraditionalEmployee.Gender newGender)
   {
      this.lastName = newLastName;
      this.firstName = newFirstName;
      this.employerName = newEmployerName;
      this.gender = newGender;
   }

   public String getEmployerName()
   {
      return this.employerName;
   }

   public String getFirstName()
   {
      return this.firstName;
   }

   public TraditionalEmployee.Gender getGender()
   {
      return this.gender;
   }

   public String getLastName()
   {
      return this.lastName;
   }

   /**
    * Using Guava to compare provided object to me for equality.
    * 
    * @param obj Object to be compared to me for equality.
    * @return {@code true} if provided object is considered equal to me or
    *    {@code false} if provided object is not considered equal to me.
    */
   @Override
   public boolean equals(Object obj)
   {
      if (obj == null)
      {
         return false;
      }
      if (getClass() != obj.getClass())
      {
         return false;
      }
      final GuavaEmployee other = (GuavaEmployee) obj;
      
      return   com.google.common.base.Objects.equal(this.lastName, other.lastName)
            && com.google.common.base.Objects.equal(this.firstName, other.firstName)
            && com.google.common.base.Objects.equal(this.employerName, other.employerName)
            && com.google.common.base.Objects.equal(this.gender, other.gender);
   }

   /**
    * Uses Guava to assist in providing hash code of this employee instance.
    * 
    * @return My hash code.
    */
   @Override
   public int hashCode()
   {
      return com.google.common.base.Objects.hashCode(
                this.lastName, this.firstName, this.employerName, this.gender);
   }

   /**
    * Method using Guava to provide String representation of this employee
    * instance.
    * 
    * @return My String representation.
    */
   @Override
   public String toString()
   {
      return com.google.common.base.Objects.toStringHelper(this)
                .addValue(this.lastName)
                .addValue(this.firstName)
                .addValue(this.employerName)
                .addValue(this.gender)
                .toString();
   }
}

As the code above proves, the use of Guava improves the readability of the implementations of the three common methods. The only thing that's not so nice is the need to explicitly scope Guava's Objects class in the code to avoid a naming collision with Java SE 7's Objects class. Of course, if one is not using Java 7, then this is not an issue and if one is using Java 7, it's most likely that the standard version should be used instead anyway.

Conclusion

Guava provides a nice approach for building safer and more readable common methods via its Objects class. Although I'll use the new java.util.Objects class instead for JDK 7 projects, Guava's com.google.common.base.Objects class provides a nice alternative for working in versions of Java prior to JDK 7.

4 comments:

Sebastian Dietrich said...

Java7 Objects work differently than Guava, commons.lang etc. It just calls the corresponding methods on the provided object.

E.g. Objects.toString(this); calls this.toString();

@DustinMarx said...

Sebastian,

If that was all Objects did, it wouldn't be very useful. I think the fact that it checks them for null first so that I don't have to is what makes it special and similar to how Guava's Objects class behaves. For example, java.util.Objects.hash(Objects...) is almost identical to com.google.common.base.Objects.hash(Objects...).

Dustin

PhiLho said...

"The only thing that's not so nice is the need to explicitly scope Guava's Objects class in the code to avoid a naming collision with Java SE 7's Objects class."
This is unnecessary, unless you import the whole java.util.*
But indeed, we can as well use Java 7's built-in methods, since they are identical to Guava one (I think).

Muhammad Ali Khojaye - Java, Cloud and Big Data said...

Nice. Very precise and brief. I also add http://muhammadkhojaye.blogspot.com/2010/02/java-hashing.html‎ which i also find useful how hashcode work with the concept of bucket.