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;
Note that your use of
getMethod
andcaller.findVirtual(…)
for the same method is redundant. If your starting point is aMethod
, you may useunreflect
, e.g.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
viafindVirtual
is enough.Then, you have to understand the three different function types:
(MyClass,boolean) → void
BiConsumer<MyClass, Boolean>
, which is(MyClass,Boolean) → void
BiConsumer
interface, which is(Object,Object) → void
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 toMyClass
and the second toBoolean
, followed by unwrapping the second argument toboolean
, 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.wrap()
will convert all primitive types to their wrapper type. Unfortunately, this also implies converting the return type toVoid
, so we have to set it back tovoid
again.This gives us the instantiatedMethodType parameter. (Compare with the documentation)
erase()
will convert all reference types toObject
but leave all primitive types as-is. So applying it to the instantiatedMethodType gives us the erased type.It depends on the particular target interface whether this simple transformation is sufficient. For the interfaces in
java.util.function
, it is.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: