这个谜题之所以被称为长整除是因为它所涉及的程序是有关两个 long 型数值整
除的。被除数表示的是一天里的微秒数;而除数表示的是一天里的毫秒数。这个
程序会打印出什么呢?
public class LongDivision{
public static void main(String args[]){
final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
}
}
这个谜题看起来相当直观。每天的毫秒数和每天的微秒数都是常量。为清楚起见,
它们都被表示成积的形式。每天的微秒数是(24 小时/天*60 分钟/小时*60 秒/
分钟*1000 毫秒/秒*1000 微秒/毫秒)。而每天的毫秒数的不同之处只是少了最
后一个因子 1000。
当你用每天的毫秒数来整除每天的微秒数时,除数中所有的因子都被约掉了,只
剩下 1000,这正是每毫秒包含的微秒数。
除数和被除数都是 long 类型的,long 类型大到了可以很容易地保存这两个乘积
而不产生溢出。因此,看起来程序打印的必定是 1000。
遗憾的是,它打印的是 5。这里到底发生了什么呢?
问题在于常数 MICROS_PER_DAY 的计算“确实”溢出了。尽管计算的结果适合放
入 long 中,并且其空间还有富余,但是这个结果并不适合放入 int 中。这个计
算完全是以 int 运算来执行的,并且只有在运算完成之后,其结果才被提升到
long,而此时已经太迟了:计算已经溢出了,它返回的是一个小了 200 倍的数值。
从int提升到long是一种拓宽原始类型转换 (widening primitive conversion),
它保留了(不正确的)数值。这个值之后被 MILLIS_PER_DAY 整除,而
MILLIS_PER_DAY 的计算是正确的,因为它适合 int 运算。这样整除的结果就得
到了 5。
那么为什么计算会是以int运算来执行的呢?因为所有乘在一起的因子都是int
数值。当你将两个 int 数值相乘时,你将得到另一个 int 数值。Java 不具有目
标确定类型的特性,这是一种语言特性,其含义是指存储结果的变量的类型会影
响到计算所使用的类型。
通过使用 long 常量来替代 int 常量作为每一个乘积的第一个因子,我们就可以
很容易地订正这个程序。这样做可以强制表达式中所有的后续计算都用 long 运
作来完成。尽管这么做只在 MICROS_PER_DAY 表达式中是必需的,但是在两个乘
积中都这么做是一种很好的方式。相似地,使用 long 作为乘积的“第一个”数
值也并不总是必需的,但是这么做也是一种很好的形式。在两个计算中都以 long
数值开始可以很清楚地表明它们都不会溢出。下面的程序将打印出我们所期望的
1000:
public class LongDivision{
public static void main(String args[ ]){
final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
}
}
这个教训很简单:当你在操作很大的数字时,千万要提防溢出——它可是一个缄
默杀手。即使用来保存结果的变量已显得足够大,也并不意味着要产生结果的计
算具有正确的类型。当你拿不准时,就使用 long 运算来执行整个计算。
语言设计者从中可以吸取的教训是: 也许降低缄默溢出产生的可能性确实是值得
做的一件事。这可以通过对不会产生缄默溢出的运算提供支持来实现。程序可以
抛出一个异常而不是直接溢出,就像 Ada 所作的那样,或者它们可以在需要的时
候自动地切换到一个更大的内部表示上以防止溢出,就像 Lisp 所作的那样。这
两种方式都可能会遭受与其相关的性能方面的损失。 降低缄默溢出的另一种方式
是支持目标确定类型,但是这么做会显著地增加类型系统的复杂度