Java 理论与实践: 您的小数点到哪里去了?
虽然几乎每种处理器和编程语言都支持浮点运算,但大多数程序员很少注意它。这容易理解 — 我们中大多数很少需要使用非整数类型。除了科学计算和偶尔的计时测试或基准测试程序,其它情况下几乎都用不着它。同样,大多数开发人员也容易忽略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:?(-1)?0*1.01?2*2?0
数 10.0 可以表示为尾数为 1.01,指数为 3:?(-1)?0*1.01?2*2?3
特殊数字
除了编码所允许的值的标准范围(对于?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.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 为 NaNf == f
true
f 为 NaNf + g - g
f
g 为无穷大或 NaN舍入误差
浮点运算很少是精确的。虽然一些数字(譬如?0.5
?)可以精确地表示为二进制(底数 2)小数(因为?0.5
?等于 2?-1),但其它一些数字(譬如?0.1
?)就不能精确的表示。因此,浮点运算可能导致舍入误差,产生的结果接近 — 但不等于 — 您可能希望的结果。例如,下面这个简单的计算将得到?2.600000000000001
?,而不是?2.6
?:
回页首浮点数比较指南
由于存在 NaN 的不寻常比较行为和在几乎所有浮点计算中都不可避免地会出现舍入误差,解释浮点值的比较运算符的结果比较麻烦。
最好完全避免使用浮点数比较。当然,这并不总是可能的,但您应该意识到要限制浮点数比较。如果必须比较浮点数来看它们是否相等,则应该将它们差的绝对值同一些预先选定的小正数进行比较,这样您所做的就是测试它们是否“足够接近”。(如果不知道基本的计算范围,可以使用测试“abs(a/b - 1) < epsilon”,这种方法比简单地比较两者之差要更准确)。甚至测试看一个值是比零大还是比零小也存在危险 —“以为”会生成比零略大值的计算事实上可能由于积累的舍入误差会生成略微比零小的数字。
NaN 的无序性质使得在比较浮点数时更容易发生错误。当比较浮点数时,围绕无穷大和 NaN 问题,一种避免 gotcha 的经验法则是显式地测试值的有效性,而不是试图排除无效值。在清单 1 中,有两个可能的用于特性的 setter 的实现,该特性只能接受非负数值。第一个实现会接受 NaN,第二个不会。第二种形式比较好,因为它显式地检测了您认为有效的值的范围。
清单 1. 需要非负浮点值的较好办法和较差办法不要用浮点值表示精确值一些非整数值(如几美元和几美分这样的小数)需要很精确。浮点数不是精确值,所以使用它们会导致舍入误差。因此,使用浮点数来试图表示象货币量这样的精确数量不是一个好的想法。使用浮点数来进行美元和美分计算会得到灾难性的后果。浮点数最好用来表示象测量值这类数值,这类值从一开始就不怎么精确。
回页首
用于较小数的 BigDecimal
从 JDK 1.3 起,Java 开发人员就有了另一种数值表示法来表示非整数:?
BigDecimal
?。?BigDecimal
?是标准的类,在编译器中不需要特殊支持,它可以表示任意精度的小数,并对它们进行计算。在内部,可以用任意精度任何范围的值和一个换算因子来表示BigDecimal
?,换算因子表示左移小数点多少位,从而得到所期望范围内的值。因此,用?BigDecimal
?表示的数的形式为unscaledValue*10?-scale?
。用于加、减、乘和除的方法给?
BigDecimal
?值提供了算术运算。由于?BigDecimal
?对象是不可变的,这些方法中的每一个都会产生新的BigDecimal
?对象。因此,因为创建对象的开销,?BigDecimal
?不适合于大量的数学计算,但设计它的目的是用来精确地表示小数。如果您正在寻找一种能精确表示如货币量这样的数值,则?BigDecimal
?可以很好地胜任该任务。所有的 equals 方法都不能真正测试相等
如浮点类型一样,?
BigDecimal
?也有一些令人奇怪的行为。尤其在使用?equals()
?方法来检测数值之间是否相等时要小心。?equals()
方法认为,两个表示同一个数但换算值不同(例如,?100.00
?和?100.000
?)的?BigDecimal
?值是不相等的。然而,?compareTo()
?方法会认为这两个数是相等的,所以在从数值上比较两个?BigDecimal
?值时,应该使用?compareTo()
?而不是?equals()
?。另外还有一些情形,任意精度的小数运算仍不能表示精确结果。例如,?
1
?除以?9
?会产生无限循环的小数?.111111...
?。出于这个原因,在进行除法运算时,?BigDecimal
?可以让您显式地控制舍入。?movePointLeft()
?方法支持 10 的幂次方的精确除法。使用 BigDecimal 作为互换类型
SQL-92 包括?
DECIMAL
?数据类型,它是用于表示定点小数的精确数字类型,它可以对小数进行基本的算术运算。一些 SQL 语言喜欢称此类型为?NUMERIC
?类型,其它一些 SQL 语言则引入了?MONEY
?数据类型,MONEY 数据类型被定义为小数点右侧带有两位的小数。如果希望将数字存储到数据库中的?
DECIMAL
?字段,或从?DECIMAL
?字段检索值,则如何确保精确地转换该数字?您可能不希望使用由 JDBC?PreparedStatement
?和?ResultSet
?类所提供的?setFloat()
?和?getFloat()
?方法,因为浮点数与小数之间的转换可能会丧失精确性。相反,请使用?PreparedStatement
?和?ResultSet
?的?setBigDecimal()
?及?getBigDecimal()
?方法。对于?
BigDecimal
?,有几个可用的构造函数。其中一个构造函数以双精度浮点数作为输入,另一个以整数和换算因子作为输入,还有一个以小数的?String
?表示作为输入。要小心使用?BigDecimal(double)
?构造函数,因为如果不了解它,会在计算过程中产生舍入误差。请使用基于整数或?String?
的构造函数。构造 BigDecimal 数
对于?
BigDecimal
?,有几个可用的构造函数。其中一个构造函数以双精度浮点数作为输入,另一个以整数和换算因子作为输入,还有一个以小数的?String
?表示作为输入。要小心使用?BigDecimal(double)
?构造函数,因为如果不了解它,会在计算过程中产生舍入误差。请使用基于整数或?String
?的构造函数。如果使用?
BigDecimal(double)
?构造函数不恰当,在传递给 JDBC?setBigDecimal()
?方法时,会造成似乎很奇怪的 JDBC 驱动程序中的异常。例如,考虑以下 JDBC 代码,该代码希望将数字?0.01
?存储到小数字段:回页首结束语
在 Java 程序中使用浮点数和小数充满着陷阱。浮点数和小数不象整数一样“循规蹈矩”,不能假定浮点计算一定产生整型或精确的结果,虽然它们的确“应该”那样做。最好将浮点运算保留用作计算本来就不精确的数值,譬如测量。如果需要表示定点数(譬如,几美元和几美分),则使用?
BigDecimal
?。参考资料
- 您可以参阅本文在 developerWorks 全球站点上的?英文原文.?
- 请参与本文的?论坛。(您也可以单击本文顶部或底部的?讨论来访问论坛)。?
- David Goldberg 的经典文章?What Every Scientist Should Know About Floating-Point Arithmetic对各种浮点表示进行了权衡,并讨论了它们各自的缺点。?
- IBM Hursley 实验室的?FAQ探讨了有关小数运算的一些问题。?
- Bill Venners 在?JavaWorld?的?Under the hood?专栏(Bill Venners 撰写)研究了 JVM 中的浮点支持。?
- IEEE 754 的首席架构设计师之一 William Kahan 在“?How Java's Floating-Point Hurts Everyone Everywhere”(PDF)一文中批评了不完整的 Java 浮点实现。?
- Intel Architecture Software Developer's Manual(PDF)详细讲述了 IEEE 754 浮点的最常见实现。?
- Castor?XML 数据绑定框架使用?
BigDecimal
?作为小数的互换类型。? - Java Developer Connection 的?Tech Tips就 Java 2 平台中数字计算提供了一些指导。?
- JavaWorld文章“?Make cents with BigDecimal”为使用?
BigDecimal
?和?NumberFormat
?来进行一些财会计算提供了一些有意义的技巧。? - 阅读?Java theory and practice?专栏?(Brian 撰写)中的所有文章。?
关于作者
Brian Goetz 是一位软件顾问,在过去的 15 年里一直是一位专业软件开发人员。他是?Quiotix的首席顾问,该公司是位于加利福尼亚 Los Altos 的软件开发和咨询公司。请在业界流行的出版物上查阅 Brian?已发表的和即将发表的文章。 可以通过brian@quiotix.com与 Brian 联系。
?
转载出处:http://www.ibm.com/developerworks/cn/java/j-jtp0114/index.html