一种语言要在JVM上运行可能有两种方式:
- 有一个产生类文件的编译器;
- 有一个用JVM字节码实现的解释器。
无论哪种方式,通常都会有个运行时环境为执行该语言编写的程序提供支持。图7-4展示了Java和一种典型非Java语言的运行时环境栈。
图7-4 非Java语言运行时支持
这些运行时支持系统复杂度各不相同,主要取决于给定的非Java语言运行时所要掌握的资源数量。几乎在所有情况下,运行时都会作为可执行程序类路径(classpath)上的一组JAR文件,并且要在程序执行开始之前启动。
本书的重点是编译型语言。至于Rhino等解释型语言,只是为了内容的完整性才提一下,所以我们不会在上面花太多篇幅。在本节剩下的内容中,我们会介绍备选语言所需的运行时支持(甚至还包括编译型语言),然后探讨编译器小说(compiler fiction)——那些可能不会出现在底层字节码中、由编译器合成的该语言特有的功能。
7.5.1 非Java语言的运行时环境
有一种评估语言运行时环境复杂度的简单办法:看运行时实现中JAR文件的大小。按这个标准,Clojure的运行时环境量级很轻,而JRuby语言则需要更多支持。
这个标准其实不是特别公平,因为有些语言把很多类库和功能都打包在标准发布版里了,而有些却不这么做。但如果不细究的话,它是个挺有用的经验。
通常来说,运行时环境是要帮助非Java语言的类型系统和其他特性符合期望的语义。备选语言对基本编程概念的看法有时和Java并不完全一致。
比如,Java的面向对象方式并不是放之四海而皆准的。在Ruby中,运行时可以往一个单独的对象里添加一个额外的方法,而这个方法在定义类时还不知道是什么,也不是在相同类的其他实例上定义的。JRuby的实现需要把这个属性(不太明白为什么叫“开放类”)照搬过来。而这种高级支持只能放在JRuby的运行时中。
7.5.2 编译器小说
语言的某些特性是由编程环境和高层语言合成的,在底层JVM中根本不存在。这些特性称为编译器小说。我们在第6章已经遇到过一个例子了——Java的字符串合并。
提示 你应该了解一下这些特性是如何实现的,否则你的代码可能跑得慢,甚至可能毁掉整个过程。有时运行时环境要做大量的工作来合成某个特性。
Java中的编译器小说还包括检查型异常和内部类(如有必要,内部类总会被转换成带有特殊合成访问方法的顶层类,如图7-5所示)。如果你曾经探究过JAR文件的内部(用jar tvf
),见到过许多名字中有$
的类,它们就是被取出并转换成“常规”类的内部类。
图7-5 用编译器小说实现的内部类
备选语言也有编译器小说。某些情况下,这些编译器小说甚至形成了语言的核心功能。我们来看两个重要的例子。
1. 函数是一等值
我们在7.1节介绍了函数式编程的关键概念——函数应该是能放到变量中的值。这通常说成“函数是一等值”。我们也指出Java对函数的建模方式并不太好。
本书第三部分讨论的所有非Java语言都把函数当做一等值。也就是说函数可以放在变量中、传给方法,并可以像操作其他任何值一样操作。JVM只能把类当做最小的代码和功能单元,所以现在所有的非Java语言都用小型匿名类作为函数的载体(不过这在Java 8中可能有所改变)。
解决源码和JVM字节码之间这种差异的办法是,记住对象只是把数据和操作数据的方法绑在一起的东西。请想象一个没有状态、只有一个方法的对象,比如第4章Callable
接口的匿名实现类。把这样一个对象放到变量中,作为参数传递,然后调用它的call
方法,这一切都很正常,像这样:
Callable<String> myFn = new Callable<String> { @Override public String call { return /"The result/"; }};try { System.out.println(myFn.call);} catch (Exception e) {}
我们把异常处理忽略掉了,因为在这个例子中myFn
的call
方法不可能抛出异常。
注意 在这个例子中,
myFn
变量是一个匿名类型,所以在编译后它看起来应该是个类似NameOfEnclosingClass$1.class
的东西。类的序号从1开始,并且编译器每遇到一个就加1。如果它们是动态创建的,并且数量很多(就像在JRuby语言中那样),会对存放类定义的PermGen内存区域造成压力。
尽管Java对此没有任何特殊的语法,但Java程序员经常用这个技巧创建匿名实现类。这就是说结果可能会有点冗长。我们所讨论的所有语言都提供了编写这些函数值(也叫函数字面值、匿名函数)的特殊语法。它们是函数式编程风格的支柱,Scala和Clojure做得都不错。
2. 多继承
还有一个例子,在Java(和JVM)中没办法表示实现的多继承。实现多继承的唯一办法就是使用接口,可它不允许有任何具体的方法。
相反,在Scala中特性机制(trait)允许把方法的实现混合到类中,所以它提供了不同的继承视图。我们会在第9章全面介绍。现在只要记住这种行为必须由Scala的编译器和运行时合成,在VM层面不提供这种特性。
对JVM上可用的不同类型的语言及其独特功能的实现方式就介绍到这里。