Search code examples
javareflectionmethodhandle

Create BiConsumer as Field setter without reflection


I try to get the maximum performance in one of my scripts, without doing a major refactor.

I spotted method that creates a BiConsumer from a Field using reflection.

return (c, v) -> {
    try {
        field.set(c, v);
    } catch (final Throwable e) {
        throw new RuntimeException("Could not set field: " + field, e);
    }
};

Reflection has the reputation of being slow. So I though I could use the method handles.

Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.unreflectSetter(field);
return (c, v) -> {
    try {
        mh.invoke(c, v);
    } catch (final Throwable e) {
        throw new RuntimeException("Could not set field: " + field, e);
    }
};

Which is already a tiny bit faster. However BiConsumer is a FunctionalInterface which could just be generated somehow.

public static <C, V> BiConsumer<C, V> createSetter(final MethodHandles.Lookup lookup,
        final Field field) throws Exception {
    final MethodHandle setter = lookup.unreflectSetter(field);
    final CallSite site = LambdaMetafactory.metafactory(lookup,
            "accept",
            MethodType.methodType(BiConsumer.class),
            MethodType.methodType(void.class, Object.class, Object.class), // signature of method BiConsumer.accept after type erasure
            setter,
            setter.type()); // actual signature of setter
    return (BiConsumer<C, V>) site.getTarget().invokeExact();
}

However then I get an Exception which I don't really understand

Exception in thread "main" java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: putField org.sample.dto.TestDTO.name:(String)void
    at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:182)
    at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303)
    at org.sample.bench.MyBenchmark.createSetter(MyBenchmark.java:120)
    at org.sample.bench.MyBenchmark.main(MyBenchmark.java:114)

In which way do I have to generate that setter correctly to increase the performance. (without actually adding a setter method)


Solution

  • You can use an invoker MethodHandle:

    public static <C, V> BiConsumer<C, V> createSetter(
                         MethodHandles.Lookup lookup, Field field) throws Throwable {
        final MethodHandle setter = lookup.unreflectSetter(field);
        MethodType type = setter.type();
        if(field.getType().isPrimitive())
            type = type.wrap().changeReturnType(void.class);
        final CallSite site = LambdaMetafactory.metafactory(lookup,
            "accept", MethodType.methodType(BiConsumer.class, MethodHandle.class),
            type.erase(), MethodHandles.exactInvoker(setter.type()), type);
        return (BiConsumer<C, V>)site.getTarget().invokeExact(setter);
    }
    

    Since the LambdaMetafactory does not allow to generate a function instance for the field method handle, the code above creates a function instance for a method handle equivalent to invoking invokeExact on the field accessor method handle.

    In the worst case, the generated code would not differ from the code generated for the lambda expression performing a manual invocation of the method handle’s invoke method, so the performance would be on par with

    public static <C, V> BiConsumer<C, V> createSetterU(
                         MethodHandles.Lookup lookup, Field field) throws Throwable {
        MethodHandle setter = lookup.unreflectSetter(field);
        return (c, v) -> {
            try {
                setter.invoke(c, v);
            } catch (final Throwable e) {
                throw new RuntimeException("Could not set field: " + field, e);
            }
        };
    }
    

    However, there are some chances that the invoker method handle is slightly more efficient in some scenarios. First, it uses the equivalent to invokeExact rather than invoke, an option, the generic code can’t use due to type erasure. Second, there might be some internal tweaks when no user code is involved in the code graph.