I have created default
methods in an interface for implementing equals(Object)
and hashCode()
in predictable manner. I use reflection to iterate all fields in a type (class) to extract the values and compare them. The code depends on Apache Commons Lang with its HashCodeBuilder
and EqualsBuilder
.
The thing is that my tests shows me that the first time I call on of these methods, it takes a lot more time in the first call. The timer uses System.nanoTime()
. Here is an example from the logs:
Time spent hashCode: 192444
Time spent hashCode: 45453
Time spent hashCode: 48386
Time spent hashCode: 50951
The actual code:
public interface HashAndEquals {
default <T> int getHashCode(final T type) {
final List<Field> fields = Arrays.asList(type.getClass().getDeclaredFields());
final HashCodeBuilder builder = new HashCodeBuilder(31, 7);
fields.forEach( f -> {
try {
f.setAccessible(true);
builder.append(f.get(type));
} catch (IllegalAccessException e) {
throw new GenericException(e.toString(), 500);
}
});
return builder.toHashCode();
}
default <T, K> boolean isEqual(final T current, final K other) {
if(current == null || other == null) {
return false;
}
final List<Field> currentFields = Arrays.asList(current.getClass().getDeclaredFields());
final List<Field> otherFields = Arrays.asList(other.getClass().getDeclaredFields());
final IsEqual isEqual = new IsEqual();
isEqual.setValue(true);
currentFields.forEach(c -> otherFields.forEach(o -> {
c.setAccessible(true);
o.setAccessible(true);
try {
if (o.getName().equals(c.getName())) {
if (!o.get(other).equals(c.get(current))) {
isEqual.setValue(false);
}
}
} catch (IllegalAccessException e) {
isEqual.setValue(false);
}
}));
return isEqual.getValue();
}
}
How these methods are used to implement hashCode
and equals
:
@Override
public int hashCode() {
return getHashCode(this);
}
@Override
public boolean equals(Object obj) {
return obj instanceof Step && isEqual(this, obj);
}
Example of a test:
@Test
public void testEqualsAndHashCode() throws Exception {
Step step1 = new Step(1, Type.DISPLAY, "header 1", "description");
Step step2 = new Step(1, Type.DISPLAY, "header 1", "description");
Step step3 = new Step(2, Type.DISPLAY, "header 2", "description");
int times = 1000;
long total = 0;
for(int i = 0; i < times; i++) {
long start = System.nanoTime();
boolean equalsTrue = step1.equals(step2);
long time = System.nanoTime() - start;
total += time;
System.out.println("Time spent: " + time);
assertTrue( equalsTrue );
}
System.out.println("Average time: " + total / times);
for(int i = 0; i < times; i++) {
assertEquals( step1.hashCode(), step2.hashCode() );
long start = System.nanoTime();
System.out.println(step1.hashCode() + " = " + step2.hashCode());
System.out.println("Time spent hashCode: " + (System.nanoTime() - start));
}
assertFalse( step1.equals(step3) );
}
The reason for putting these methods in an interface is to be as flexible as possible. Some of my classes may need inheritance.
My test indicate that I can trust that both hashcode and equals always returns the same value for objects with the same internal state.
What I'd like to know is if I am missing something. And if the behavior of these methods can be trusted? (I know project Lombok and AutoValue offers some help to implement these methods, but my client are not too keen on these libraries).
Any insight as to why it always takes approximately 5 times longer to perform the method call the first time would also be very helpful.
There is nothing special with default
methods here. The first time you invoke a method on a formerly unused class, the invocation will trigger class loading, verification, and initialization of the class and the method’s execution will start in interpreted mode before the JIT compiler/ hotspot optimizer will kick in. In the case of an interface
, it will be loaded and some of the verification steps performed when a class implementing it has been initialized, however the other steps are still being deferred until it will be actually used, in your case when a default
method of the interface
is invoked for the first time.
It is a normal phenomenon in Java that a first time execution takes more time than subsequent executions. In your case, you are using lambda expressions which have an additional first time overhead when the functional interface implementation will be generated at runtime.
Note that your code is a common antipattern that exists longer than default
methods. There is no is-a relationship between HashAndEquals
and the class “implementing” it. You can (and should) provide these two utility method as static
methods in a dedicated class instead and use import static
if you want to invoke these methods without prepending the declaring class.
There is no benefit in inheriting these methods from an interface
. After all, each class has to override Object.hashCode
and Object.equals
anyway and can choose deliberately whether to use these utility methods or not.