Java 语言规范第 5 版向 java.lang.Math 和 java.lang.StrictMath 添加了 10 种新方法,Java 6 又添加了 10 种。这个共两部分的系列文章的 第 1 部分 介绍了很有意 义的新的数学方法。它提供了在还未出现计算机的时代中数学家比较熟悉的函数。在第 2 部 分中,我主要关注这样一些函数,它们的目的是操作浮点数,而不是抽象实数。
就像 我在 第 1 部分中 提到的一样,实数(比如 e 或 0.2)和它的 计算机表示(比如 Java double)之间的区别是非常重要的。最理想的数字应该是无限精确 的,然而 Java 表示的位数是固定的(float 为 32 位,double 为 64 位)。float 的最大 值约为 3.4*1038。这个值还不足以表示某些东西,比如宇宙中的电子数目。
double 的最大值为 1.8*10308,几乎能够表示任何物理量。不过涉及到 抽象数学量的计算时,可能超出这些值的范围。例如,光是 171! (171 * 170 * 169 * 168 * ... * 1) 就超出了 double 最大值。float 只能表示 35! 以内的数字。非常小的数(值 接近于 0 的数字)也会带来麻烦,同时涉及到非常大的数和非常小的数的计算是非常危险的 。
为了处理这个问题,浮点数学 IEEE 754 标准(参见 参考资料)添加了特殊值 Inf 和 NaN,它们分别表示无穷大(Infinity)和非数字(Not a Number)。IEEE 754 还定 义了正 0 和负 0(在一般的数学中,0 是不分正负的,但在计算机数学中,它们可以是正的 ,也可以是负的)。这些值给传统的原理带来了混乱。例如,当使用 NaN 时,排中律就不成 立了。x == y 或 x != y 都有可能是不正确的。当 x 或 y 为 NaN 时,这两个式子都不成 立。
除了数字大小问题外,精度是一个更加实际的问题。看看这个常见的循环,将 1.0 相加 10 次之后等到的结果不是 10,而是 9.99999999999998:
for (double x = 0.0; x <= 10.0; x += 0.1) {
System.err.println(x);
}
对于简单的应用程序,您通常让 java.text.DecimalFormat 将最终的输出格式化为与其 值最接近的整数,这样就可以了。不过,在科学和工程应用方面(您不能确定计算的结果是 否为整数),则需要加倍小心。如果需要在特别大的数字之间执行减法以得到较小的数字, 则需要万分 小心。如果以特别小的数字作为除数,也需要加以注意。这些操作能够将很小的 错误变成大错误,并给现实应用带来巨大的影响。由有限精度浮点数字引起的很小的舍入错 误就会严重歪曲数学精度计算。
浮点数和双精度数字的二进制表示
由 Java 实现的 IEEE 754 浮点数有 32 位。第一位是符号位,0 表示正,1 表示负。接 下来的 8 位表示指数,其值的范围是 -125 到 +127。最后的 23 位表示尾数(有时称为有 效数字),其值的范围是 0 到 33,554,431。综合起来,浮点数是这样表示的: sign * mantissa * 2exponent 。
敏锐的读者可能已经注意到这些数字有些不对劲。首先,表示指数的 8 位应该是从 -128 到 127,就像带符号的字节一样。但是这些指数的偏差是 126,即用不带符号的值(0 到 255)减去 126 获得真正的指数(现在是从 -126 到 128)。但是 128 和 -126 是特殊值。 当指数都是 1 位(128)时,则表明这个数字是 Inf、-Inf 或 NaN。要确定具体情况,必须 查看它的尾数。当指数都是 0 位(-126)时,则表明这个数字是不正常的(稍后将详细介绍 ),但是指数仍然是 -125。
尾数一般是一个 23 位的不带符号的整数 — 它非常简单。23 位可以容纳 0 到 224-1,即 16,777,215。等一下,我刚才是不是说尾数的范围是从 0 到 33,554,431?即 225-1。多出的一位是从哪里来的?
因此,可以通过指数表示第 1 位是什么。如果指数都是 0 位,则第 1 位为 0。否则第 1 位为 1。因为我们通常知道第 1 位是什么,所以没有必要包含在数字中。您 “免费” 得 到一个额外的位。是不是有些离奇?
尾数的第 1 位为 1 的浮点数是正常的。即尾数的值通常在 1 到 2 之间。尾数的第 1 位为 0 的浮点数是不正常的,尽管指数通常为 -125,但它通常能够表示更小的数字。
双精度数是以类似的方式编码的,但是它使用 52 位的尾数和 11 位的指数来获得更高的 精度。双精度数的指数的偏差是 1023。