JDK7发布字节码首位新成员——invokedynamic指令。以实现动态类型语言支持。也是为JDK8里可以顺利实现Lambda表达式而做的技术储备。我们将在本文详细了解动态语言支持这项特性出现的背景和它的意义与价值。
1 动态类型语言
动态类型语言的关键特征是它的类型检查的主体过程是运行期而不是编译器进行的。变量无类型而变量值才有类型。
1.1 类型检查
类型检查是指验证操作接受的是否为合适的类型数据以及赋值是否符合类型要求。最自然的方式是认为检查发生在运行时,即当设计到具体的数据值时,即动态类型检查(也称运行时检查)。编译时检查(静态检查)通过对程序的静态分析,检查所有使用值的使用操作、调用和赋值,在程序运行前排除潜在的类型错误。
public class Test1 {
static class HelloWord {
public void sayHello() {
System.out.println("Hello Java");
}
}
public static void main(String[] args) {
HelloWord helloWord = new HelloWord();
helloWord.sayHello();
}
}
图 上述代码的字节码
Java在编译器将sayHello方法完整的符号引用生成出来,并作为方法调用指令的参数存储到Class文件中。而其他动态类型语言在编译期最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型。
1.2 静态类型语言与动态类型语言的特点
静态类型语言 | 能在编译器确定变量类型,编译器可以提供全面严谨的类型检查,有利于稳定性及让项目容易达到更大的规模。 |
动态类型语言 | 可以为开发人员提供极大灵活性,某些在静态类型语言中要花大量臃肿代码来实现的功能,由动态类型语言去做可能会很清晰简洁,提升开发效率。 |
表 静态类型语言与动态类型语言的特点
2 Java动态语言支持的技术背景
《Java虚拟机规范》第一版中规划了一个愿景:“在未来,我们会对Java虚拟机进行适当的扩展,以便更好地支持其他语言运行于Java虚拟机之上。”目前确实已经有许多动态类型语言运行于Java虚拟机之上了。
JDK7以前,Java虚拟机层面对动态类型语言的支持一直都有所欠缺。主要表现在方法调用方面: 字节码的指令集中,4条方法调用指令的第一个参数都是被调用的方法的符号引用,在编译时就已经确定好。而动态类型语言只有在运行期才能确定方法的接收者。
3 java.lang.invoke包
3.1 方法句柄
方法句柄是一个强类型的,能被直接执行的引用。该引用可以指向常规的静态方法或者实例方法,也可以指向构造器或者字段。当指向字段时,方法句柄实则指向包含字段访问字节码的虚构方法,语义上等价于目标字段的getter或者setter方法。
方法句柄的类型(MethodType)是由所指向方法的参数类型以及返回类型组成的。它是用来确认方法句柄是否适配的唯一关键。当使用方法句柄时,我们并不关心方法句柄所指向方法的类名或者方法名。
3.1.1 方法句柄的使用
方法句柄包含两个重要的类,MethodHandle和MethodType。
MethodHandle调用时有两个方法invoke和invoeExact,后者要求参数类型与底层方法的参数完全匹配,前者则在有出入时做修改如包装类型。
MethodType,不可变对象,是对方法的一个映射,在lookup时也是通过它来寻找的。
public class MethodHandleDemo {
public void sayWord(String word) {
System.out.println("MethodHandleDemo:" + word);
}
public static class TempClass {
public void sayWord(String word) {
System.out.println("TempClass:" + word);
}
}
public static void main(String[] args) throws Throwable {
MethodType methodType = MethodType.methodType(void.class, String.class);
MethodHandle sayWord1 = MethodHandles.lookup().findVirtual(MethodHandleDemo.class, "sayWord", methodType);
sayWord1.invoke(MethodHandleDemo.class.newInstance(),"hello sayWord1");
MethodHandle sayWord2 = MethodHandles.lookup().findVirtual(TempClass.class, "sayWord", methodType);
sayWord2.invoke(TempClass.class.newInstance(),"sayWord2");
}
}
/*
运行结果:
MethodHandleDemo:hello sayWord1
TempClass:sayWord2
*/
MethodHandle方法 | 字节码 | 描述 |
findStatic | invokestatic | 调用静态方法 |
findSpecial | invokespecial | 调用实例构造方法,私有方法,父类方法 |
findVirtual | invokevirtual | 调用所有虚方法 |
findVirtual | invokeinterface | 调用接口方法,会在运行时再确定一个实现此接口的对象 |
表 MethodHandle方法与字节码的对应
3.1.2 方法句柄与反射
/**
* 反射demo
*/
public class ReflectDemo {
public void sayWord(String word) {
System.out.println("ReflectDemo:" + word);
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Class<?> aClass = Class.forName("day06.ReflectDemo");
Method method = aClass.getMethod("sayWord", String.class);
method.invoke(aClass.newInstance(),"hello reflect");
}
}
/*
运行结果:
ReflectDemo:hello reflect
*/
MethodHandle在使用方法和效果上与Reflection有众多相似之处。它们也有以下这些区别:
1)两者本质都是在模拟方法调用,反射是模拟Java代码层次,MethodHandle是模拟字节码层次的方法调用。
2)反射是重量级,而MethodHandle是轻量级。
3.2 invokedynamic指令
每一处含有invokedynamic指令的位置都被称作“动态调用点”,其第一个参数是CONSTANT_InvokeDynamic_info常量。
类型 | 名称 | 含义 |
u1 | tag | 名称,值为InvokeDynamic |
u2 | bootstrap_method_attr_index | 引导方法,有固定的参数,并且返回值规定是CallSite对象,这个对象代表了真正要执行的目标方法调用 |
u2 | name_and_type_index | 方法类型MethodType |
表 CONSTANT_InvokeDynamic_info结构
public class DynamicDemo {
public static void main(String[] args) {
String[] array = {"2hello", "1java", "5other"};
List<String> list = Arrays.asList(array);
list.sort((o1, o2) -> o1.compareTo(o2));
/* 等效于:
list.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
*/
System.out.println(list);
}
}
图 上面代码的部份字节码
#7 后面的0只是作为占位,没其他用处。#0代码引导方法取BootstrapMethod属性表的第0项。