Java虚拟机的字节码指令集的数目从面世以来,只在JDK的发布的时候新增过一条(invokedynamic指令)。这条新增的指令是JDK7的项目目标:实现动态类型语言(Dynamically Typed Language)支持而进行的改进之一,也是为JDK8里可以顺利实现Lambda表达式而做的技术储备。
动态类型语言
动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,满足这个特征的语言有很多,常用的包括:APL、Clojure、Erlang、Groovy、JavaScript、Liso、Lua、PHP、Prolog、Ruby、Smalltalk、Tcl等等。相对的,在编译期就进行类型检查过程的语言,如C++和Java等就是常用的静态类型语言。
通过两个例子来说明什么是“类型检查”和是什么叫“在编译期还是在运行期进行”?
public static void main(String[] args){
int[][][] array = new int[1][0][-1];
}
上面这段Java代码能正常编译,但运行时会出现NegativeArraySizeException异常。在《Java虚拟机规范》中明确规定了NegativeArraySizeException是运行时异常(Runtime Exception),即运行时异常指只要代码不执行到这一行就不会出现问题。与运行时异常相对应的概念就是连接时异常,如常见的NoClassDefFoundError,即导致连接时异常的代码放在一条根本无法被执行的路径分支上,类加载时也照样会抛出异常。
但在C语言里,语义相同的代码就会在编译期就直接报错,而不会等到运行时:
int main(void){
int i[1][0][-1];//GCC拒绝编译,报“size of array is negative”
return 0;
}
由此可看出,一门语言的哪一种检查行为要在运行期进行,哪一种检查要在编译期进行并没有什么必然的因果逻辑关系,关键是在语言规范中认为设定的约定。
obj.printIn("hello world");
先假设这行代码是在Java语言中,且变量obj的静态类型为java.io.PrintStream.那变量obj的实际类型就必须是PrintStream的子类(实现了PrintStream接口的类)才是合法的。否则就算obj属于一个确实包含了printIn(String)方法相同签名的类型,但只要它与PrintStream接口没有继承关系,代码依然不可能运行——因为类型检查不合法。
但相同的代码在ECMAScript(JavaScript)中情况不一样,无论obj具体是何种类型,无论其继承关系如何,只要这种类型的方法定义中确实包含有printIn(String)方法,能够找到相同签名的方法,调用便可成功。
产生这种差别的主要原因是Java语言在编译期就已将printIn(String)方法完整的符号引用(本例为一项CONSTANT_Methodref_info常量)生成出来,并作为方法调用指令的参数存储到Class文件中,如下:
invokevirtual #4;//Method java/io/PrintStream.printIn:(Ljava/lang/String;)v
这个符号引用包含了该方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,Java虚拟机就可以翻译出该方法的直接引用。而ECMAScript等动态型语言与Java有一个核心的差异就是变量obj本身并没有类型,变量obj的值才有类型,所以编译器在编译时最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型(即方法接收者不固定)。“变量无类型而变量值才有类型”这个特点也是动态语言的一个核心特征
静态类型语言能够在编译期确定变量类型,最显著的好处是编译器可以提供全面严谨的类型检查,这样与数据类型相关的潜在问题就能在编码时被及时发现,利于稳定性及让项目容易达到更大的规模。而动态类型语言在运行期才确定类型,这可为开发人员提供极大的灵活性,某些在静态类型语言中要花大量臃肿代码来实现的功能,由动态类型语言去做可能会很清晰简洁,也就意味着开发效率的提高。
《深入理解Java虚拟机》第三版 学习
网友评论