本帖最后由 如梦初醒 于 2012-4-14 09:44 编辑
动态类型语言
在介绍JDK 7提供的动态类型语言支持之前,我们要先弄明白动态类型语言是什么?它与Java语言、Java虚拟机有什么关系?了解JDK 7提供动态类型语言支持的技术背景,对理解这个语言特性是很必要的。
何谓动态类型语言[2]?动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,满足这个特征的语言有很多,常用的包括:APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tcl等等。那相对地,在编译期就进行类型检查过程的语言,如C++和Java等就是最常用的静态类型语言。
j觉上面定义过于概念化?那我们不妨通过两个例子以最浅显的方式来说明什么是“在编译期/运行期进行”和什么是“类型检查”。首先看这段简单的Java代码,它是否能正常编译和运行?
public static void main(String[] args) {
int[][][] array = new int[1][0][-1];
} 这段代码能够正常编译,但运行的时候会报NegativeArraySizeException异常。在《Java虚拟机规范》中明确规定了NegativeArraySizeException是一个运行时异常,通俗一点说,运行时异常就是只要代码不运行到这一行就不会有问题。与运行时异常相对应的是连接时异常,例如很常见的NoClassDefFoundError便属于连接时异常,即使会导致连接时异常的代码放在一条无法执行到的分支路径上,类加载时(Java的连接过程不在编译阶段,而在类加载阶段)也照样会抛出异常。
不过,在C语言里,含义相同的代码的代码就会在编译期报错:
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接口没有继承关系,代码依然不能运行——因为类型检查不合法。
但是相同的代码在ECMAScript(JavaScript)中情况则不一样,无论obj具体是何种类型,只要这种类型的定义中确实包含有println(String)方法,那方法调用便可成功。
这种差别产生的原因是Java语言在编译期间却已将println(String)方法完整的符号引用(本例中为一项CONSTANT_InterfaceMethodref_info常量)生成出来,作为方法调用指令的参数存储到Class文件中,例如下面这个样子:
invokevirtual #4; //Method java/io/PrintStream.printlnLjava/lang/String;)V
这个符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,虚拟机就可以翻译出这个方法的直接引用(譬如方法内存地址或者其他实现形式)。而在ECMAScript等动态类型语言中,变量obj本身是没有类型的,变量obj的值才具有的类型,编译时候最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型(方法接收者不固定)。“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征。
了解了动态和静态类型语言的区别后,也许读者的下一个问题就是动态、静态类型语言两者谁更好,或者谁更加先进?这种比较不会有确切答案,它们都有自己的优点,选择哪种语言是需要权衡的事情。静态类型语言在编译期确定类型,最显著的好处是编译器可以提供严谨的类型检查,这样与类型相关的问题能在编码的时候就及时发现,利于稳定性及代码达到更大的规模。而动态类型语言在运行期确定类型,这可以为开发人员提供更大的灵活性,某些在静态类型语言中要花大量臃肿代码来实现的功能,由动态类型语言来实现可能会很清晰简洁,清晰简洁通常也就意味着开发效率的提升。
另外,动态、静态类型语言的界限并非泾渭分明,如C#是一门静态类型语言,但是在.NET 4.0中新增加的dynamic关键字(它作用就是用来描述一个“无类型”的变量)以及相应的DLR支持后,C# 4.0便拥有了动态语言的特性。
JDK 7与动态类型
现在,我们回到本专栏的主题,来看看Java语言、虚拟机与动态类型语言之间有什么关系。Java虚拟机毫无疑问是Java语言的运行平台,但它的使命并不仅限于此,早在1997年出版的《Java虚拟机规范》第一版中就规划了这样一个愿景:“在未来,我们会对Java虚拟机进行适当的扩展,以便更好的支持其他语言运行于Java虚拟机之上”。而目前确实已经有许多动态类型语言运行于Java虚拟机之上了,如Clojure、Groovy、Jython和JRuby等等,能够在同一个虚拟机之上可以实现静态类型语言的严谨与动态类型语言的灵活,这是一件很美妙的事情。
但遗憾的是Java虚拟机层面对动态类型语言的支持一直都有所欠缺,主要表现在方法调用方面:JDK 7以前字节码指令集中,四条方法调用指令(invokevirtual、invokespecial、invokestatic、invokeinterface)的第一个参数都是被调用的方法的符号引用(CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量),前面已经提到过,方法的符号引用在编译时产生,而动态类型语言只有在运行期才能确定接收者类型。这样,在Java虚拟机上实现的动态类型语言就不得不使用“曲线救国”的方式(如编译时留个占位符类型,运行时动态生成字节码实现具体类型到占位符类型的适配)来实现,这样势必让动态类型语言实现的复杂度增加,也可能带来额外的性能或者内存开销。尽管可以想一些办法(如Call Site Caching)让这些开销尽量变小,但这种底层问题终归是应当在虚拟机层次上去解决才最合适,因此在Java虚拟机层面上提供动态类型的直接支持就成为了Java平台的发展趋势之一,这就是JDK 7(JSR-292)中invokedynamic指令以及java.lang.invoke包出现的技术背景。
|
|