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.