【问题标题】:Create BiConsumer from LambdaMetafactory从 LambdaMetafactory 创建 BiConsumer
【发布时间】:2020-05-04 22:46:33
【问题描述】:

我正在尝试通过 LambdaMetafactory 动态创建 BiConsumer 类型的方法引用。 我试图应用在https://www.cuba-platform.com/blog/think-twice-before-using-reflection/ 上找到的两种方法 - createVoidHandlerLambda 和这里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 {

    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);
    }

}

MyClass 如下所示:

public class MyClass {

    public boolean valid;

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

我将不胜感激。

编辑#1。 在咨询@Holger 之后,我将 createSetter 方法修改为:

@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);
    }

现在这个方法不会抛出初始异常,尽管在这个方法引用上调用 accept 似乎没有效果。我在此调用的日志中没有看到“Called setValid”。仅适用于 MyClass::setValid;

【问题讨论】:

  • 您从this Q&A 复制的代码专门用于直接访问该字段,即无需调用setter。
  • 我知道,Setter 是用于 createHandlerLambda 方法的。基本上我找到了两种方法来解决这个问题(一种来自你的问题),但我不能让它们工作;/对于你的方法,我使用直接访问布尔字段有效。这就是它公开的原因。
  • @Holger,我现在明白了。是的,您的解决方案有效,谢谢。值更改不是通过设置器进行的。这就是为什么我没有看到该日志行。我现在想知道如何使其他解决方案发挥作用。

标签: java reflection method-reference lambda-metafactory


【解决方案1】:

请注意,您对同一方法使用getMethodcaller.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&lt;MyClass, Boolean&gt;,即(MyClass,Boolean) → void
  • BiConsumer接口的擦除类型,即(Object,Object) → void

只有正确指定所有三种类型才能告诉工厂它必须实现方法
void accept(Object,Object),代码将第一个参数转换为MyClass,第二个转换为Boolean,然后解开第二个参数到boolean,最终调用目标方法。

我们可以显式指定类型,但为了使代码尽可能可重用,我们可以在目标上调用type(),然后使用适配器方法。

  • wrap() 会将所有原始类型转换为它们的包装类型。不幸的是,这也意味着将返回类型转换为Void,因此我们必须再次将其设置回void
    这为我们提供了 instantiatedMethodType 参数。 (与the documentation比较)
  • 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();
}

【讨论】:

  • 感谢您的回答。这正是我想要的。
猜你喜欢
  • 1970-01-01
  • 2015-01-02
  • 1970-01-01
  • 1970-01-01
  • 2019-06-25
  • 2021-11-03
  • 2022-12-20
  • 1970-01-01
  • 2021-10-27
相关资源
最近更新 更多