执行篇:解析JDK 7的动态类型语言支持
声明:本文为笔者原创,但首发于InfoQ中文站,详见文末声明。
Java虚拟机的字节码指令集的数量自从Sun公司的第一款Java虚拟机问世至JDK 7来临之前的十余年时间里,一直没有发生任何变化[注1]。随着JDK 7的发布,字节码指令集终于迎来了第一位新成员——invokedynamic指令。这条新增加的指令是JDK 7实现“动态类型语言(Dynamically Typed Language)”支持而进行的改进之一,也是为JDK 8可以顺利实现Lambda表达式做技术准备。在这篇文章中,我们将去了解JDK 7这项新特性的出现前因后果和它的意义。
动态类型语言
在介绍JDK 7提供的动态类型语言支持之前,我们要先弄明白动态类型语言是什么?它与Java语言、Java虚拟机有什么关系?了解JDK 7提供动态类型语言支持的技术背景,对理解这个语言特性是很必要的。
何谓动态类型语言注释[注2]?动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,满足这个特征的语言有很多,常用的包括:APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tcl等等。那相对地,在编译期就进行类型检查过程的语言,如C++和Java等就是最常用的静态类型语言。
觉得上面定义过于概念化?那我们不妨通过两个例子以最浅显的方式来说明什么是“在编译期/运行期进行”和什么是“类型检查”。首先看这段简单的Java代码,它是否能正常编译和运行?
public static void main(String[] args) { int[][][] array = new int[1][0][-1]; }这段代码能够正常编译,但运行的时候会报NegativeArraySizeException异常。在《Java虚拟机规范》中明确规定了NegativeArraySizeException是一个运行时异常,通俗一点说,运行时异常就是只要代码不运行到这一行就不会有问题。与运行时异常相对应的是连接时异常,例如很常见的NoClassDefFoundError便属于连接时异常,即使会导致连接时异常的代码放在一条无法执行到的分支路径上,类加载时(Java的连接过程不在编译阶段,而在类加载阶段)也照样会抛出异常。
int main(void) { int i[1][0][-1]; // GCC拒绝编译,报“size of array is negative” return 0;}由此看来,一门语言的哪一种检查行为要在运行期进行,哪一种检查要在编译期进行并没有必然的因果逻辑关系,关键是在语言规范中人为规定的,再举一个例子来解释“类型检查”,例如下面这一句再普通不过的代码:
obj.println(“hello world”);显然,这行代码需要一个具体的上下文才有讨论的意义,假设它在Java语言中,并且变量obj的类型为java.io.PrintStream,那obj的值就必须是PrintStream的子类(实现了PrintStream接口的类)才是合法的。否则,哪怕obj属于一个确实有用println(String)方法,但与PrintStream接口没有继承关系,代码依然不能运行——因为类型检查不合法。
invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V这个符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,虚拟机就可以翻译出这个方法的直接引用(譬如方法内存地址或者其他实现形式)。而在ECMAScript等动态类型语言中,变量obj本身是没有类型的,变量obj的值才具有的类型,编译时候最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型(方法接收者不固定)。“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征。
void sort(int list[], const int size, int (*compare)(int, int))但Java语言中做不到这一点,没有办法单独把一个函数作为参数进行传递。普遍的做法是设计一个带有compare()方法的Comparator接口,以实现了这个接口的对象作为参数,例如Collections.sort()就是这样定义的:
void sort(List list, Comparator c)不过,在拥有Method Handle之后,Java语言也可以拥有类似于函数指针或者委托的方法别名的工具了。下面代码演示了MethodHandle的基本用途,无论obj是何种类型(临时定义的ClassA抑或是实现PrintStream接口的实现类System.out),都可以正确调用到println()方法。
import static java.lang.invoke.MethodHandles.lookup;import java.lang.invoke.MethodHandle;import java.lang.invoke.MethodType;/** * JSR 292 MethodHandle基础用法演示 * @author IcyFenix */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文件的字节码上的,而是通过一个具体方法来实现。而这个方法本身的返回值(MethodHandle对象),可以视为对最终调用方法的一个“引用”。以此为基础,有了MethodHandle就可以写出类似于这样的函数声明了:
void sort(List list, MethodHandle compare)从上面的例子看来,使用MethodHandle并没有多少困难,不过看完它的用法之后,读者大概就会疑问到,相同的事情,用反射不是早就可以实现了吗?
import static java.lang.invoke.MethodHandles.lookup;import java.lang.invoke.CallSite;import java.lang.invoke.ConstantCallSite;import java.lang.invoke.MethodHandle;import java.lang.invoke.MethodHandles;import java.lang.invoke.MethodType;public class InvokeDynamicTest { public static void main(String[] args) throws Throwable { INDY_BootstrapMethod().invokeExact("icyfenix"); } public static void testMethod(String s) { System.out.println("hello String:" + s); } public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable { return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt)); } private static MethodType MT_BootstrapMethod() { return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null); } private static MethodHandle MH_BootstrapMethod() throws Throwable { return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod()); } private static MethodHandle INDY_BootstrapMethod() throws Throwable { CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), "testMethod", MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null)); return cs.dynamicInvoker(); }}这段代码与前面MethodHandleTest的作用基本上是一样的,虽然笔者没有加以注释,但是阅读起来应当不困难。真没读懂也不要紧,我没写注释的原因是这段代码并非写给人看的(@_@,我不是在骂人)。由于目前光靠Java语言的编译器javac没有办法生成带有invokedynamic 指令的字节码(曾经有一个java.dyn.InvokeDynamic的语法糖可以实现,但后来被取消了),所以只能用一些变通的办法,John Rose(Da Vinci Machine Project的Leader)编写了一个把程序的字节码转换为使用invokedynamic的简单工具INDY[注4]来完成这件事情,我们要使用这个工具来产生最终要的字节码,因此这个示例代码中的方法名称不能乱改,更不能把几个方法合并到一起写。
Constant pool: #121 = NameAndType #33:#30 // testMethod:(Ljava/lang/String;)V #123 = InvokeDynamic #0:#121 // #0:testMethod:(Ljava/lang/String;)V public static void main(java.lang.String[]) throws java.lang.Throwable; Code: stack=2, locals=1, args_size=1 0: ldc #23 // String abc 2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V 7: nop 8: return public static java.lang.invoke.CallSite BootstrapMethod(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.invoke.MethodType) throws java.lang.Throwable; Code: stack=6, locals=3, args_size=3 0: new #63 // class java/lang/invoke/ConstantCallSite 3: dup 4: aload_0 5: ldc #1 // class org/fenixsoft/InvokeDynamicTest 7: aload_1 8: aload_2 9: invokevirtual #65 // Method java/lang/invoke/MethodHandles$Lookup.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle; 12: invokespecial #71 // Method java/lang/invoke/ConstantCallSite."<init>":(Ljava/lang/invoke/MethodHandle;)V 15: areturn从main()方法的字节码中可见,原本的方法调用指令已经被替换为invokedynamic了,它的参数为第123项常量(第二个值为0的参数在HotSpot中用不到,与invokeinterface那个的值为0的参数一样是占位的):
2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V从常量池中可见,第123项常量显示“#123 = InvokeDynamic #0:#121”说明它是一项CONSTANT_InvokeDynamic_info类型常量,常量值中前面“#0”代表引导方法取BootstrapMethods属性表的第0项(javap没有列出属性表的具体内容,不过示例中仅有一个引导方法,即BootstrapMethod()),而后面的“#121”代表引用第121项类型为CONSTANT_NameAndType_info的常量,从个常量中可以获取方法名称和描述符,既后面输出的“testMethod:(Ljava/lang/String;)V”。