Testing java equals and hashcode methods
Write a java test for the equals method! What is the point in wasting time on that?
All good java programmers know the equals and hashcode methods are vitally important. I have seen some unpredictable behavior through bugs in these two methods.
If your about to click away to something more interesting fine, but first read my page with an example equals bug. See if you can spot the problem before I show the solution.
Writing a test for the equals method
Writing a test for equals is so easy its tedious. Which is perhaps why so much code gets written and not tested. First from the javadoc what are the specifications for a good equals and hashcode implementations?
Suns javadoc for the equals method
- It is reflexive: for any non-null reference value x, x.equals(x) should return true.
- It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
- It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
- It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
- For any non-null reference value x, x.equals(null) should return false.
Suns javadoc for the hashcode method
- Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
- If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
- It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hashtables.
Simple implementations for equals and hashcode
These two implementations were built using the generator in netbeans. Arguably some further optimisation could be done, especially if the class were made fully immutable. They are fine for the purpose of this blog.
public final class SimpleBean {
private Integer goodInt = 0;
public SimpleBean(final Integer goodInt) {
this.goodInt = goodInt;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final SimpleBean other = (SimpleBean) obj;
if (this.goodInt != other.goodInt
&& (this.goodInt == null
|| !this.goodInt.equals(other.goodInt))) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 41 * hash + (this.goodInt != null ? this.goodInt.hashCode() : 0);
return hash;
}
}
Test class for SimpleBean just showing the equals and hashcode tests
import org.junit.Test;
import static org.junit.Assert.*;
public class TestSimpleBean {
static final class Fixture {
static SimpleBean x = new SimpleBean(new Integer(100));
static SimpleBean y = new SimpleBean(new Integer(100));
static SimpleBean z = new SimpleBean(new Integer(100));
static SimpleBean notx = new SimpleBean(new Integer(1));
}
@Test
/**
* A class is equal to itself.
*/
public void testEqual_ToSelf() {
assertTrue("Class equal to itself.", Fixture.x.equals(Fixture.x));
}
/**
* x.equals(WrongType) must return false;
*
*/
@Test
public void testPassIncompatibleType_isFalse() {
assertFalse("Passing incompatible object to equals should return false", Fixture.x.equals("string"));
}
/**
* x.equals(null) must return false;
*
*/
@Test
public void testNullReference_isFalse() {
assertFalse("Passing null to equals should return false", Fixture.x.equals(null));
}
/**
* 1. x, x.equals(x) must return true.
* 2. x and y, x.equals(y) must return true if and only if y.equals(x) returns true.
*/
@Test
public void testEquals_isReflexive_isSymmetric() {
assertTrue("Reflexive test fail x,y", Fixture.x.equals(Fixture.y));
assertTrue("Symmetric test fail y", Fixture.y.equals(Fixture.x));
}
/**
* 1. x.equals(y) returns true
* 2. y.equals(z) returns true
* 3. x.equals(z) must return true
*/
@Test
public void testEquals_isTransitive() {
assertTrue("Transitive test fails x,y", Fixture.x.equals(Fixture.y));
assertTrue("Transitive test fails y,z", Fixture.y.equals(Fixture.z));
assertTrue("Transitive test fails x,z", Fixture.x.equals(Fixture.z));
}
/**
* Repeated calls to equals consistently return true or false.
*/
@Test
public void testEquals_isConsistent() {
assertTrue("Consistent test fail x,y", Fixture.x.equals(Fixture.y));
assertTrue("Consistent test fail x,y", Fixture.x.equals(Fixture.y));
assertTrue("Consistent test fail x,y", Fixture.x.equals(Fixture.y));
assertFalse(Fixture.notx.equals(Fixture.x));
assertFalse(Fixture.notx.equals(Fixture.x));
assertFalse(Fixture.notx.equals(Fixture.x));
}
/**
* Repeated calls to hashcode should consistently return the same integer.
*/
@Test
public void testHashcode_isConsistent() {
int initial_hashcode = Fixture.x.hashCode();
assertEquals("Consistent hashcode test fails", initial_hashcode, Fixture.x.hashCode());
assertEquals("Consistent hashcode test fails", initial_hashcode, Fixture.x.hashCode());
}
/**
* Objects that are equal using the equals method should return the same integer.
*/
@Test
public void testHashcode_twoEqualsObjects_produceSameNumber() {
int xhashcode = Fixture.x.hashCode();
int yhashcode = Fixture.y.hashCode();
assertEquals("Equal object, return equal hashcode test fails", xhashcode, yhashcode);
}
/**
* A more optimal implementation of hashcode ensures
* that if the objects are unequal different integers are produced.
*
*/
@Test
public void testHashcode_twoUnEqualObjects_produceDifferentNumber() {
int xhashcode = Fixture.x.hashCode();
int yhashcode = Fixture.notx.hashCode();
assertTrue("Equal object, return unequal hashcode test fails", !(xhashcode == yhashcode));
}
}
Conclusions
To me the benefit is clear. Its very easy to break these two methods. Converting one of the attribute types can easily break equals and possibly hashcode. Its essential to have a test in place to prevent the bug going unnoticed.
Note on nulls
In some cases extra tests may be required for null checking on both sides. For instance in the case where an object relies upon an inner class to provide it a key, and its possible for this key to be null.:
/**
* x.key = null, y.key = null, must return false.
*/
@Test
public void testKeysNull_NotEqual() {
Thing noKeyAvailableA = new Thing();
Thing noKeyAvailableB = new Thing ();
assertFalse(noKeyAvailableA.equals(noKeyAvailableB));
}
/**
* x.key= null, y.key != null, must return false.
*/
@Test
public void testKeyNearsideNullFarsideNotNull_NotEqual() {
Fixture fixture = new Fixture();
Thing noKeyAvailable = new Thing ();
assertFalse(noKeyAvailable.equals(fixture.x));
}
/**
* y.key = null, x.key != null, must return false.
*/
@Test
public void testKeyFarsideNotNullFarsideNull_NotEqual() {
Fixture fixture = new Fixture();
Thing noKeyAvailable = new Thing ();
assertFalse(fixture.x.equals(noKeyAvailable));
}
Comments are closed.
