Java理论与实践:您的小数点在哪?
Java理论与实践:您的小数点在哪?
??使用浮点数和小数中的技巧和陷阱
作者:Brian Goetz 本文选自:IBM DW中国网站 2003年04月15日
许多程序员在其整个开发生涯中都不曾使用定点或浮点数,可能的例外是,偶尔在计
时测试或基准测试程序中会用到。Java 语言和类库支持两类非整数类型 ? IEEE 754 浮
点(float 和 double,包装类(wrapper class)为 Float 和 Double),以及任意精度
的小数(java.math.BigDecimal)。在本月的 Java 理论和实践中,Brian Goetz 探讨了
在 Java 程序中使用非整数类型时一些常碰到的陷阱和“gotcha”。
虽然几乎每种处理器和编程语言都支持浮点运算,但大多数程序员很少注意它。这容
易理解 ? 我们中大多数很少需要使用非整数类型。除了科学计算和偶尔的计时测试或基
准测试程序,其它情况下几乎都用不着它。同样,大多数开发人员也容易忽略
java.math.BigDecimal 所提供的任意精度的小数 ? 大多数应用程序不使用它们。然而,
在以整数为主的程序中有时确实会出人意料地需要表示非整型数据。例如,JDBC 使用
BigDecimal 作为 SQL DECIMAL 列的首选互换格式。
IEEE 浮点
Java 语言支持两种基本的浮点类型:float 和 double,以及与它们对应的包装类
Float 和 Double。它们都依据 IEEE 754 标准,该标准为 32 位浮点和 64 位双精度浮
点二进制小数定义了二进制标准。
IEEE 754 用科学记数法以底数为 2 的小数来表示浮点数。IEEE 浮点数用 1 位表示
数字的符号,用 8 位来表示指数,用 23 位来表示尾数,即小数部分。作为有符号整数
的指数可以有正负之分。小数部分用二进制(底数 2)小数来表示,这意味着最高位对应
着值 ?(2-1),第二位对应着 ?(2-2),依此类推。对于双精度浮点数,用 11 位表示指
数,52 位表示尾数。IEEE 浮点值的格式如图 1 所示。
图 1. IEEE 754 浮点数的格式
因为用科学记数法可以有多种方式来表示给定数字,所以要规范化浮点数,以便用底
数为 2 并且小数点左边为 1 的小数来表示,按照需要调节指数就可以得到所需的数字。
所以,例如,数 1.25 可以表示为尾数为 1.01,指数为 0:
除了编码所允许的值的标准范围(对于 float,从 1.4e-45 到 3.4028235e+38),
还有一些表示无穷大、负无穷大、-0 和 NaN(它代表“不是一个数字”)的特殊值。这些
值的存在是为了在出现错误条件(譬如算术溢出,给负数开平方根,除以 0 等)下,可
以用浮点值集合中的数字来表示所产生的结果。 这些特殊的数字有一些不寻常的特
征。例如,0 和 -0 是不同值,但在比较它们是否相等时,被认为是相等的。用一个非零
数去除以无穷大的数,结果等于 0。特殊数字 NaN 是无序的;使用 ==、< 和 > 运算符
将 NaN 与其它浮点值比较时,结果为 false。如果 f 为 NaN,则即使 (f == f) 也会得
到 false。如果想将浮点值与 NaN 进行比较,则使用 Float.isNaN() 方法。表 1 显示
了无穷大和 NaN 的一些属性。
表 1. 特殊浮点值的属性
表达式 结果
Math.sqrt(-1.0) -> NaN
.0 / 0.0 -> NaN
1.0 / 0.0 -> 无穷大
-1.0 / 0.0 -> 负无穷大
NaN + 1.0 -> NaN
无穷大 + 1.0 -> 无穷大
无穷大 + 无穷大 -> 无穷大
NaN > 1.0 -> false
NaN == 1.0 -> false
NaN < 1.0 -> false
NaN == NaN -> false
0.0 == -0.01 -> true
基本浮点类型和包装类浮点有不同的比较行为
使事情更糟的是,在基本 float 类型和包装类 Float 之间,用于比较 NaN 和 -0 的规
则是不同的。对于 float 值,比较两个 NaN 值是否相等将会得到 false,而使用
Float.equals() 来比较两个 NaN Float 对象会得到 true。造成这种现象的原因是,如
果不这样的话,就不可能将 NaN Float 对象用作 HashMap 中的键。类似的,虽然 0 和
-0 在表示为浮点值时,被认为是相等的,但使用 Float.compareTo() 来比较作为 Float
对象的 0 和 -0 时,会显示 -0 小于 0。
浮点中的危险
由于无穷大、NaN 和 0 的特殊行为,当应用浮点数时,可能看似无害的转换和优化
实际上是不正确的。例如,虽然好象 0.0-f 很明显等于 -f,但当 f 为 0 时,这是不正
确的。还有其它类似的 gotcha,表 2 显示了其中一些 gotcha。
表 2. 无效的浮点假定
这个表达式…… 不一定等于…… 当……
0.0 - f -f f 为 0
f < g ! (f >= g) f 或 g 为 NaN
f == f true f 为 NaN
f + g - g f g 为无穷大或 NaN
舍入误差
浮点运算很少是精确的。虽然一些数字(譬如 0.5)可以精确地表示为二进制(底数
2)小数(因为 0.5 等于 2-1),但其它一些数字(譬如 0.1)就不能精确的表示。因
此,浮点运算可能导致舍入误差,产生的结果接近 ? 但不等于 ? 您可能希望的结果。例
如,下面这个简单的计算将得到 2.600000000000001,而不是 2.6:
double s=0;
for (int i=0; i<26; i++)
s += 0.1;
System.out.println(s);
类似的,.1*26 相乘所产生的结果不等于 .1 自身加 26 次所得到的结果。当将浮点
数强制转换成整数时,产生的舍入误差甚至更严重,因为强制转换成整数类型会舍弃非整
数部分,甚至对于那些“看上去似乎”应该得到整数值的计算,也存在此类问题。例如,下
面这些语句:
double d = 29.0 * 0.01;
System.out.println(d);
System.out.println((int) (d * 100));
将得到以下输出:
0.29
28
这可能不是您起初所期望的。
浮点数比较指南
由于存在 NaN 的不寻常比较行为和在几乎所有浮点计算中都不可避免地会出现舍入
误差,解释浮点值的比较运算符的结果比较麻烦。
最好完全避免使用浮点数比较。当然,这并不总是可能的,但您应该意识到要限制浮
点数比较。如果必须比较浮点数来看它们是否相等,则应该将它们差的绝对值同一些预先
选定的小正数进行比较,这样您所做的就是测试它们是否“足够接近”。(如果不知道基本
的计算范围,可以使用测试“abs (a/b - 1) < epsilon”,这种方法比简单地比较两者之
差要更准确)。甚至测试看一个值是比零大还是比零小也存在危险 ?“以为”会生成比零略
大值的计算事实上可能由于积累的舍入误差会生成略微比零小的数字。
NaN 的无序性质使得在比较浮点数时更容易发生错误。当比较浮点数时,围绕无穷大
和 NaN 问题,一种避免 gotcha 的经验法则是显式地测试值的有效性,而不是试图排除
无效值。在清单 1 中,有两个可能的用于特性的 setter 的实现,该特性只能接受非负
数值。第一个实现会接受 NaN,第二个不会。第二种形式比较好,因为它显式地检测了您
认为有效的值的范围。
清单 1. 需要非负浮点值的较好办法和较差办法
// Trying to test by exclusion -- this doesnt catch NaN or infini
补充:软件开发 , Java ,