BigDecimal研究(3)
BigDecimal研究(3)--使⽤浮点数和⼩数中的技巧和陷阱
虽然⼏乎每种处理器和编程语⾔都⽀持浮点运算,但⼤多数程序员很少注意它。这容易理解―我们中⼤多数很少需要使⽤⾮整数类型。除了科学计算和偶尔的计时测试或基准测试程序,其它情况下⼏乎都⽤不着它。同样,⼤多数开发⼈员也容易忽略java.math.BigDecimal所提供的任意精度的⼩数―⼤多数应⽤程序不使⽤它们。然⽽,在以整数为主的程序中有时确实会出⼈意料地需要表⽰⾮整型数据。例如,JDBC 使⽤BigDecimal作为 SQL DECIMAL列的⾸选互换格式。
浮点
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 所⽰。
因为⽤科学记数法可以有多种⽅式来表⽰给定数字,所以要规范化浮点数,以便⽤底数为 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在表⽰为浮点值时,被认为是相等的,但使⽤FloatpareTo()来⽐较作为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,第⼆个不会。第⼆种形式⽐较好,因为它显式地检测了您认为有效的值的范围。 // Trying to test by exclusion -- this doesn't catch NaN or infinity
public void setFoo(float foo) {
if (foo < 0)
throw new String(f));
this.foo = foo;
}
// Testing by inclusion -- this does catch NaN
public void setFoo(float foo) {
if (foo >= 0 && foo < Float.INFINITY)
this.foo = foo;
else
throw new String(f));
}
⼀些⾮整数值(如⼏美元和⼏美分这样的⼩数)需要很精确。浮点数不是精确值,所以使⽤它们会导致舍⼊误差。因此,使⽤浮点数来试图表⽰象货币量这样的精确数量不是⼀个好的想法。使⽤浮点数来进⾏美元和美分计算会得到灾难性的后果。浮点数最好⽤来表⽰象测量值这类数值,这类值从⼀开始就不怎么精确。
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会产⽣⽆限循环的⼩数.。出于这个原因,在进⾏除法运算时,BigDecimal可以让您显式地控制舍⼊。movePointL
eft()⽅法⽀持 10 的幂次⽅的精确除法。
bigdecimal除法保留小数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存储到⼩数字段:
PreparedStatement ps =
connection.prepareStatement("INSERT INTO Foo SET name=?, value=?");
ps.setString(1, "penny");
ps.setBigDecimal(2, new BigDecimal(0.01));
在执⾏这段似乎⽆害的代码时会抛出⼀些令⼈迷惑不解的异常(这取决于具体的 JDBC 驱动程序),因为0.01的双精度近似值会导致⼤的换算值,这可能会使 JDBC 驱动程序或数据库感到迷惑。JDBC 驱动程序会产⽣异常,但可能不会说明代码实际上错在哪⾥,除⾮意识到⼆进制浮点数的局限性。相
反,使⽤BigDecimal("0.01")或BigDecimal(1, 2)构造BigDecimal来避免这类问题,因为这两种⽅法都可以精确地表⽰⼩数。
在 Java 程序中使⽤浮点数和⼩数充满着陷阱。浮点数和⼩数不象整数⼀样“循规蹈矩”,不能假定浮点计算⼀定产⽣整型或精确的结果,虽然它们的确“应该”那样做。最好将浮点运算保留⽤作计算本来就不精确的数值,譬如测量。如果需要表⽰定点数(譬如,⼏美元和⼏美分),则使⽤BigDecimal
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论