Java理论与实践您的小数点在哪?
——使用浮点数和小数中的技巧和陷阱
作者Brian Goetz 本文选自IBM DW中国网站 年月日
许多程序员在其整个开发生涯中都不曾使用定点或浮点数可能的例外是偶尔在计时测试或基准测试程序中会用到Java 语言和类库支持两类非整数类型 — IEEE 浮点(float 和 double包装类(wrapper class)为 Float 和 Double)以及任意精度的小数(javamathBigDecimal)在本月的 Java 理论和实践中Brian Goetz 探讨了在 Java 程序中使用非整数类型时一些常碰到的陷阱和gotcha
虽然几乎每种处理器和编程语言都支持浮点运算但大多数程序员很少注意它这容易理解 — 我们中大多数很少需要使用非整数类型除了科学计算和偶尔的计时测试或基准测试程序其它情况下几乎都用不着它同样大多数开发人员也容易忽略 javamathBigDecimal 所提供的任意精度的小数 — 大多数应用程序不使用它们然而在以整数为主的程序中有时确实会出人意料地需要表示非整型数据例如JDBC 使用 BigDecimal 作为 SQL DECIMAL 列的首选互换格式
IEEE 浮点
Java 语言支持两种基本的浮点类型float 和 double以及与它们对应的包装类 Float 和 Double它们都依据 IEEE 标准该标准为 位浮点和 位双精度浮点二进制小数定义了二进制标准
IEEE 用科学记数法以底数为 的小数来表示浮点数IEEE 浮点数用 位表示数字的符号用 位来表示指数用 位来表示尾数即小数部分作为有符号整数的指数可以有正负之分小数部分用二进制(底数 )小数来表示这意味着最高位对应着值 ?()第二位对应着 ?()依此类推对于双精度浮点数用 位表示指数 位表示尾数IEEE 浮点值的格式如图 所示
IEEE 浮点数的格式
因为用科学记数法可以有多种方式来表示给定数字所以要规范化浮点数以便用底数为 并且小数点左边为 的小数来表示按照需要调节指数就可以得到所需的数字所以例如数 可以表示为尾数为 指数为
除了编码所允许的值的标准范围(对于 float从 e 到 e+)还有一些表示无穷大负无穷大 和 NaN(它代表不是一个数字)的特殊值这些值的存在是为了在出现错误条件(譬如算术溢出给负数开平方根除以 等)下可以用浮点值集合中的数字来表示所产生的结果这些特殊的数字有一些不寻常的特征例如 和 是不同值但在比较它们是否相等时被认为是相等的用一个非零数去除以无穷大的数结果等于 特殊数字 NaN 是无序的使用 ==< 和 > 运算符将 NaN 与其它浮点值比较时结果为 false如果 f 为 NaN则即使 (f == f) 也会得到 false如果想将浮点值与 NaN 进行比较则使用 FloatisNaN() 方法表 显示了无穷大和 NaN 的一些属性
表 特殊浮点值的属性
表达式 结果
Mathsqrt() > NaN
/ > NaN
/ > 无穷大
/ > 负无穷大
NaN + > NaN
无穷大 + > 无穷大
无穷大 + 无穷大> 无穷大
NaN > > false
NaN == > false
NaN < > false
NaN == NaN > false
== > true
基本浮点类型和包装类浮点有不同的比较行为
使事情更糟的是在基本 float 类型和包装类 Float 之间用于比较 NaN 和 的规则是不同的对于 float 值比较两个 NaN 值是否相等将会得到 false而使用 Floatequals() 来比较两个 NaN Float 对象会得到 true造成这种现象的原因是如果不这样的话就不可能将 NaN Float 对象用作 HashMap 中的键类似的虽然 和 在表示为浮点值时被认为是相等的但使用 pareTo() 来比较作为 Float 对象的 和 时会显示 小于
浮点中的危险
由于无穷大NaN 和 的特殊行为当应用浮点数时可能看似无害的转换和优化实际上是不正确的例如虽然好象 f 很明显等于 f但当 f 为 时这是不正确的还有其它类似的 gotcha表 显示了其中一些 gotcha
表 无效的浮点假定
这个表达式…… 不一定等于…… 当……
f f f 为
f < g ! (f >= g) f 或 g 为 NaN
f == f true f 为 NaN
f + g g f g 为无穷大或 NaN
捨入误差
浮点运算很少是精确的虽然一些数字(譬如 )可以精确地表示为二进制(底数 )小数(因为 等于 )但其它一些数字(譬如 )就不能精确的表示因此浮点运算可能导致捨入误差产生的结果接近 — 但不等于 — 您可能希望的结果例如下面这个简单的计算将得到 而不是
double s=;
for (int i=; i<; i++)
s += ;
Systemoutprintln(s);
类似的* 相乘所产生的结果不等于 自身加 次所得到的结果当将浮点数强制转换成整数时产生的捨入误差甚至更严重因为强制转换成整数类型会捨弃非整数部分甚至对于那些看上去似乎应该得到整数值的计算也存在此类问题例如下面这些语句
double d = * ;
Systemoutprintln(d);
Systemoutprintln((int) (d * ));
将得到以下输出
这可能不是您起初所期望的
浮点数比较指南
由于存在 NaN 的不寻常比较行为和在几乎所有浮点计算中都不可避免地会出现捨入误差解释浮点值的比较运算符的结果比较麻烦
最好完全避免使用浮点数比较当然这并不总是可能的但您应该意识到要限制浮点数比较如果必须比较浮点数来看它们是否相等则应该将它们差的绝对值同一些预先选定的小正数进行比较这样您所做的就是测试它们是否足够接近(如果不知道基本的计算范围可以使用测试abs(a/b ) < epsilon这种方法比简单地比较两者之差要更准确)甚至测试看一个值是比零大还是比零小也存在危险 —以为会生成比零略大值的计算事实上可能由于积累的捨入误差会生成略微比零小的数字
NaN 的无序性质使得在比较浮点数时更容易发生错误当比较浮点数时围绕无穷大和 NaN 问题一种避免 gotcha 的经验法则是显式地测试值的有效性而不是试图排除无效值在清单 中有两个可能的用于特性的 setter 的实现该特性只能接受非负数值第一个实现会接受 NaN第二个不会第二种形式比较好因为它显式地检测了您认为有效的值的范围
清单 需要非负浮点值的较好办法和较差办法
// Trying to test by exclusion this doesnt catch NaN or infinity
public void setFoo(float foo) {
if (foo < )
throw new IllegalArgumentException(FloattoString(f));
thisfoo = foo;
}
// Testing by inclusion this does catch NaN
public void setFoo(float foo) {
if (foo >= && foo < FloatINFINITY)
thisfoo = foo;
else
throw new IllegalArgumentException(FloattoString(f));
}
不要用浮点值表示精确值
一些非整数值(如几美元和几美分这样的小数)需要很精确浮点数不是精确值所以使用它们会导致捨入误差因此使用浮点数来试图表示象货币量这样的精确数量不是一个好的想法使用浮点数来进行美元和美分计算会得到灾难性的后果浮点数最好用来表示象测量值这类数值这类值从一开始就不怎么精确
用于较小数的 BigDecimal
从 JDK 起Java 开发人员就有了另一种数值表示法来表示非整数BigDecimalBigDecimal 是标准的类在编译器中不需要特殊支持它可以表示任意精度的小数并对它们进行计算在内部可以用任意精度任何范围的值和一个换算因子来表示 BigDecimal换算因子表示左移小数点多少位从而得到所期望范围内的值因此用 BigDecimal 表示的数的形式为
scale
unscaledValue*
用于加减乘和除的方法给 BigDecimal 值提供了算术运算由于 BigDecimal 对象是不可变的这些方法中的每一个都会产生新的 BigDecimal 对象因此因为创建对象的开销BigDecimal 不适合于大量的数学计算但设计它的目的是用来精确地表示小数如果您正在寻找一种能精确表示如货币量这样的数值则 BigDecimal 可以很好地胜任该任务
所有的 equals 方法都不能真正测试相等
如浮点类型一样BigDecimal 也有一些令人奇怪的行为尤其在使用 equals() 方法来检测数值之间是否相等时要小心equals() 方法认为两个表示同一个数但换算值不同(例如 和 )的 BigDecimal 值是不相等的然而compareTo() 方法会认为这两个数是相等的所以在从数值上比较两个 BigDecimal 值时应该使用 compareTo() 而不是 equals()
另外还有一些情形任意精度的小数运