— Disclaimer: I know this is pretty basic stuff but many, many programmers are doing it still wrong —
As a Java programmer you know how to implement equals and that hashCode has to be implemented as well. You use your favorite IDE to generate the necessary code, use common wisdom to help you code it by hand or use annotations. But there is a fourth way: introducing EqualsBuilder (not the apache commons one which has some drawbacks over this one) which implements the general rules for equals and hashCode:
public class EqualsBuilder { public static interface IComparable { public Object[] getValuesToCompare(); } private EqualsBuilder() { super(); } public static int getHashCode(IComparable one) { if (null == one) { return 0; } final int prime = 31; int result = 1; for (Object o : one.getValuesToCompare()) { result = prime * result + EqualsBuilder.calculateHashCode(o); } return result; } private static int calculateHashCode(Object o) { if (null == o) { return 0; } return o.hashCode(); } public static boolean isEqual(IComparable one, Object two) { if (null == one || null == two) { return false; } if (one.getClass() != two.getClass()) { return false; } return compareTwoArrays(one.getValuesToCompare(), ((IComparable) two).getValuesToCompare()); } private static boolean compareTwoArrays(Object arrayOne, Object arrayTwo) { if (Array.getLength(arrayOne) != Array.getLength(arrayTwo)) { return false; } for (int i = 0; i < Array.getLength(arrayOne); i++) { if (!EqualsBuilder.areEqual(Array.get(arrayOne, i), Array.get(arrayTwo, i))) { return false; } } return true; } private static boolean areEqual(Object objectOne, Object objectTwo) { if (null == objectOne) { return null == objectTwo; } if (null == objectTwo) { return false; } if (objectOne.getClass().isArray() && objectTwo.getClass().isArray()) { return compareTwoArrays(objectOne, objectTwo); } return objectOne.equals(objectTwo); } }
The interface IComparable ensures that equals and hashCode are based on the same instance variables.
To use it your class needs to implement the interface and call the appropiate methods from EqualsBuilder:
public class MyClass implements IComparable { private int count; private String name; public Object[] getValuesToCompare() { return new Object[] {Integer.valueOf(count), name}; } @Override public int hashCode() { return EqualsBuilder.getHashCode(this); } @Override public boolean equals(Object obj) { return EqualsBuilder.isEqual(this, obj); } }
Update: If you want to use isEqual directly one test should be added to the start:
if (one == two) { return true; }
Thanks to Nyarla for this hint.
Update 2: Thanks to a hint by Alex I fixed a bug in areEqual: when an array (especially a primitive one) is passed than the equals would return a wrong result.
Update 3: The newly added compareTwoArrays method had a bug: it resulted in true if arrayTwo is bigger than arrayOne but starts the same. Thanks to Thierry for pointing that out.
Interesting sample code – I just hope that people don’t use it as boiler plate code for everything since it’ll choke horribly with class cast exceptions as soon as the equal object is a non-comparable object!
Also, you’ve got what looks like a bug in your public “isEqual” method. If both are null (not going to happen from where you call it, but since it is public then it could be called from anywhere) then you get “false”. Surely the two nulls are the same since they’re both uninitialised objects? JUnit’s assertEquals() certainly thinks they are.
Hi,
you say this has advantages over the one from apache commons. What are those?
regards,
Wim
“I know this is pretty basic stuff but many, many programmers are doing it still wrong”. With all due respect, yourself included!
Here are some of the immediate problems I see with your code:
Creating an object array for each call to hashCode() and TWO object arrays for each call to equals(), along with possible boxing of primitives, is a sure-fire way to poor performance and excessive garbage collection whenever using these objects in a Collection or tight loop.
A ClassCastException whenever equals() is called with an object that doesn’t implement IComparable violates the equals() spec.
Calling equals() but passing in a different class that still implements IComparable (including even a subclass of the class being compared against) leads to a possible ArrayIndexOutOfBounds exception.
Those problems aside, I can’t see the benefit anyway. You still have to either type in the IComparable implementation or get your IDE to autogenerate it for each class, at which point you might as well let the IDE generate an optimal equals() and hashcode() instead that doesn’t suffer from any of the above bugs or design flaws.
Can you elaborate on what’s the drawback of Apache’s EqualsBuilder over this one?
Hi,
You may need to consider testing for arrays as part of the areEquals method:
if (objectOne.getClass().isArray() && objectTwo.getClass().isArray()) {
if (objectOne.getClass().isPrimitive() && objectTwo.getClass().isPrimitive()) {
// lots of if/elseif for primitive conversions
return Arrays.equals((…)objectOne,(…)objectTwo)
} else {
return Arrays.deepEquals((Object[])objectOne,(Object[])objectTwo);
}
}
Its a nice idea, but seems a lot of code to do an equals test. I think you’re right when you wrote:
“..use common wisdom to help you code it by hand..”
No better way 🙂
Yes, indeed, this was a bug. Updated the code with the fix. Thanks!
@IBBoard, @Chris: Regarding the ClassCastExceptions: in line 36 there is a test that the class of object one is the same as object two and since object one is an IComparable object two is one also. So casting object two is safe.
@Kevin, @Wim: The major benefit over other approaches like the Apache Commons EqualsBuilder or the handcoding one is the coupling of the fields that hashCode and equals are testing. So it is assured that hashCode and equals are using the same fields to test against.
There is a small error at the isEqual method:
33 if (null == one || null == two) {
34 return false;
35 }
Calling isEqual(null, null) will result in a false!
Just check the identity before to improve performance and correct the error:
if (one == two) return true;
@Nyarla: Added. Thanks
The method private static boolean compareTwoArrays is wrong, you should start by comparing there length if arrayTwo is bigger than arrayOne but starts the same they will be equals.
Yes, you are right. Corrected. Thanks