javaequalsrecord

What does Java's record equals() method actually do under the hood?


In my Java code, I have a simple record class just like so:

record SingleIndexProperties<A>(A property) implements IndexProperties {

    // Here go implementations of IndexProperties methods; not relevant to question.

}

When I run a JMH benchmark of code using this record, I notice something interesting in the CPU time flame graph that is generated at the end:

Flame graph showing what the equals() actually does.

This is just an excerpt from a much larger flame graph of the benchmarked code. The interesting thing here is that, for some reason, the default equals() method of the record seems to be doing some dynamic calls.

Naively, I'd expect that the method would do something like this:

public boolean equals(Object o) {
     if (o instanceof SingleIndexProperties other)  {
         return Objects.equals(property, other.property);
     }
     return false;
}

Clearly, that is not the case. What does it actually do instead, and why? I looked for the source code, but I couldn't find anything.

(I'm on OpenJDK Temurin-21.0.1+12. The Pair you see in the flame graph is what is put into the record, and is itself also a record, whose equals() exhibits the same default behavior. Then inside the Pair, there are Integers.)


Solution

  • Please take a look at jdk internal ObjectMethods class, especially bootstrap and makeEquals methods.

    private static MethodHandle makeEquals(Class<?> receiverClass,
                                          List<MethodHandle> getters) {
        MethodType rr = MethodType.methodType(boolean.class, receiverClass, receiverClass);
        MethodType ro = MethodType.methodType(boolean.class, receiverClass, Object.class);
        MethodHandle instanceFalse = MethodHandles.dropArguments(FALSE, 0, receiverClass, Object.class); // (RO)Z
        MethodHandle instanceTrue = MethodHandles.dropArguments(TRUE, 0, receiverClass, Object.class); // (RO)Z
        MethodHandle isSameObject = OBJECT_EQ.asType(ro); // (RO)Z
        MethodHandle isInstance = MethodHandles.dropArguments(CLASS_IS_INSTANCE.bindTo(receiverClass), 0, receiverClass); // (RO)Z
        MethodHandle accumulator = MethodHandles.dropArguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z
    
        for (MethodHandle getter : getters) {
            MethodHandle equalator = equalator(getter.type().returnType()); // (TT)Z
            MethodHandle thisFieldEqual = MethodHandles.filterArguments(equalator, 0, getter, getter); // (RR)Z
            accumulator = MethodHandles.guardWithTest(thisFieldEqual, accumulator, instanceFalse.asType(rr));
        }
    
        return MethodHandles.guardWithTest(isSameObject,
                                           instanceTrue,
                                           MethodHandles.guardWithTest(isInstance, accumulator.asType(ro), instanceFalse));
    }
    

    Seems that current implementation of jdk generates equals as invokedynamic call, which in turns calls many recursive calls over methodhandle composition. I would expect record classes to have bytecode generated equals/hashcode (be it during compilation or runtime), but it seems to be not a case in the current version. (current means JDK 21 and 23 as of time of writing)

    This obviously is a performance hit. As others have mentioned, it is not likely to hit anybody in real life, given the cost of everything else in the application, but if your profiling shows that at all, implementing it by hand for few bottleneck classes might be a good idea.