Java Equals and HashCode by Reflection

Writing proper ”equals()” and ”hashCode()” methods
A class which is to be used in Maps and Sets must always have their equals() and hashCode() method overridden so that they reflect the data structure of the actual object.

This means that the fields of the object which are required for equality must be taken into account when overriding these methods. The JavaDoc of Object.equals() and Object.hashCode() go into further detail, but it remains to be mentioned that if a field has a unique identifier then this will mostly suffice for the equality implementation.

When using the Eclipse IDE, implementing the equals() and hashCode() method are a matter of:

  • Opening the context menu in the class’s source editor
  • Opening the Source menu
  • Selecting Generate hashCode() and equals()…
  • Select on the fields required for uniqueness
  • Further configure the dialog and then selecting ok

Note: For further information, see Joshua Bloch’s book Effective Java which describes this issue further.

Performance issues
Apache Commons has two classes HashCodeBuilder and EqualsBuilder as an easy way of implementing these methods. These classes use reflection to dynamically build the hashCode or check equality of two instances.

They should be avoided as they can have a serious impact on performance as the following test will show.

Test Class:

  public static class TestClass {
    public static final int GENERATED = 0;
    public static final int REFLECTION = 1;
    public static final int COMMONS_BUILDER = 2;
    public static int hashCodeBuilding = GENERATED;
    private int a;
    private int b;
    private String c;
    private String d;

    public TestClass(final int a, final int b, final String c, final String d) {
      this.a = a;
      this.b = b;
      this.c = c;
      this.d = d;
    }

    @Override
    public int hashCode() {
      if (hashCodeBuilding == REFLECTION) {
        return HashCodeBuilder.reflectionHashCode(this);
      }
      if (hashCodeBuilding == COMMONS_BUILDER) {
        return new HashCodeBuilder()
                      .append(this.a)
                      .append(this.b)
                      .append(this.c)
                      .append(this.d)
                      .toHashCode();
      }

      final int prime = 31;
      int result = 1;
      result = prime * result + this.a;
      result = prime * result + this.b;
      result = prime * result + ((this.c == null) ? 0 : this.c.hashCode());
      result = prime * result + ((this.d == null) ? 0 : this.d.hashCode());
      return result;
    }

    @Override
    public boolean equals(final Object obj) {

      if (hashCodeBuilding == REFLECTION) {
        return EqualsBuilder.reflectionEquals(this, obj);
      }

      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (getClass() != obj.getClass()) {
        return false;
      }

      TestClass other = (TestClass) obj;
      if (hashCodeBuilding == COMMONS_BUILDER) {
        return new EqualsBuilder().append(this.a, other.a)
                     .append(this.b, other.b)
                     .append(this.c, other.c)
                     .append(this.d, other.d)
                     .isEquals();
      }

      if (this.a != other.a) {
        return false;
      }

      if (this.b != other.b) {
        return false;
      }

      if (this.c == null) {
        if (other.c != null) {
          return false;
        }
      }
      else if (!this.c.equals(other.c)) {
        return false;
      }

      if (this.d == null) {
        if (other.d != null) {
          return false;
        }
      }
      else if (!this.d.equals(other.d)) {
        return false;
      }

      return true;
    }
  }

JUnit Test method:

  @Test
  public void hashCodePerformanceTest() {
    TestClass a = new TestClass(22, 11, "bla bla", "no no");
    TestClass b = new TestClass(52, 63, "dfgdfgdfg", "xv3dfg1");

    System.out.println("First pass...");
    runTest(a, b, TestClass.REFLECTION);
    runTest(a, b, TestClass.COMMONS_BUILDER);
    runTest(a, b, TestClass.GENERATED);

    System.out.println("Second pass...");
    runTest(a, b, TestClass.REFLECTION);
    runTest(a, b, TestClass.COMMONS_BUILDER);
    runTest(a, b, TestClass.GENERATED);

    System.out.println("Third pass...");
    runTest(a, b, TestClass.REFLECTION);
    runTest(a, b, TestClass.COMMONS_BUILDER);
    runTest(a, b, TestClass.GENERATED);

    System.out.println("Fourth pass...");
    runTest(a, b, TestClass.REFLECTION);
    runTest(a, b, TestClass.COMMONS_BUILDER);
    runTest(a, b, TestClass.GENERATED);

    System.out.println("Fifth pass...");
    runTest(a, b, TestClass.REFLECTION);
    runTest(a, b, TestClass.COMMONS_BUILDER);
    runTest(a, b, TestClass.GENERATED);
  }

  private void runTest(final TestClass a, final TestClass b, final int hashCodeBuilding) {
    TestClass.hashCodeBuilding = hashCodeBuilding;
    long start = System.nanoTime();
    for (int i = 0; i < 10000; i++) {
      a.equals(b);
      a.hashCode();
    }

    long time = System.nanoTime() - start;
    if (hashCodeBuilding == TestClass.REFLECTION) {
      String msg = String.format("With reflection took %.3f ms", (time / 1000000.0D));
      System.out.println(msg);
    }
    else if (hashCodeBuilding == TestClass.COMMONS_BUILDER) {
      String msg = String.format("With apache commons builder took %.3f ms", (time / 1000000.0D));
      System.out.println(msg);
    }
    else {
      String msg = String.format("With eclipse generated took %.3f ms", (time / 1000000.0D));
      System.out.println(msg);
    }
  }

Test result:

First pass...
With reflection took 72.796 ms
With apache commons builder took 1.689 ms
With eclipse generated took 0.607 ms
Second pass...
With reflection took 48.158 ms
With apache commons builder took 1.199 ms
With eclipse generated took 0.151 ms
Third pass...
With reflection took 48.240 ms
With apache commons builder took 1.095 ms
With eclipse generated took 0.151 ms
Fourth pass...
With reflection took 47.274 ms
With apache commons builder took 2.264 ms
With eclipse generated took 0.150 ms
Fifth pass...
With reflection took 46.475 ms
With apache commons builder took 1.199 ms
With eclipse generated took 0.160 ms