浮点数陷阱与BigDecimal精确数字

2017-01-23 From 程序之心 By 丁仪

浮点数陷阱

在计算机中,浮点数N是用尾数M、基数R、阶码E的形式表示的,N = M * R ^ E。通常情况下,R为2。M用定点小数的形式表示,决定了浮点数的精度;E用整数的形式表示,决定了浮点数的表示范围。

1、舍入误差

如下程序,运行结果我们希望是5,但由于浮点数的计算误差,最终打印的结果却是4.999999999999998

double x = 0;
for(int i = 0; i < 50; i++){
  x += 0.1;
}
System.out.println(x);

而在进行整形转换时,误差更大,如下语句期望结果是29,但是Java运行结果打印出来的是28

System.out.println((int)(29.0 * 0.01 * 100));

2、比较相等

正是以上的舍入误差,导致相等比较运算不能简单地使用==运算符来进行。通常程序员都会选择一个可接受的误差范围,与目标值比较是否在此误差范围内。然而,可接受的误差范围本身可能也是无法准确给出的。

这样的表示形式决定了,浮点数无法精确表示,在计算中也容易产生误差。因此在对计算精度有较高要求的场景中,就不能使用浮点数,在Java中需要使用BigDecimal进行精确计算。

BigDecimal

BigDecimal 是标准的类,在编译器中不需要特殊支持,它可以表示任意精度的小数,并对它们进行计算。在内部,可以用任意精度任何范围的值和一个换算因子来表示 BigDecimal ,换算因子表示左移小数点多少位,从而得到所期望范围内的值。因此,用 BigDecimal 表示的数的形式为 unscaledValue*10^scale。

用于加、减、乘和除的方法给 BigDecimal 值提供了算术运算。由于 BigDecimal 对象是不可变的,这些方法中的每一个都会产生新的BigDecimal 对象。因此,因为创建对象的开销, BigDecimal 不适合于大量的数学计算,但设计它的目的是用来精确地表示小数。如果您正在寻找一种能精确表示如货币量这样的数值,则 BigDecimal 可以很好地胜任该任务。

BigDecimal基本使用方法

1、BigDecimal提供了多个构造函数用于创建BigDecimal对象

bigDecimal = new BigDecimal(new char[]{'1','2'});//char[]
bigDecimal = new BigDecimal(1.2);//double
bigDecimal = new BigDecimal(5);//int
bigDecimal = new BigDecimal(5L);//long
bigDecimal = new BigDecimal("1.2");//String

2、BigDecimal的加减乘除运算不能使用+-*/运算符,需要调用专门的方法

BigDecimal a = new BigDecimal("5.5");
BigDecimal b = new BigDecimal("1.2");
BigDecimal c = null;
c = a.add(b); //c = a + b
c = a.subtract(b); //c = a - b
c = a.multiply(b); //c = a * b
c = a.divide(b); //c = a / b

3、小数的舍入操作

设置小数位数,及取舍方式的方法为 setScale(newScale, roundingMode)

newScale为小数位数,roundingMode有如下几种:

BigDecimal.ROUND_CEILING;    //向正无穷方向取整,正数同ROUND_UP,负数同ROUND_DOWN

BigDecimal.ROUND_DOWN;       //向0方向取整

BigDecimal.ROUND_FLOOR;      //向负无穷方向取整,正数同ROUND_DOWN,负数同ROUND_UP

BigDecimal.ROUND_HALF_DOWN;  //小数>0.5向非0方向进位,其他向0方向进位

BigDecimal.ROUND_HALF_EVEN;  //小数偶数同ROUND_HALF_DOWN,奇数同ROUND_HALF_UP

BigDecimal.ROUND_HALF_UP;    //小数>=0.5向非0方向进位,其他向0方向进位

BigDecimal.ROUND_UNNECESSARY;//断言所请求的操作具有精确的结果,因此不需要舍入。 如果在产生不精确结果的操作上指定此舍入模式,则抛出ArithmeticException

BigDecimal.ROUND_UP;         //向非0方向取整

BigDecimal需要注意的问题

1、首选String类型的构造函数

虽然BigDecimal能够精确表示数值,但是不同类型的构造函数得到的数字未必完全一样。

如下面的语句

System.out.println(new BigDecimal(0.333));

System.out.println(new BigDecimal("0.333"));

打印为

0.333000000000000018207657603852567262947559356689453125

0.333

这是因为double类型构造函数有一定的不可预知性,double类型的误差导致BigDecimal接收到的值会有误差,而String类型的构造函数结果可以预知。当必须使用double类型构造BigDecimal时,先使用Double.toString(double)方法,然后使用BigDecimal(String)构造方法,将double转换为String。

2、除法操作需要设置小数位数

使用BigDecimal进行除法运算,如果结果出现无限小数,会抛出异常,如

Java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. at java.math.BigDecimal.divide(Unknown Source)

此时需要对除法计算设置小数位数,方法为divide(BigDecimal divisor, int scale, int roundingMode)。

3、不要使用equals判断是否相等

BigDecimal的equals() 方法会考虑数字的换算值,即使看起来相同的数字 equals() 比较未必相等。如对于1.00和1.0000两个数字,BigDecimal的equals() 方法比较结果是不相等的,而 compareTo() 方法会认为这两个数是相等的。因此比较两个BigDecimal数值是否相等,应使用compareTo() 方法而不是equals()方法。

本文来源:程序之心,转载请注明出处!

君子曰:学不可以已。
《黑匣子思维》
“黑匣子思维”是一种记录和审视失败并从中吸取经验的积极态度。不惧怕面对失败,反而视失败为学习的途径。不会否认过失、推诿责任和想方设法脱身,而会把失败作为样本深入研究。 缺乏从失败中学习的态度、勇气和能力,会对个体或行业带来严重危害。千方百计避免犯错并不是我们的目标,学习如何聪明而有意义地犯错,将每一次失败作为测试我们成绩的机会。
发表感想

© 2016 - 2023 chengxuzhixin.com All Rights Reserved.

浙ICP备2021034854号-1    浙公网安备 33011002016107号