英文:
Create BiConsumer from LambdaMetafactory
问题
我试图通过LambdaMetafactory动态创建一个类型为BiConsumer的方法引用。我尝试了两种方法,分别在https://www.cuba-platform.com/blog/think-twice-before-using-reflection/和https://stackoverflow.com/questions/50835353/create-biconsumer-as-field-setter-without-reflection Holger的答案中找到。
然而,在这两种情况下,我都遇到了以下错误:
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)
我的代码类似于以下内容:
public class App {
// ... (其他代码)
@SuppressWarnings("unchecked")
public static BiConsumer<MyClass, Boolean> createHandlerLambda(Class<?> classType) throws Throwable {
// ... (其他代码)
}
// ... (其他代码)
}
public class MyClass {
// ... (其他代码)
}
现在,这个方法不会抛出最初的异常,但是调用这个方法引用的accept方法似乎没有效果。对于这个调用,日志中不会出现 "Called setValid"。只有MyClass::setValid会出现。
英文:
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 https://stackoverflow.com/questions/50835353/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;
答案1
得分: 5
注意,您对相同方法使用 getMethod
和 caller.findVirtual(…)
是多余的。如果您的起始点是一个 Method
,您可以使用 unreflect
,例如:
Method method = classType.getMethod("setValid", boolean.class);
MethodHandles.Lookup caller = MethodHandles.lookup();
MethodHandle target = caller.unreflect(method);
当您动态地发现方法和/或在过程中寻找其他工件(如注释)时,这可能会很有用。否则,只通过 findVirtual
获取 MethodHandle
就足够了。
然后,您必须理解三种不同的函数类型:
- 目标方法句柄具有特定的类型,在将方法句柄隐式传递给工厂时会隐含给出。在您的情况下,它是
(MyClass,boolean) → void
- 与预期结果类型相关联的通用函数类型
BiConsumer<MyClass, Boolean>
,它是(MyClass,Boolean) → void
BiConsumer
接口的擦除类型,它是(Object,Object) → void
只有正确指定所有三种类型,工厂才知道它必须实现方法 void accept(Object,Object)
,其中的代码将第一个参数转换为 MyClass
,第二个参数转换为 Boolean
,然后将第二个参数拆封为 boolean
,最终调用目标方法。
我们可以明确指定这些类型,但为了使代码尽可能可重用,我们可以在目标上调用 type()
,然后使用适配器方法。
wrap()
将所有原始类型转换为其包装类型。不幸的是,这还意味着将返回类型转换为Void
,因此我们必须将其重新设置为void
。
这给我们带来了 instantiatedMethodType 参数。 (与文档进行比较)erase()
将所有引用类型转换为Object
,但将所有原始类型保持不变。因此,将其应用于 instantiatedMethodType 将给我们擦除的类型。
这取决于特定的目标接口是否足够简单。对于java.util.function
中的接口,是这样的。
提高可重用性的另一点是为方法接收器类使用实际的类型参数,因为我们无论如何都会将类作为参数获取:
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();
}
英文:
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:
- The target method handle has a specific type which is given implicitly when passing the method handle to the factory. In your case, it is
(MyClass,boolean) → void
- The generic function type associated with the intended result type
BiConsumer<MyClass, Boolean>
, which is(MyClass,Boolean) → void
- The erased type of the
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 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.
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 injava.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:
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();
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论