大多数处理器和编程语言支持浮点运算,但大多数程序员很少注意它。 这很容易理解
我们大多数人几乎不需要使用非整数型。 除了科学计算和偶尔的计时测试和基准程序外,几乎没有使用。 同样,大多数开发者也很容易忽视
由java.math.BigDecimal提供的任意精度的小数
大多数APP应用程序都没有使用。 但是,在以整数为中心的程序中,有时意外地需要表示非整数数据。 例如,JDBC使用BigDecimal
作为SQL DECIMAL列的首选兼容格式。 IEEE浮点
Java
语言支持两种基本浮点类型: float和double,以及它们对应的软件包类float和double。 它们都是
IEEE 754标准。 该标准为32位浮点和64位双精度浮点二进制小数定义了二进制标准。 IEEE 754
在科学记数法中,用底数2的小数来表示浮点数。 IEE浮点数以1位表示数字的符号,8位表示指数,表示23
位数表示尾数,即小数部分。 作为有符号整数的指数有正负之分。 小数部分用二进制(底数2 )的小数表示。 这是否意味着最高有效位对应于值? (2)
-1)、第2名有对应吗? (2 -2),这样类推。 关于双精度浮点数,指数用11比特表示,尾数用52比特表示。
由于科学表示法可以用几种方法表示给定的数字,所以将浮点数归一化,使底数为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 .特殊浮点值的属性基本浮点类型和包装器类浮点有不同的比较行为
更糟糕的是,基本浮点类型和软件包类浮点之间用于比较NaN和-0的规则不同。 相对于浮动
价格。 比较两个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显示了其中的一部分。
表2 .无效的浮点假设舍入误差浮点运算是准确的。 一些数字(
可以正确地表示为二进制(底数2 )小数(因为0.5等于2 -1 ),但其他几个数字)例如0.1
不能正确地表示。 因此,浮点运算可能会导致舍入误差,结果接近——但与你期望的结果不同。 例如,可以得到以下简单的计算
2.600000000001,而不是2.6 :
同样,乘以.1*26的结果不等于. 1本身加上26
其次得到的结果。 如果强制将浮点数转换为整数,舍入误差会更严重。 “看起来像整数值”计算也存在这样的问题,因为强制转换为整数类型会截断非整数部分。 例如,以下语句:
将获得以下输出:
这可能不是你最初期待的。
浮点数比较指南
p>由于存在 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 作为互换类型 S
QL-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 存储到小数字段:
在执行这段似乎无害的代码时会抛出一些令人迷惑不解的异常(这取决于具体的 JDBC
驱动程序),因为 0.01 的双精度近似值会导致大的换算值,这可能会使 JDBC 驱动程序或数据库感到迷惑。JDBC
驱动程序会产生异常,但可能不会说明代码实际上错在哪里,除非意识到二进制浮点数的局限性。相反,使用
BigDecimal("0.01") 或 BigDecimal(1, 2) 构造 BigDecimal
来避免这类问题,因为这两种方法都可以精确地表示小数。
结束语
在 Java
程序中使用浮点数和小数充满着陷阱。浮点数和小数不象整数一样“循规蹈矩”,不能假定浮点计算一定产生整型或精确的结果,虽然它们的确“应该”那样做。最好将浮点运算保留用作计算本来就不精确的数值,譬如测量。如果需要表示定点数(譬如,几美元和几美分),则使用
BigDecimal 。