javareflectionmethod-referencelambda-metafactory

Create BiConsumer from LambdaMetafactory


I'm trying to dynamically create a method reference of type BiConsumer through LambdaMetafactory. I was trying to apply two approaches found on https://www.cuba-platform.com/blog/think-twice-before-using-reflection/ - createVoidHandlerLambda and here Create BiConsumer as Field setter without reflection the Holger's answer.

However in both cases I'm having below error:

Exception in thread "main" java.lang.AbstractMethodError: Receiver class org.home.ref.App$$Lambda$15/0x0000000800066040 does not define or inherit an implementation of the resolved method abstract accept(Ljava/lang/Object;Ljava/lang/Object;)V of interface java.util.function.BiConsumer.
    at org.home.ref.App.main(App.java:20)

My code is something like this:

public class App {

    public static void main(String[] args) throws Throwable {
        MyClass myClass = new MyClass();
        BiConsumer<MyClass, Boolean> setValid = MyClass::setValid;
        setValid.accept(myClass, true);

        BiConsumer<MyClass, Boolean> mappingMethodReferences = createHandlerLambda(MyClass.class);
        mappingMethodReferences.accept(myClass, true);
    }

    @SuppressWarnings("unchecked")
    public static BiConsumer<MyClass, Boolean> createHandlerLambda(Class<?> classType) throws Throwable {
        Method method = classType.getMethod("setValid", boolean.class);
        MethodHandles.Lookup caller = MethodHandles.lookup();
        CallSite site = LambdaMetafactory.metafactory(caller,
                "accept",
                MethodType.methodType(BiConsumer.class),
                MethodType.methodType(void.class, MyClass.class, boolean.class),
                caller.findVirtual(classType, method.getName(),
                        MethodType.methodType(void.class, method.getParameterTypes()[0])),
                MethodType.methodType(void.class, classType, method.getParameterTypes()[0]));

        MethodHandle factory = site.getTarget();
        return (BiConsumer<MyClass, Boolean>) factory.invoke();
    }

    public static <C, V> BiConsumer<C, V> createSetter(Class<?> classType) throws Throwable {
        Field field = classType.getDeclaredField("valid");
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        final MethodHandle setter = lookup.unreflectSetter(field);
        final CallSite site = LambdaMetafactory.metafactory(lookup,
                "accept", MethodType.methodType(BiConsumer.class, MethodHandle.class),
                setter.type().erase(), MethodHandles.exactInvoker(setter.type()), setter.type());
        return (BiConsumer<C, V>)site.getTarget().invokeExact(setter);
    }

}

Where MyClass looks like this:

public class MyClass {

    public boolean valid;

    public void setValid(boolean valid) {
        this.valid = valid;
        System.out.println("Called setValid");
    }
}

I will appreciate for help with this one.

EDIT #1. After consulting @Holger I've modified createSetter method to:

@SuppressWarnings("unchecked")
    public static <C, V> BiConsumer<C, V> createSetter(Class<?> classType) throws Throwable {
        Field field = classType.getDeclaredField("valid");
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        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);
    }

Now this method does not throw the initial Exception althoug it seems that calling accept on this method reference has no effect. I do not see "Called setValid" in logs for this call. Only for MyClass::setValid;


Solution

  • Note that your use of getMethod and caller.findVirtual(…) for the same method is redundant. If your starting point is a Method, you may use unreflect, e.g.

    Method method = classType.getMethod("setValid", boolean.class);
    MethodHandles.Lookup caller = MethodHandles.lookup();
    MethodHandle target = caller.unreflect(method);
    

    This might be useful when you discover methods dynamically and/or are looking for other artifacts like annotations in the process. Otherwise, just getting the MethodHandle via findVirtual is enough.

    Then, you have to understand the three different function types:

    Only specifying all three types correctly tells the factory that it must implement the method
    void accept(Object,Object) with code which will cast the first argument to MyClass and the second to Boolean, followed by unwrapping the second argument to boolean, to eventually invoke the target method.

    We could specify the types explicitly, but to make the code as reusable as possible, we can call type() on the target, followed by using adapter methods.

    Another point to raise the reusability is to use an actual type parameter for the method receiver class, as we get the class as parameter anyway:

    public static <T>
           BiConsumer<T, Boolean> createHandlerLambda(Class<T> classType) throws Throwable {
    
        MethodHandles.Lookup caller = MethodHandles.lookup();
        MethodHandle target = caller.findVirtual(classType, "setValid",
            MethodType.methodType(void.class, boolean.class));
        MethodType instantiated = target.type().wrap().changeReturnType(void.class);
    
        CallSite site = LambdaMetafactory.metafactory(caller,
                "accept", MethodType.methodType(BiConsumer.class),
                instantiated.erase(), target, instantiated);
        return (BiConsumer<T, Boolean>)site.getTarget().invoke();
    }