用invokedynamic实现Java多分派(2)——MethodHandles API

invokedynamic是一条虚拟机指令,但在Java语言层面上,并没有提供生成该指令的接口(以前有过,但删掉了)。因为这条指令不是给Java应用程序开发人员用的,而是给在JVM上实现其它语言的人用的。JVM上实现了许多其它语言,如javascript,这些语言的变量并没有静态类型,它们是弱类型语言,而具体类型要到运行期才会知道,这就使得用Java实现起来比较麻烦,因此催生了JSR292。JSR292除了引入一条虚拟机指令外,还连带着一组辅助API,即MethodHandle家族,也同样为了提高JVM语言的开发和运行效率(比反射快)。

简而言之,一个MethodHandle就是一个函数句柄或者叫函数指针,可以使用它快速调用任何函数。相对于反射API里的Method对象,它是个轻量级的东西,是个更底层的工具。

作为系列的第二篇,本文将介绍MethodHandle家族的几个函数。我们第一篇已经简单直接地演示过用MethodHandle和MethodType对类里的方法进行“查找——调用”的过程。这是最浅尝辄止的一类,也是大部分关于该API的文章会载的。而本文要介绍的则是稍微深入一点的几个函数,它们对本系列的下一篇是一个铺垫。这几个函数是一个工具类MethodHandles提供的静态方法。

一、MethodHandles.insertArguments(MethodHandle target, int pos, Object... values)

MethodHandles.insertArguments方法可以把实际参数分批次放入一个MethodHandle中,在Lambda演算中,这叫做Curry,意思是"放料"。例子如下:

package mhtest;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import static java.lang.invoke.MethodType.methodType;
import static java.lang.invoke.MethodHandles.*;

public class MethodHandleTest {
    
    public static void assertEquals(Object o1, Object o2) throws Exception {
        if (!o1.equals(o2))
            throw new Exception("Not equal: " + o1 + ", " + o2);
    }
    
    static void log(Object o) {
        System.out.println(o);
    }

...

    public static void testInsertArguments() throws Throwable {
        MethodHandle target = lookup().findVirtual(
                String.class, "indexOf", 
                methodType(int.class, String.class, int.class)).bindTo("abcdefgabc");
        log("target: " + target);
        MethodHandle curried1 = insertArguments(target, 1, 3); //先放入第二个参数3
        log("curried1: " + curried1);
        MethodHandle curried2 = insertArguments(curried1, 0, "bc"); //又放入第一个参数"bc"
        log("curried2: " + curried2);
        int ret = (int)curried2.invokeExact();
        assertEquals(8, ret);
    }
...
}

在以上代码中,首先,我们找出String这个类的indexOf方法,并用一个MethodType指明返回值为int,两个参数分别为String和int,也就是 String.indexOf(String substr, int begin) 这个方法。然后给这个方法句柄绑定了一个接收者(receiver):字符串"abcdefgabc",也就是要调用"abcdefgabc".indexOf(String substr, int begin)。此时绑定了接收者,没有绑定实参。

接着,我们调用了两次MethodHandles.insertArguments(),放入了两个参数(顺序可以任意),而每次调用都会返回一个新的方法句柄,以作为下一次调用的参数。最后使用MethodHandle.invoke或invokeExact来进行调用即可。

另外值得注意的是,输出中打印出来的target,curried1和curried2分别为:

target: MethodHandle(String,int)int
curried1: MethodHandle(String)int
curried2: MethodHandle()int

可见,每绑定一个参数,都会导致新返回的MethodHandle的类型少一个参数。我们可以推知,如果刚开始不调用bindTo,只调用到findVirtual这一步的话,返回的MethodHandle类型会是MethodHandle(String,String,int)int,其中括号内第一个String是指receiver的类型,后面的是参数类型,而最后括号外面那个int是返回值类型。

二、MethodHandles.dropArguments(MethodHandle target, int pos, List<Class<?>> valueTypes)

顾名思义,MethodHandles.dropArguments是insertArguments的逆过程,但是理解起来却没那么直观。

    public static void testDropArguments() throws Throwable {
        MethodHandle cat = lookup().findVirtual(String.class, "concat", 
                methodType(String.class, String.class));
        log("cat: " + cat);
        assertEquals("xy", (String) cat.invokeExact("x", "y"));
        MethodType bigType = cat.type().insertParameterTypes(0, int.class, String.class);
        log("bigType: " + bigType);
        
        MethodHandle mh = dropArguments(cat, 0, bigType.parameterList().subList(0, 2));
        log("mh: " + mh.type());
        assertEquals(bigType, mh.type());

        String s = (String) mh.invokeExact(123, "x", "y", "z");
        assertEquals("yz", s);
    }

输出是:

cat: MethodHandle(String,String)String
bigType: (int,String,String,String)String
mh: (int,String,String,String)String

首先,我们拿到一个函数的句柄:String.concat(String another),返回String。我用红绿蓝分别表示接收者类型、参数类型和返回值类型。

然后我们取得这个函数句柄的type,并且调用insertParameter给它凭空加上了两个参数,一个int型,一个String型,加在最前面(第0位置)。从输出我们看出,bigType确实多了两个参数。

接着,我们调用了dropArguments,它生成一个新的MethodHandle,并指示,在调用的时候,要从第0位置,删掉bigType的前两个参数。同时我们从输出观察到,在调用dropArguments后,返回的MethodHandle(mh)多出了两个参数,这与insertArguments正好相反:insertArguments是每绑定n个参数,返回的MH就减少n个参数;而dropArguments每解绑n个参数,返回的MH就增加n个相应类型的参数。结合以下两句来看:

    MethodHandle mh = dropArguments(cat, 0, bigType.parameterList().subList(0, 2));

    String s = mh.invokeExact(123, "x", "y", "z")

它会做成一个新方法,用cat来调用,但在实际调用时,会把实参中符合bigType前两个参数类型的实参去掉,用剩下的参数去调用。即去除冗余参数。在这里,cat是实际调用方法,dropArguments以及0和后面那个list是指示对实际调用前采取的预处理措施。这种写法不直观,尤其在不熟悉前面那个insertArguments函数的情况下,乍看之下,还以为要把cat这个函数本身的形参去掉,其实是把实际执行调用时的实参去掉。我们可以用如下伪代码来理解:

    invokeExact "cat" on: (123, "x", "y", "z").dropArguments(0, bigType.parameterList().subList(0, 2))

即:

    invokeExact "cat" on: ("y", "z")

即:

    "y".cat("z")

可见,把这个MethodHandle的定义和其调用结合起来反而更好理解。

当然,就纯Java程序来看,上面这个例子没有任何意义,直接将"y"和"z"传给cat就好了,何必画蛇添足多此一举。还是那句话,这些方法(实际上是函数拼装工具)是给动态语言作者用的,在将寄宿语言翻译成宿主语言(Java)的过程中会用到。

三、MethodHandles.foldArguments(MethodHandle target, MethodHandle combiner)

foldArguments是在调用执行时将实际参数拷贝给后一个函数句柄(combiner),combiner有几个参数就拷几个,先调用这个combiner,然后将控制流交给target——即foldArguments会调用combiner,不会调用target,target仍由invoke()或invokeExact()来调用。假如combiner有返回值,那么这个返回值作为target的第一个参数(这种情况下,必须保证target接受的第一个参数和这个值类型匹配),而原始实参作为第二个、第三个…

也就是说,combiner的参数类型必须是target的一个子集,而且顺序要么从target第一个参数开始对齐(没有返回值);要么从target第二个参数开始对齐(有返回值)并且返回类型和target第一个参数相同。

比如,我们让target为String类的如下方法:

    int indexOf(String substr)

让combiner为String类的如下方法:

    String toUpperCase()

那么,由于combiner有返回值,所以它的第一个参数要与target的第二个参数开始对齐,这里,它们都是空(没有),所以对齐了。同时,combiner的返回值类型与targe的第一个参数相同,都是String,因此符合foldArguments的要求。这同时意味着,在实际调用invoke()或者invokeExact()时,传入的参数应该从target的第二个开始,这里是空。

public static void testFoldArguments() throws Throwable {
        
        // target will call "SayHELLO".indexOf(substr), argument substr will be provided by combiner
        MethodHandle target = publicLookup().findVirtual(String.class, "indexOf", 
                methodType(int.class, String.class)).bindTo("SayHELLO");
        log("target: " + target);
        
        // combiner will call "hello".toUpperCase(), it will return "HELLO"
        MethodHandle combiner = lookup().findVirtual(String.class, "toUpperCase",
                methodType(String.class)).bindTo("hello");
        log("combiner: " + combiner);

        // folded will call "SayHELLO".indexOf("HELLO")
        MethodHandle folded = foldArguments(target, combiner);
        log("folded: " + folded);
        
        assertEquals(3, (int)folded.invokeExact());
    }

输出:

target: MethodHandle(String)int
combiner: MethodHandle()String
folded: MethodHandle()int

显然,foldArguments是用第二个函数(combiner)的执行结果作为第一个函数(target)的条件,或输入。另外,返回的MethodHandle(folded),类型里已经没有最初那个String参数了,combiner的返回值与target的第一个参数相消了。

四、结合fold与drop

    public static void testDropAndFoldArguments() throws Throwable {
        
        // target will call "SayHELLO".indexOf(substr), argument substr will be provided by combiner
        MethodHandle target = publicLookup().findVirtual(String.class, "indexOf",
                methodType(int.class, String.class)).bindTo("SayHELLO");
        log("target: " + target);
        
        // combiner will call "hello".toUpperCase(), it will return "HELLO"
        MethodHandle combiner = lookup().findVirtual(String.class, "toUpperCase",
                methodType(String.class)).bindTo("hello");
        log("combiner: " + combiner);

        // folded will call "SayHELLO".indexOf("HELLO")
        MethodHandle folded = foldArguments(target, combiner);
        log("folded: " + folded);
        
        // because we know that we will (somehow) call the folded MH with a dummy string argument, 
        // we drop it in advance.
        MethodHandle adaptor = dropArguments(folded, 0, String.class);
        
        log("adaptor: " + adaptor); 
        
        assertEquals(3, (int)adaptor.invokeExact("dummy"));
    }

输出:

target: MethodHandle(String)int
combiner: MethodHandle()String
folded: MethodHandle()int
adaptor: MethodHandle(String)int

这个例子和上面的例子基本一样,除了最后用dropArguments然后多加一个参数“画蛇添足”一番外。

在下一篇里我们将使用这些知识以及invokedynamic虚拟机指令实现多分派功能。注意,我们完全可以使用invokevirtual或invokestatic来实现多分派;Java本身是不需要用invokedynamic的,我们只不过借个由头试用invokedynamic罢了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值