【问题标题】:Implement duck typing using java MethodHandles使用 java MethodHandles 实现鸭子类型
【发布时间】:2020-09-30 21:55:00
【问题描述】:

我有两个类AB,都定义了具有共同签名的方法foo()(不接受任何内容,返回无效)。它们没有声明此方法的通用基类(或接口)。我想在 As 或 B 上调用这个方法,只要他们能响应这个调用。这种方法称为Duck Typing

我知道有一条指令叫invokedynamic

invokedynamic 指令的每个实例都称为动态调用 地点。动态调用站点最初处于未链接状态,这 表示没有指定调用站点调用的方法。 如前所述,动态调用站点通过以下方式链接到方法 引导方法的手段。动态调用站点的引导方法是 编译器为动态类型语言指定的方法 JVM 调用一次以链接站点。返回的对象 从引导方法永久确定调用站点的 行为。

所以我尝试使用MethodHandles 来实现这一目标。示例如下:

public static class A {
    public void foo() {
    }
}

public static class B {
    public void foo() {
    }
}

public static void main(String[] args) throws Throwable {
    final MethodHandle foo = MethodHandles.lookup()
            .findVirtual(A.class, "foo", MethodType.methodType(void.class));

    foo.invoke(new B());
}

当然,我有:

Exception in thread "main" java.lang.ClassCastException: Cannot cast Main$B to Main$A
    at sun.invoke.util.ValueConversions.newClassCastException(ValueConversions.java:461)
    at sun.invoke.util.ValueConversions.castReference(ValueConversions.java:456)
    at Main.main(Main.java:30)

我清楚地看到了invokedynamicMethodHanle 之间的区别。我看到问题在于foo MethodHandle 绑定到class A,而不是class B。但是在这种特殊情况下,我有可能以某种方式利用invokedynamic 吗?

我为什么需要这个?这是我的小型研究项目的一部分。我正在尝试深入了解方法句柄,并且我想在从字段和方法中检索到的注释实例上调用常用方法。我无法在 Java 中为注释定义基类,因此我希望尽可能实现这种鸭子类型,而不是使用 instanceof 链和类强制转换或使用违反访问权限的反射检索这些值。

谢谢。

【问题讨论】:

  • 您当前的问题与invokedynamic 或任何问题无关,您需要一个正确类的实例来调用实例方法。当然,您不能在B 对象上调用A 方法,因为A 实现foo() 可能取决于仅存在于A 中的字段/数据/私有函数。 invokedynamic 也需要一个实例。在静态方法上,您可以将null 作为this 参数传递给invoke,但实际上:只有当您已经拥有该对象的实例时,鸭子类型才有效,而您缺少atm :)

标签: java annotations duck-typing methodhandle invokedynamic


【解决方案1】:

当 VM 第一次遇到 invokedynamic 指令时,它会调用工厂方法或“引导”方法,该方法会返回一个目标是实现实际功能的 CallSite 对象。您可以使用MutableCallSite 自己实现它,该MutableCallSite 在第一次调用时查找您的目标方法,然后将其自己的目标设置为查找的方法。

但是,这还不足以满足您的目的。当您遇到新的接收者类型时,您希望重新链接呼叫站点。

这里是一个例子(目前只支持findVirtual):

class DuckTypingCallSite extends MutableCallSite {

    private static final MethodHandle MH_relink;
    private static final MethodHandle MH_isInstance;

    static {
        try {
            MH_relink = lookup().findVirtual(DuckTypingCallSite.class, "link", methodType(Object.class, Object[].class));
            MH_isInstance = lookup().findVirtual(Class.class, "isInstance", methodType(boolean.class, Object.class));
        } catch (ReflectiveOperationException e) {
            throw new InternalError(e);
        }
    }

    private final MethodHandles.Lookup lookup;
    private final String methodName;
    private final MethodType lookupType;

    private DuckTypingCallSite(MethodHandles.Lookup lookup, String methodName, MethodType lookupType) {
        super(lookupType.insertParameterTypes(0, Object.class)); // insert receiver
        this.lookup = lookup;
        this.methodName = methodName;
        this.lookupType = lookupType;
    }

    public static DuckTypingCallSite make(MethodHandles.Lookup lookup, String methodName, MethodType lookupType) {
        DuckTypingCallSite cs = new DuckTypingCallSite(lookup, methodName, lookupType);
        cs.setTarget(MH_relink.bindTo(cs).asCollector(Object[].class, cs.type().parameterCount()).asType(cs.type()));
        return cs;
    }

    public Object link(Object[] args) throws Throwable {
        Object receiver = args[0];
        Class<?> holder = receiver.getClass();
        MethodHandle target = lookup.findVirtual(holder, methodName, lookupType).asType(type());

        MethodHandle test = MH_isInstance.bindTo(holder);
        MethodHandle newTarget = guardWithTest(test, target, getTarget());
        setTarget(newTarget);

        return target.invokeWithArguments(args);
    }

}

在第一次调用之前,调用调用站点的动态调用程序将直接跳转到link 方法,该方法将查找目标方法然后调用它,以及重新链接 DuckTypingCallSite 以基本上缓存查找到的 MethodHandle , 由类型检查保护。

在第一次调用之后,这实际上创建了一个 if/else,如下所示:

if (A.class.isInstance(receiver)) {
    // invoke A.foo
} else {
    // re-link
}

然后当遇到第二种类型时,它会变成这样:

if (B.class.isInstance(receiver)) {
    // invoke B.foo
} else if (A.class.isInstance(receiver)) {
    // invoke A.foo
} else {
    // re-link
}

等等

这是一个示例用法:

public class DuckTyping {

    private static final MethodHandle MH_foo = DuckTypingCallSite.make(lookup(), "foo", methodType(void.class)).dynamicInvoker();

    private static void foo(Object receiver) {
        try {
            MH_foo.invokeExact(receiver);
        } catch (Throwable throwable) {
            throw new IllegalStateException(throwable);
        }
    }

    public static void main(String[] args) {
        foo(new A()); // prints "A.foo"
        foo(new B()); // prints "B.foo"
    }
}

class A {
    public void foo() {
        System.out.println("A.foo");
    }
}

class B {
    public void foo() {
        System.out.println("B.foo");
    }
}

【讨论】:

  • 谢谢,它工作得很好!但它有多快?我看到relink() 每个新类型只被调用一次。您已将其显示为伪代码中的 ifs 链,但真正发生了什么?再次感谢您。
  • @Netherwire ifs 链几乎就是发生了什么。一段时间后,MethodHandle 链将被渲染为字节码,这将像往常一样进行优化。如果您对内部实现感兴趣,我建议您查看一些关于它的 JVMLS 讨论:Lambda Forms & State of java.lang.invoke
猜你喜欢
  • 2011-05-18
  • 1970-01-01
  • 1970-01-01
  • 2015-05-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多