Caching hashCodes
Normally, a hashCode is calculated every time you call the hashCode() method. That’s usually not a problem, since the calculation is usually not very complex.
Sometimes, however, a hashCode needs to be cached for performance reasons. Java’s own String class is a good example where the hashCode is only calculated once, and then cached.
Here is another example of a class which implements a cached hashCode:
class ObjectWithCachedHashCode {
private final String name;
private final int cachedHashCode;
public ObjectWithCachedHashCode(String name) {
this.name = name;
this.cachedHashCode = calcHashCode();
}
@Override
public final int hashCode() {
return cachedHashCode;
}
private int calcHashCode() {
return name.hashCode();
}
// equals method elided for brevity
}The easiest way to deal with this, is to use #withFactory():
@Test
public void testCachedHashCode() {
EqualsVerifier.forClass(ObjectWithCachedHashCode.class)
.withFactory(v -> new ObjectWithCachedHashCode(v.getString("name")))
.withIgnoredFields("cachedHashCode")
.verify();
}Note that you have to ignore the cachedHashCode field, since it doesn’t participate in equals directly.
For more information on how to use #withFactory(), read the chapter on “final means final”
EqualsVerifier has another, older way of dealing with cached hashCodes too, that hooks into its traditional way of instantiating objects by using reflection. It takes some work, and it only works with immutable classes, so we recommend the above method using #withFactory(). In fact, this method may be deprecated in the future. Still, if you want to use it, there are three things you have to do.
-
First, your class must contain a
private intfield that contains the cached hashCode. -
Second, the class must have a method that can calculate the hashCode. This method can have no parameters, it cannot be public, and it must return the hashCode as an
int. That means that the method can’t assign to the field that contains the cached hashCode. The assignment has to happen in the constructor. It also means you can’t do the calculation in thehashCode()method. -
Finally, you must give EqualsVerifier an example of an object with a correctly initialized hashCode. EqualsVerifier uses this to make sure that your class isn’t cheating, and that the method from the second point is actually used to assign to the field from the first point.
These three elements must be passed to EqualsVerifier’s withCachedHashCode method.
All of this is pretty cumbersome, but it’s necessary for technical reasons. There certainly are easier ways to correctly implement cached hashCodes, but EqualsVerifier can only test them if they’re implemented in this particular way.
Here is an example of an EqualsVerifier test that exercises this cached hashCode:
@Test
public void testCachedHashCode() {
EqualsVerifier.forClass(ObjectWithCachedHashCode.class)
.withCachedHashCode("cachedHashCode", "calcHashCode",
new ObjectWithCachedHashCode("something"))
.verify();
}Note that EqualsVerifier will fail the test if cachedHashCode or calcHashCode can’t be found in the class.
When a class is hard to construct
Sometimes it’s hard to construct a one-off instance of a class to serve as an example (see the third point, above). The class may have a lot of dependencies, or might be part of an elaborate inhertiance hierarchy. If this is the case, and you have other ways to test that the hashCode is indeed initialized when the object is constructed, there is a way to bypass the example:
EqualsVerifier.forClass(ObjectWithCachedHashCode.class)
.withCachedHashCode("cachedHashCode", "calcHashCode", null)
.suppress(Warning.NO_EXAMPLE_FOR_CACHED_HASHCODE)
.verify();The code for this (suppressing a warning with a very long name, passing a null value) is intentionally left a bit ugly, to urge you to do this only when it’s absolutely necessary 😉.