Saturday, November 15, 2008

Apache Commons EqualsBuilder and HashCodeBuilder

I previously blogged on the Apache Commons ToStringBuilder and discussed how it takes away much of the tedium normally associated with implementing toString methods. While implementing toString() does provide significant value in debugging and logging and is a recommended practice in Joshua Bloch's Effective Java (Item 10 in Second Edition), it normally does not impact the logic and performance of an application (unless toString() is specifically used as part of the logic). However, there are methods defined in Object that do impact both logic and performance in an application and two of them [equals() and hashCode()] are discussed in this blog entry.

While hashCode() and equals() typically impact logic and performance more than toString() does, they are also often more tricky to implement correctly. Many Java developers follow the Joshua Bloch's advice for implementing these methods as described in Effective Java (where 18 pages of the core 315 pages are devoted to these two methods). For example, the article Hashtables: When You Create Your Own Key Object in a Hashtable, Be Careful summarizes the rules that an equals() method should adhere to and provides Bloch's recommendations in Java code. The article Hashing it Out: Designing hashCode() and equals() Effectively and Correctly also discusses how to implement these two important methods (equals and hashCode). Of course, the easiest rule to remember is that when one of these two methods is overridden, the other one should be as well.

Because it can be tricky to implement hashCode() and equals() correctly, it is helpful to have reusable implementations of these provided as part of Apache Commons Lang builder package (same package that contains the previously mentioned ToStringBuilder). Even better, these implementations were explicitly written to follow Bloch's frequently cited advice as described in the Javadoc documentation for both EqualsBuilder and HashCodeBuilder.

In my blog entry on ToStringBuilder, I demonstrated and heavily used its reflective capabilities. I am less inclined to use reflection capabilities in conjunction with EqualsBuilder and HashCodeBuilder because these methods are often used in situations where performance is a major issue. Details on applying reflection-based use of EqualsBuilder and HashCodeBuilder are available here and in the respective Javadoc descriptions for these classes.

The example code used in this blog entry is very simple and only scratches the surface of what EqualsBuilder and HashCodeBuilder are capable of accomplishing. However, the example code does provide a simple example of common use of these two classes. An added bonus is that the code also demonstrates the Commons CLI and Commons Lang ToStringBuilder in action as well.

The first class to look at is the SimpleDataExample class because it is the class that actually contains the implementations of equals() and hashCode() methods using EqualsBuilder and HashCodeBuilder respectively. This example also uses ToStringBuilder to implement its toString() method.


package dustin.builders;

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

/**
* This is a "simple" data class intended for demonstration of Apache Commons
* EqualsBuilder and HashCodeBuilder. This is an immutable class and all of its
* state must be provided at construction.
*
* @author Dustin
*/
public class SimpleDataExample
{
/** ID associated with this class. */
private final Long id;

/** Name of data (does not need to be unique). */
private final String name;

/**
* Constructor accepting arguments to populate my state.
*
* @param newId ID of this object instance.
* @param newName Name of this object instance.
*/
public SimpleDataExample(
final Long newId, final String newName)
{
this.id = newId;
this.name = newName;
}

/** Private constructor - not meant to be used. */
private SimpleDataExample()
{
this.id = null;
this.name = null;
}

/**
* Provide my ID.
*
* @return My ID.
*/
public Long getId()
{
return this.id;
}

/**
* Provide my name.
*
* @return My name.
*/
public String getName()
{
return this.name;
}

/**
* My hash code implementation.
*
* @return My hash code.
*/
@Override
public int hashCode()
{
return new HashCodeBuilder()
.append(this.id)
.append(this.name)
.toHashCode();
}

/**
* My implementation of equals() method. The NetBeans-generated version is
* left in place (but commented out) to notice the order of magnitude of code
* required without EqualsBuilder.
*
* @param obj The object to compare to me for equality.
* @return true if the other object and I are equal; false otherwise.
*/
@Override
public boolean equals(Object obj)
{
if (obj instanceof SimpleDataExample == false)
{
return false;
}
if (this == obj)
{
return true;
}
final SimpleDataExample otherObject = (SimpleDataExample) obj;

return new EqualsBuilder()
.append(this.id, otherObject.id)
.append(this.name, otherObject.name)
.isEquals();
/*
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final SimpleDataExample other = (SimpleDataExample) obj;
if (this.id != other.id && (this.id == null || !this.id.equals(other.id)))
{
return false;
}
if (this.name != other.name && (this.name == null || !this.name.equals(other.name)))
{
return false;
}
return true;
*/
}

/**
* Provide String representation of me.
*
* @return String representation of me.
*/
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("ID", this.id)
.append("Name", this.name)
.toString();
}
}


The code of most interest from this blog entry's perspective is all in the class above, particularly in the equals() and hashCode() methods. The next code listing lists a "test" class that uses the simple data class defined above in both an ArrayList and a HashSet, depending on the command-line argument provided to its main() method. This demonstrates Commons CLI, but more importantly demonstrates use of the equals() and hashCode() methods in the data class.


package dustin.builders;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

/**
* Main application to run examples of Apache Commons EqualsBuilder and
* HashCodeBuilder.
*
* @author Dustin
*/
public class Main
{
/** Collection of Simple Data. */
private Collection data;

/** Line separator / carriage return / line feed. */
private static String NEW_LINE = System.getProperty("line.separator");

/**
* Preferred approach for acquiring an instance of Main object.
*
* @return Instantiated Main object.
*/
public static Main newInstance(final String[] arguments)
{
final Main main = new Main();
final ExampleCollectionTypeEnum collectionType =
parseCmdLineArgsForCollectionType(arguments);
main.initializeData(collectionType, System.out);
return main;
}

/** No-arguments constructor; should not be used directly by outsiders. */
private Main() {}

/**
* Initialize the data.
*
* @param collectionType Type of implementation to be used for Collection.
*/
public void initializeData(
final ExampleCollectionTypeEnum collectionType,
final OutputStream out)
{
String collectionTypeStr = "";
switch ( collectionType )
{
case ARRAY_LIST :
this.data = new ArrayList();
collectionTypeStr = "ArrayList";
break;
case HASH_SET :
this.data = new HashSet();
collectionTypeStr = "HashSet";
break;
default :
System.err.println(
collectionType + " is an unexpected type of collection." + NEW_LINE);
System.exit(-1);
}
try
{
out.write(
("*****" + collectionTypeStr + " is being used." + NEW_LINE).getBytes());
}
catch (IOException ioEx)
{
System.out.println("*****" + collectionTypeStr + " is being used.");
}
this.data.add(new SimpleDataExample(1L, "The First One"));
this.data.add(new SimpleDataExample(5L, "The Fifth Element"));
this.data.add(new SimpleDataExample(3L, "The Third Kind of Encounter"));
this.data.add(new SimpleDataExample(2L, "The Second of a Kind"));
this.data.add(new SimpleDataExample(4L, "Fourth is a Quarter"));
}

/**
* Demonstate use of equals() method and hashCode() method implementations.
*
* @param out OutputStream to which to write the results of the test.
*/
public void testForCollectionContainment(
final OutputStream out)
{
final SimpleDataExample simple1 = new SimpleDataExample(1L, "The First One");
final SimpleDataExample simple2 = new SimpleDataExample(1L, "The First Uno");
try
{
for (final SimpleDataExample simple : this.data)
{
out.write(("COMPARISON" + NEW_LINE).getBytes());
out.write(("----------" + NEW_LINE).getBytes());
out.write(("First Object: " + simple1 + NEW_LINE).getBytes());
out.write(("Second Object: " + simple + NEW_LINE).getBytes());
out.write(("\tEqual?: " + simple1.equals(simple) + NEW_LINE).getBytes());
out.write(("\t==?: " + (simple1 == simple) + NEW_LINE).getBytes());
out.write(("\tHash Codes: " + simple1.hashCode() + " vs " + simple.hashCode()).getBytes());
out.write(NEW_LINE.getBytes());
}

for (final SimpleDataExample simple : this.data)
{
out.write(("COMPARISON" + NEW_LINE).getBytes());
out.write(("----------" + NEW_LINE).getBytes());
out.write(("First Object: " + simple2 + NEW_LINE).getBytes());
out.write(("Second Object: " + simple + NEW_LINE).getBytes());
out.write(("\tEqual?: " + simple2.equals(simple) + NEW_LINE).getBytes());
out.write(("\t==?: " + (simple2 == simple) + NEW_LINE).getBytes());
out.write(("\tHash Codes: " + simple2.hashCode() + " vs " + simple.hashCode()).getBytes());
out.write(NEW_LINE.getBytes());
}
}
catch (IOException ioEx)
{
System.err.println(
"Error encountered while trying to write out two objects to be "
+ "compared and the result of their comparison: " + NEW_LINE
+ ioEx.getMessage() );
}
}

/**
* Write this application's output header/title to the provided output stream.
*
* @param out OutputStream to which to write application header information.
*/
public static void printAppHeader(final OutputStream out)
{
final String separatorString =
"==================================================================="
+ NEW_LINE;
final String headerText =
"= Example associated with blog entry 'Apache Commons" + NEW_LINE
+ "= EqualsBuilder and HashCodeBuilder' from Dustin's Software" + NEW_LINE
+ "= Development Cogitations and Speculations" + NEW_LINE
+ "= (http://marxsoftware.blogspot.com)." + NEW_LINE;
try
{
out.write(separatorString.getBytes());
out.write(headerText.getBytes());
out.write(separatorString.getBytes());
}
catch (IOException ioEx)
{
System.out.print(separatorString + headerText + separatorString);
}
}

/**
* Build up the command-line handling GNU-style options.
*
* @return Command-line handling options.
*/
private static Options buildGnuOptions()
{
final Options gnuOptions = new Options();
gnuOptions.addOption("c", true, "Collection Implementation Type to Use");
return gnuOptions;
}

/**
* Apply Apache Commons CLI GnuParser to command-line arguments.
*
* @param commandLineArguments Command-line arguments to be processed with
* Gnu-style parser.
* @return Collection implementation type.
*/
private static ExampleCollectionTypeEnum parseCmdLineArgsForCollectionType(
final String[] commandLineArguments)
{
final CommandLineParser cmdLineGnuParser = new GnuParser();

final Options gnuOptions = buildGnuOptions();
ExampleCollectionTypeEnum collectionType = null;
String optionStr = "";
CommandLine commandLine;
try
{
commandLine = cmdLineGnuParser.parse(gnuOptions, commandLineArguments);
if ( commandLine.hasOption("c") )
{
optionStr = commandLine.getOptionValue("c");
collectionType = ExampleCollectionTypeEnum.valueOf(optionStr);
}
}
catch (ParseException parseException) // checked exception
{
System.err.println(
"Encountered exception while parsing using GnuParser:\n"
+ parseException.getMessage() );
}
catch (IllegalArgumentException illegalArgEx)
{
System.err.println(optionStr + " is not a valid value for -c option.");
final ExampleCollectionTypeEnum[] possibleValues =
ExampleCollectionTypeEnum.values();
System.err.print("\tPlease use one of these values instead: ");
for (final ExampleCollectionTypeEnum value : possibleValues)
{
System.err.print(value + " ");
}
System.err.println("\n");
System.exit(-2);
}
return collectionType;
}

/**
* Main executable to run examples for Apache Commons EqualsBuilder and
* HashCodeBuilder.
*
* @param arguments The command line arguments: -c with a value should be
* specified where the value passed in with -c is the collection
* implementation type (such as HASH_SET or ARRAY_LIST).
*/
public static void main(final String[] arguments)
{
printAppHeader(System.out);
final Main me = Main.newInstance(arguments);
me.testForCollectionContainment(System.out);
}
}


There is an enum used in the code above. It is defined next.


package dustin.builders;

/**
* Enum intended for differentiation between Collection implementation types
* to be used in this example.
*
* @author Dustin
*/
public enum ExampleCollectionTypeEnum
{
ARRAY_LIST,
HASH_SET
}



The main "test" executable defined above requires the user to specify the collection type using the -c option. The choices for collection type are HASH_SET or ARRAY_LIST, which obviously correspond to specific Collections of HashSet and ArrayList.

The output from running this example with the collection type specified as ArrayList looks like this:


===================================================================
= Example associated with blog entry 'Apache Commons
= EqualsBuilder and HashCodeBuilder' from Dustin's Software
= Development Cogitations and Speculations
= (http://marxsoftware.blogspot.com).
===================================================================
*****ArrayList is being used.
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@8813f2[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@e09713[ID=1,Name=The First One]
Equal?: true
==?: false
Hash Codes: 702165685 vs 702165685
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@8813f2[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@47b480[ID=5,Name=The Fifth Element]
Equal?: false
==?: false
Hash Codes: 702165685 vs 1718548326
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@8813f2[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@19b49e6[ID=3,Name=The Third Kind of Encounter]
Equal?: false
==?: false
Hash Codes: 702165685 vs 1087926502
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@8813f2[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@10d448[ID=2,Name=The Second of a Kind]
Equal?: false
==?: false
Hash Codes: 702165685 vs 2097031442
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@8813f2[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@e0e1c6[ID=4,Name=Fourth is a Quarter]
Equal?: false
==?: false
Hash Codes: 702165685 vs 1513975290
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@e09713[ID=1,Name=The First One]
Equal?: false
==?: false
Hash Codes: 702171461 vs 702165685
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@47b480[ID=5,Name=The Fifth Element]
Equal?: false
==?: false
Hash Codes: 702171461 vs 1718548326
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@19b49e6[ID=3,Name=The Third Kind of Encounter]
Equal?: false
==?: false
Hash Codes: 702171461 vs 1087926502
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@10d448[ID=2,Name=The Second of a Kind]
Equal?: false
==?: false
Hash Codes: 702171461 vs 2097031442
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@e0e1c6[ID=4,Name=Fourth is a Quarter]
Equal?: false
==?: false
Hash Codes: 702171461 vs 1513975290



When running the same application but with the -c option specifying HASH_SET, the response looks like what follows:


===================================================================
= Example associated with blog entry 'Apache Commons
= EqualsBuilder and HashCodeBuilder' from Dustin's Software
= Development Cogitations and Speculations
= (http://marxsoftware.blogspot.com).
===================================================================
*****HashSet is being used.
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@1d58aae[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@de6f34[ID=4,Name=Fourth is a Quarter]
Equal?: false
==?: false
Hash Codes: 702165685 vs 1513975290
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@1d58aae[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@47b480[ID=3,Name=The Third Kind of Encounter]
Equal?: false
==?: false
Hash Codes: 702165685 vs 1087926502
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@1d58aae[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@19b49e6[ID=1,Name=The First One]
Equal?: true
==?: false
Hash Codes: 702165685 vs 702165685
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@1d58aae[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@10d448[ID=2,Name=The Second of a Kind]
Equal?: false
==?: false
Hash Codes: 702165685 vs 2097031442
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@1d58aae[ID=1,Name=The First One]
Second Object: dustin.builders.SimpleDataExample@e0e1c6[ID=5,Name=The Fifth Element]
Equal?: false
==?: false
Hash Codes: 702165685 vs 1718548326
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@de6f34[ID=4,Name=Fourth is a Quarter]
Equal?: false
==?: false
Hash Codes: 702171461 vs 1513975290
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@47b480[ID=3,Name=The Third Kind of Encounter]
Equal?: false
==?: false
Hash Codes: 702171461 vs 1087926502
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@19b49e6[ID=1,Name=The First One]
Equal?: false
==?: false
Hash Codes: 702171461 vs 702165685
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@10d448[ID=2,Name=The Second of a Kind]
Equal?: false
==?: false
Hash Codes: 702171461 vs 2097031442
COMPARISON
----------
First Object: dustin.builders.SimpleDataExample@6ca1c[ID=1,Name=The First Uno]
Second Object: dustin.builders.SimpleDataExample@e0e1c6[ID=5,Name=The Fifth Element]
Equal?: false
==?: false
Hash Codes: 702171461 vs 1718548326



While there will be times when you need something different than what Apache Commons provides for building equals() and hashCode() methods, the EqualsBuilder and HashCodeBuilder work well for general cases. Advantages these implementations provide include (often) improved consistency in implementation, implementations that strive to meet the best practices outlined in Effective Java, and more concise and easier-to-read methods for overriding Object's overridable methods. Best of all, because Apache Commons is open source (Apache License 2.0), you can view the code to see if it meets your needs and you have significant flexibility for modifying it as needed.


A great resource for additional information on using these Apache Commons Builder classes with some informative comments in the feedback section is available online as Build equals(), hashCode(), and toString() with Jakarta Commons. A good resource on using EqualsBuilder and HashCodeBuilder in conjunction with Hibernate for equals() and hashCode() is available in Write Simpler equals and hashCode Java Methods Using EqualsBuilder and HashCodeBuilder from Apache Commons.

1 comment:

John Ortiz said...

Good job. I am writing a custom class that overrides this native methods for a web project. Thanks for this guide.