一次编写,随处运行。这是承诺,但 Java 语言有时候并不能做到。诚然,JVM 把跨平台互操作性的程度提到了前所未有的高度,然而,规范和实现级别上的一些小毛病却使得程序无法在多平台上表现出正确的行为。
用 Java 编程的主要优点之一是它给您带来的很大程度的平台无关性。您只要将您的产品编译成字节码,然后分发到任何带有 JVM 的平台就行了,而不必为每个目标平台构建一个独立的构建版。或者说,至少事情应该是这样的。
但事情并没有那么简单。尽管通过对多平台的支持,Java 编程能够为开发者节约无数的时间,但是,不同的 JVM 版本之间存在许多兼容性问题。其中一些问题很容易就可以找到和纠正,例如:在构造路径名的时候使用特定于平台的分隔符字符。但其它问题可能就很难或者不可能截查到。
因此,一些难以解释的不正常的程序行为在某个特定的 JVM 中有可能是一个错误,记住这一点是很重要的。
与供应商相关的错误
当然,如果想看看存在于 JVM 中的众多微妙的与平台相关的错误中的一些,您只需偶而查查 Sun 的 Java Bug Parade(请参阅 参考资料)。这里所列的许多错误都是仅仅适用于某一特定平台上的 JVM 的 实现错误。如果碰巧不在该平台上进行开发,您甚至可能不知道您的程序会在那个平台上受阻。
但是,并非所有的 Java 平台相关性都是 JVM 实现错误的结果。显著的平台相关性是 JVM 规范自身带来的。当 JVM 的细节在规范级别上不受限制时,就可能在 JVM 之间产生与供应商相关的行为。
例如,正如我们回顾“ Improve the performance of your Java code”(2001 年 5 月)所看到的,JVM 规范对 尾递归调用(tail-recursive call)的优化不作要求。尾递归调用就是作为方法的最后一个操作出现的递归的方法调用。更一般地说,任何方法调用,不管是不是递归的,只要出现在方法的末尾就是 尾调用(tail call)。例如,考虑以下简单的代码:
清单 1. 一个尾递归的 factorial
public class Math {
public int factorial(int n) {
return _factorial(n, 1);
}
private int _factorial(int n, int result) {
if (n <= 0) {
return result;
}
else {
return _factorial(n - 1, n * result);
}
}
}
在这个示例中,公共的 factorial方法和私有的 助手方法 _factorial 都包含尾调用; factorial 包含一个对 _factorial 的尾调用, _factorial 包含一个对它自身的尾递归调用。
如果您觉得用这种办法编写 factorial特别复杂,那您并不是唯一有这种感受的人。为什么不用如下自然得多的形式编写它呢?
清单 2. 一个纯递归的 factorial
public class Math {
int factorial(int n) {
if (n <= 0) {
return 1;
}
else {
return n * factorial(n-1);
}
}
}
回答是尾递归考虑到了很强有力的优化 ― 尾递归让我们用为 被调方法构建的堆栈帧来代替为 主调方法构建的堆栈帧。这可以极大地减小运行时的堆栈深度,从而避免堆栈溢出(尤其是如果尾调用是递归的话,例如清单 2 中对 _factorial 的尾调用)。
有些 JVM 实现这种优化;有些则不然。结果是有些程序在有些平台上会引起堆栈溢出,在其它平台上则不会。要是这种优化可以静态地进行,我们就可以只将字节码编译成尾调用优化过的形式,这样就能同时享有平台无关性和这种优化。不幸的是,正如我在上面所引用的讨论这个主题的文章中所讲解的那样,这种优化无法静态地进行。