浮点数陷阱与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()方法。

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

君子曰:学不可以已。
《计算机科学导论(原书第4版)》

本书是国际知名的高等学校计算机科学及相关专业基础课教材,也是非常受欢迎的计算机入门读物。该教材是一本百科全书式的计算机专业入门指南,涉及计算机科学的方方面面。虽然读者对象是计算机专业的学生,但这本书深入浅出、引人入胜,勾画出了计算机科学体系的框架,可以为有志于IT行业的学生奠定计算机科学知识的基础,架设进一步深入专业理论学习的桥梁。

发表感想

© 2016 - 2024 chengxuzhixin.com All Rights Reserved.

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