DigitalJoel

2011/02/17

Java Equals Implementation Performance

Filed under: java — Tags: , — digitaljoel @ 4:13 pm

I had some questions about various implementations of the equals (and by association hashcode) methods in Java. I recently implemented a solution in a project I’m working on that uses Apache’s EqualsBuilder in order to create a simple, elegant looking implementation. Knowing that the solution used reflection, and that equals may be called a lot more than you would think. So, I implemented a little test today in order to see the performance of various implementations. The contenders are the equals method generated by the Eclipse IDE, Apache Commons EqualsBuilder implementation using append, and also using reflection, and finally Pojomatic.

Here’s the source of the test

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.pojomatic.Pojomatic;
import org.pojomatic.annotations.AutoProperty;

public class EqualsTest {

  // how many objects we want to create
  static long count = 10000;
  // seed for our random number generator so all tests get the same variation
  static long seed = 12345;
  // range of variation in the values for our objects
  static int variation = 15;
  // how many times equals was called from the eclipse method
  static long plainCounter = 0;
  // how many times equals was called from the reflection method
  static long reflectCounter = 0;
  // how many times equals was called from the append method
  static long appendCounter = 0;
  // how many times equals was called from the pojomatic method
  static long pojomaticCounter = 0;
  // how many times equals was called from the broken method.
  static long brokenCounter = 0;

  public static void main(String[] args) {
    EqualsTest test = new EqualsTest();
    System.gc();
    test.testPlainEquals();
    System.gc();
    test.testAppendEquals();
    System.gc();
    test.testReflectEquals();
    System.gc();
    test.testPojomaticEquals();
    System.gc();
    test.testBrokenEquals();
  }

  // Do our little test that calls equals a lot of times.
  private void testSomething(List<Object> objects, Object object) {
    for (Object o : objects) {
      if (o.equals(object)) {
        return;
      }
    }
    objects.add(object);
  }

  // Test the performance of equals as implemented using pojomatic
  // http://pojomatic.sourceforge.net/pojomatic/index.html
  private void testPojomaticEquals() {
    Random rand = new Random(seed);
    List<Object> list = new ArrayList<Object>();
    long start = System.currentTimeMillis();
    for (long i = 0; i < count; i++) {
      String s = "asdf";
      long suffix = i * rand.nextInt(variation);
      MyObjectPojomatic obj = new MyObjectPojomatic(s + suffix, s + (suffix + 1), s + (suffix - 1),
          suffix, suffix + 1, suffix - 1);
      testSomething(list, obj);
    }
    long end = System.currentTimeMillis();
    System.out.println("Time taken for pojomatic is " + (end - start) + "ms" + " set size is "
      + list.size() + " calls = " + pojomaticCounter);
  }

  // test a broken implementation of equals to show a difference in the set size and total calls
  private void testBrokenEquals() {
    Random rand = new Random(seed);
    List<Object> list = new ArrayList<Object>();
    long start = System.currentTimeMillis();
    for (long i = 0; i < count; i++) {
      String s = "asdf";
      long suffix = i * rand.nextInt(variation);
      MyObjectBroken obj = new MyObjectBroken(s + suffix, s + (suffix + 1), s + (suffix - 1),
          suffix, suffix + 1, suffix - 1);
      testSomething(list, obj);
    }
    long end = System.currentTimeMillis();
    System.out.println("Time taken for broken is " + (end - start) + "ms" + " set size is "
      + list.size() + " calls = " + brokenCounter);
  }

  // test EqualsBuilder.reflectionEquals from apache commons.
  // http://commons.apache.org/lang/api-2.6/org/apache/commons/lang/builder/EqualsBuilder.html
  private void testReflectEquals() {
    Random rand = new Random(seed);
    List<Object> list = new ArrayList<Object>();
    long start = System.currentTimeMillis();
    for (long i = 0; i < count; i++) {
      String s = "asdf";
      long suffix = i * rand.nextInt(variation);
      MyObjectReflect obj = new MyObjectReflect(s + suffix, s + (suffix + 1), s + (suffix - 1),
          suffix, suffix + 1, suffix - 1);
      testSomething(list, obj);
    }
    long end = System.currentTimeMillis();
    System.out.println("Time taken for reflection is " + (end - start) + "ms" + " set size is "
      + list.size() + " calls = " + reflectCounter);
  }

  // test EqualsBuilder.append equals from apache commons.
  // http://commons.apache.org/lang/api-2.6/org/apache/commons/lang/builder/EqualsBuilder.html
  private void testAppendEquals() {
    Random rand = new Random(seed);
    List<Object> list = new ArrayList<Object>();
    long start = System.currentTimeMillis();
    for (long i = 0; i < count; i++) {
      String s = "asdf";
      long suffix = i * rand.nextInt(variation);
      MyObjectAppend obj = new MyObjectAppend(s + suffix, s + (suffix + 1), s + (suffix - 1),
          suffix, suffix + 1, suffix - 1);
      testSomething(list, obj);
    }
    long end = System.currentTimeMillis();
    System.out.println("Time taken for append is " + (end - start) + "ms" + " set size is "
      + list.size() + " calls = " + appendCounter);
  }

  // Test the equals method as generated by Eclipse IDE
  // http://eclipse.org/
  private void testPlainEquals() {
    Random rand = new Random(seed);
    List<Object> list = new ArrayList<Object>();
    long start = System.currentTimeMillis();
    for (long i = 0; i < count; i++) {
      String s = "asdf";
      long suffix = i * rand.nextInt(variation);
      MyObjectPlain obj = new MyObjectPlain(s + suffix, s + (suffix + 1), s + (suffix - 1), suffix,
          suffix + 1, suffix - 1);
      testSomething(list, obj);
    }
    long end = System.currentTimeMillis();
    System.out.println("Time taken for plain is " + (end - start) + "ms" + " set size is "
      + list.size() + " calls = " + plainCounter);
  }

  // Object that uses EqualsBuilder.append for equals.
  public class MyObjectAppend {
    public String s1;
    public String s2;
    public String s3;
    public Long l1;
    public Long l2;
    public Long l3;

    public MyObjectAppend(String a, String b, String c, Long d, Long e, Long f) {
      s1 = a;
      s2 = b;
      s3 = c;
      l1 = d;
      l2 = e;
      l3 = f;
    }

    @Override
    public boolean equals(Object obj) {
      appendCounter += 1;
      if (obj instanceof MyObjectAppend) {
        MyObjectAppend that = (MyObjectAppend) obj;
        EqualsBuilder builder = new EqualsBuilder();
        builder.append(this.s1, that.s1);
        builder.append(this.s2, that.s2);
        builder.append(this.s3, that.s3);
        builder.append(this.l1, that.l1);
        builder.append(this.l2, that.l2);
        builder.append(this.l3, that.l3);
        return builder.isEquals();
      }
      return false;
    }

    @Override
    public int hashCode() {
      HashCodeBuilder builder = new HashCodeBuilder();
      builder.append(this.s1);
      builder.append(this.s2);
      builder.append(this.s3);
      builder.append(this.l1);
      builder.append(this.l2);
      builder.append(this.l3);
      return builder.toHashCode();

    }
  }

  // Object that uses EqualsBuilder.reflectEquals for the implementation
  public class MyObjectReflect {
    public String s1;
    public String s2;
    public String s3;
    public Long l1;
    public Long l2;
    public Long l3;

    public MyObjectReflect(String a, String b, String c, Long d, Long e, Long f) {
      s1 = a;
      s2 = b;
      s3 = c;
      l1 = d;
      l2 = e;
      l3 = f;
    }

    @Override
    public boolean equals(Object obj) {
      reflectCounter += 1;
      return EqualsBuilder.reflectionEquals(this, obj);
    }

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

  // Object that users pojomatic's equals implementation
  @AutoProperty
  public class MyObjectPojomatic {
    public String s1;
    public String s2;
    public String s3;
    public Long l1;
    public Long l2;
    public Long l3;

    public MyObjectPojomatic(String a, String b, String c, Long d, Long e, Long f) {
      s1 = a;
      s2 = b;
      s3 = c;
      l1 = d;
      l2 = e;
      l3 = f;
    }

    @Override
    public boolean equals(Object obj) {
      pojomaticCounter += 1;
      return Pojomatic.equals(this, obj);
    }

    @Override
    public int hashCode() {
      return Pojomatic.hashCode(this);
    }
  }

  // Object with a broken equals implementation.
  public class MyObjectBroken {
    public String s1;
    public String s2;
    public String s3;
    public Long l1;
    public Long l2;
    public Long l3;

    public MyObjectBroken(String a, String b, String c, Long d, Long e, Long f) {
      s1 = a;
      s2 = b;
      s3 = c;
      l1 = d;
      l2 = e;
      l3 = f;
    }

    @Override
    public boolean equals(Object obj) {
      brokenCounter += 1;
      return false;
    }

    @Override
    public int hashCode() {
      return 1;
    }
  }

  // Object that uses Eclipse's generated equals implementation.
  public class MyObjectPlain {
    public String s1;
    public String s2;
    public String s3;
    public Long l1;
    public Long l2;
    public Long l3;

    public MyObjectPlain(String a, String b, String c, Long d, Long e, Long f) {
      s1 = a;
      s2 = b;
      s3 = c;
      l1 = d;
      l2 = e;
      l3 = f;
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + getOuterType().hashCode();
      result = prime * result + ((l1 == null)
          ? 0
          : l1.hashCode());
      result = prime * result + ((l2 == null)
          ? 0
          : l2.hashCode());
      result = prime * result + ((l3 == null)
          ? 0
          : l3.hashCode());
      result = prime * result + ((s1 == null)
          ? 0
          : s1.hashCode());
      result = prime * result + ((s2 == null)
          ? 0
          : s2.hashCode());
      result = prime * result + ((s3 == null)
          ? 0
          : s3.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      plainCounter += 1;
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      MyObjectPlain other = (MyObjectPlain) obj;
      if (!getOuterType().equals(other.getOuterType()))
        return false;
      if (l1 == null) {
        if (other.l1 != null)
          return false;
      }
      else if (!l1.equals(other.l1))
        return false;
      if (l2 == null) {
        if (other.l2 != null)
          return false;
      }
      else if (!l2.equals(other.l2))
        return false;
      if (l3 == null) {
        if (other.l3 != null)
          return false;
      }
      else if (!l3.equals(other.l3))
        return false;
      if (s1 == null) {
        if (other.s1 != null)
          return false;
      }
      else if (!s1.equals(other.s1))
        return false;
      if (s2 == null) {
        if (other.s2 != null)
          return false;
      }
      else if (!s2.equals(other.s2))
        return false;
      if (s3 == null) {
        if (other.s3 != null)
          return false;
      }
      else if (!s3.equals(other.s3))
        return false;
      return true;
    }

    private EqualsTest getOuterType() {
      return EqualsTest.this;
    }
  }
}

And here’s the output

Time taken for plain is 804ms set size is 8696 calls = 39185108
Time taken for append is 3030ms set size is 8696 calls = 39185108
Time taken for reflection is 20765ms set size is 8696 calls = 39185108
Time taken for pojomatic is 4094ms set size is 8696 calls = 39185108
Time taken for broken is 604ms set size is 10000 calls = 49995000

Given that the set size and the number of equals calls are the same, you can be reasonably sure that each implementation is as correct as the others, other than the intentionally broken one, which was meant to help illustrate this point.

The results speak for themselves. The Eclipse generated solution is fastest, followed by append, followed closely by pojomatic. Finally, the reflection based implementation took over 25x the amount of time as the fastest solution.

Advertisements

Create a free website or blog at WordPress.com.