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

5 Comments »

  1. Interesting results, and thank you for sharing them. Obviously performance isn’t the only concern or you may as well pick the broken implementation 🙂 . I am curious about your thoughts on how these approaches affect code readability and maintenance. Also, have you decided which you prefer?

    Comment by Chris — 2011/02/22 @ 7:36 pm

    • Thanks for the comment Chris. We settled on the reflection based EqualsBuilder solution before I did this test because of the readability. In the abstract base class for our entities we have a simple equals and hashcode implementation that just calls the builder reflection method. It references a protected transient string array that lists the fields that should be excluded. In the base abstract class it defaults to the key and the jpa version properties for exclusions, so every other field will be included in the equals and hashcode methods. But, the descendants of the abstract class can override the excluded fields array values for that type and manage that without having to override the equals or hashcode implementation itself. It has actually made for much cleaner looking entities with an inherited equals and hashcode implementation (we also used the reflective string builder just for simplicity) that doesn’t rely on the database sequence generated id like a default, dumb implementation may. If we have to re-evaluate the implementation, we’ll do that when profiling shows that it’s causing more of a problem than other code.

      Comment by digitaljoel — 2011/02/22 @ 11:23 pm

    • To answer your second question, Chris, if I were on the project alone, I would probably go with pojomatic. It’s basically as easy as the EqualsBuilder reflection based solution, but much much faster. My partner has a real problem with the name “pojomatic” and feels that the @Property annotation is not descriptive enough. I kind of have to agree with him on the names, but I don’t know that I dislike them enough to not use the technology, especially when you basically get the performance of the EqualsBuiler.append solution while also getting the simplicity of the EqualsBuilder reflection solution. I remember thinking the name “Google” was stupid sounding. Same with “Yahoo” but it didn’t prove to be the death of either of those companies.

      Comment by digitaljoel — 2011/02/23 @ 2:38 pm

  2. Nice article but I believe its important to understand the consequences of not following this contract as well and for that its important to understand application of hashcode in collection classes e.g. How HashMap works in Java and how hashcode() of key is used to insert and retrieve object from hashMap.

    Javin

    Comment by Javin @ FIX Protocol Tutorial — 2011/02/23 @ 12:42 am

    • Agreed Javin, but the equals contract is out of the scope of this article. Hopefully, if you are worried the performance of the implementation, you are aware of the contract. All of the provided implementations here keep the contract of equals and hashCode except for the intentionally broken one.

      Comment by digitaljoel — 2011/02/23 @ 4:46 pm


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.