8.4.3 java.lang.invoke包

8.4.3 java.lang.invoke包

JDK 7时新加入的java.lang.invoke包[^1]是JSR 292的一个重要组成部分,这个包的主要目的是在之前 单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制,称 为“方法句柄”(Method Handle)。这个表达听起来也不好懂?那不妨把方法句柄与C/C++中的函数指 针(Function Pointer),或者C#里面的委派(Delegate)互相类比一下来理解。举个例子,如果我们要 实现一个带谓词(谓词就是由外部传入的排序时比较大小的动作)的排序函数,在C/C++中的常用做 法是把谓词定义为函数,用函数指针来把谓词传递到排序方法,像这样:

1
void sort(int list[], const int size, int (*compare)(int, int))

但在Java语言中做不到这一点,没有办法单独把一个函数作为参数进行传递。普遍的做法是设计一个带有compare()方法的Comparator接口,以实现这个接口的对象作为参数,例如Java类库中的Collections::sort()方法就是这样定义的:

1
void sort(List list, Comparator c)

不过,在拥有方法句柄之后,Java语言也可以拥有类似于函数指针或者委托的方法别名这样的工具了。代码清单8-12演示了方法句柄的基本用法,无论obj是何种类型(临时定义的ClassA抑或是实现PrintStream接口的实现类System.out),都可以正确调用到println()方法。

代码清单8-12 方法句柄演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
/**
* JSR 292 MethodHandle基础用法演示
* @author zzm
*/
public class MethodHandleTest {
static class ClassA {
public void println(String s) {
System.out.println(s);
}
}
public static void main(String[] args) throws Throwable {
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
// 无论obj最终是哪个实现类,下面这句都能正确调用到println方法。
getPrintlnMH(obj).invokeExact("icyfenix");
}
private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
// MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数)。
MethodType mt = MethodType.methodType(void.class, String.class);
// lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。
// 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接收者,也即this指向的对象,这个参数以前是放在参数列表中进行传递,现在提供了bindTo() 方法来完成这件事情。
return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
}
}

方法getPrintlnMH()中实际上是模拟了invokevirtual指令的执行过程,只不过它的分派逻辑并非固化在Class文件的字节码上,而是通过一个由用户设计的Java方法来实现。而这个方法本身的返回值 (MethodHandle对象),可以视为对最终调用方法的一个“引用”。以此为基础,有了MethodHandle就可以写出类似于C/C++那样的函数声明了:

1
void sort(List list, MethodHandle compare)

从上面的例子看来,使用MethodHandle并没有多少困难,不过看完它的用法之后,读者大概就会产生疑问,相同的事情,用反射不是早就可以实现了吗?

确实,仅站在Java语言的角度看,MethodHandle在使用方法和效果上与Reflection有众多相似之处。不过,它们也有以下这些区别:

  • Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次 的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的3个方法 findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual(以及 invokeinterface)和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用 Reflection API时是不需要关心的。
  • Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的 java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java端的全面映像,包含了方法 的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含执行权限等的运行期信息。而 后者仅包含执行该方法的相关信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle 是轻量级。
  • 由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化 (如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还在继续完善 中),而通过反射去调用方法则几乎不可能直接去实施各类调用点优化措施。

MethodHandle与Reflection除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前 提“仅站在Java语言的角度看”之后:Reflection API的设计目标是只为Java语言服务的,而MethodHandle 则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已,而且Java在这里并不是主 角。

[^1]: 这个包曾经在不算短的时间里的名称是java.dyn,也曾经短暂更名为java.lang.mh,如果读者在其他 资料上看到这两个包名,可以把它们与java.lang.invoke理解为同一种东西。