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罢了。