诊断Java代码: 平台相关性“gotcha问题”

一次编写,随处运行。这是承诺,但 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 实现这种优化;有些则不然。结果是有些程序在有些平台上会引起堆栈溢出,在其它平台上则不会。要是这种优化可以静态地进行,我们就可以只将字节码编译成尾调用优化过的形式,这样就能同时享有平台无关性和这种优化。不幸的是,正如我在上面所引用的讨论这个主题的文章中所讲解的那样,这种优化无法静态地进行。

时间: 2024-10-31 15:18:11

诊断Java代码: 平台相关性“gotcha问题”的相关文章

诊断Java代码

诊断Java代码: Broken Dispatch错误模式 诊断Java代码: Double Descent错误模式 诊断Java代码: Impostor Type错误模式 诊断Java代码: Java编程中的断言和时态逻辑 诊断Java代码: Liar View错误模式 诊断Java代码: Repl提供交互式评价 诊断Java代码: 单元测试与自动化代码分析协同工作 诊断Java代码: 将时态逻辑用于错误模式 诊断Java代码: 进行记录器测试以正确调用方法 诊断Java代码: 空标志错误模式

诊断Java代码: 进行记录器测试以正确调用方法

用 JUnit进行单元测试是一个功能强大的方法,它可以确保您的代码基础的完整性,但是一些不变量比其他(方法调用序列是其中一种)更难测试.在诊断Java 代码这一部分,Eric Allen描述了怎样在您的单元测试中使用记录器(一种特殊的侦听器),来确保一个方法调用序列按恰当的顺序发生.请点击文章顶部和底部的 讨论,与作者和其他读者在论坛上分享您关于本文的看法. 随着时间的推移,当系统开发人员,维护人员甚至是系统详细说明改变时,JUnit 框架提供一个很好的方法来改善系统的坚固性.通过测试,您可以检

诊断Java代码: Impostor Type错误模式

当使用字段中特殊的标记来区别对象类型时,可能会产生标记对相关数据误贴标签的错误 ― 通称为 Impostor Type 错误模式.在诊断 Java 代码的这一部分中,Eric Allen 对这个错误的症状和起因进行了分析,详细说明了预防错误发生的方法,并讨论了一种吸引人的混合实现方法,这种方法不使用 impostor type,但最后,还是有很多相同的缺点产生.请在 讨论论坛与作者及其他读者分享您对本文的看法. 程序中除了最无关紧要的部分外都要对某些数据类型进行操作.静态类型系统提供了一种方法,

诊断Java代码: 设计可扩展应用程序,第3部分

对应于我们上一篇" 诊断 Java 代码"中所讨论的透明盒可扩展性,黑盒可扩展性是指,在源代码既不能查看也不能修改时,可以扩展软件系统的方法.通常通过系统配置或使用特定于应用程序的脚本语言来进行这样的扩展.在本专题中,Eric Allen 讨论了何时设计黑盒可 扩展性的系统是有意义的,并提供了如何有效地实现这一设计的一些想法.阅读了本文后,您将知道何时使用黑盒并掌握如何实现它的一些技巧. 我已在以前的文章中谈到了代码重用设计策略的重要性(主要是因为各种信息处理任务的差异和相应费用的增加

诊断Java代码: 设计可扩展的应用程序,第2部分

玻璃箱可扩展性是指这样一种方式:软件系统可在源代码可以查看而不可以修改时被扩展 ― 它是黑箱设计(在这里构建扩展时,不查看原始代码)和开放箱设计(扩展代码直接写入到基础代码)的折衷.因为新的扩展直接建立在原始代码基础上,但不改动原始代码,所以,玻璃箱设计或许是扩展一个软件系统最有效.最安全的方法.在 诊断 Java 代码的这一部分中,Eric Allen 详述了上个月谈及的玻璃箱可扩展性主题.读完本文后,您将知道什么时候使用玻璃箱,并将获得一些如何实现它的提示. 随着信息处理任务(和与之相关的成

诊断Java代码:孤线程(Orphaned Thread)错误模式

在多线程代码中,使用驱动其它线程所负责的动作的单个主线程是常见的.这个主线程发送消息,通常是通过把它们放到一个队列中,然后其它线程处理这些消息.但是如果主线程抛出一个异常,那么剩余的线程会继续运行,等待更多输入到该队列,导致程序冻结.在诊断 Java 代码的这一部分中,专职 Java 开发者兼兼职捉虫者 Eric Allen 讨论检测.修复和避免这一错误模式. 用多线程编写代码对程序员大有好处.多线程能使编程(和程序)进行得快得多,而且代码能有效得多地使用资源.然而,跟生活中的很多事情一样,多线

诊断Java代码: 在规范钢丝上行走

要构建可靠的软件,程序规范很关键.没有良好定义的规范,很难诊断软件系统的异常行为.但是很多软件系统的程序规范定义得很差劲.而且更糟的,是许多软件系统根本就没有规范. 直观的看,程序规范是对程序行为的一种描述.它可以采取许多形式,但无论采取何种形式,都有一条主线贯穿所有实例:必须有某种类型的系统规范,因为您得依靠它来判断系统是否运转正常. 规范可以形式化也可以松散地定义,这取决于开发中系统的稳定性和危险程度,还与开发完毕后修改系统的容易程度有关. 我们将通过讨论规范为什么重要.为什么会经常被忽略以

诊断 Java 代码:设计轻松的代码维护

设计 本月,Eric Allen 解释了在使代码更易于维护的同时,避免和控制无理由的变化怎么会是保持代码健壮性的关键.他集中讨论了诸如函数样式代码编写之类的概念,以及标记字段.方法和类的方法来处理并防止可变性.Eric 还解释了本任务中单元测试和重构的角色,并提供了协助实现重构的两个工具.在相关论坛中与作者和其他读者分享您对本文的看法.(您也可以单击本文顶部或底部的"讨论",访问该论坛.)有效调试源自良好的编程.设计易于维护的程序是程序员面临的最困难挑战之一,其部分原因在于程序通常并不

诊断Java代码: 将时态逻辑用于错误模式

尽管传统的断言可以增加对 Java 代码所作的检查次数,但仅用它们,还是有许多检查无法完成.处理这种情况的方法之一就是使用 时态逻辑.请回忆上个月的文章" Assertions and temporal logic in Java programming",时态逻辑有助于提供比程序中的方法更有力的断言,从而有助于增强用其它方式难以正式表达的不变量. 我们不必费力搜寻去发现有助于防止我们程序出错的许多有用的不变量.实际上,可以通过使用此类时态逻辑断言来加大我们消除一些最常见错误模式的力度