Java浮点数运算精度丢失问题
问题
今天被⽼师问到了⼀个问题: 9.8 - 0.1 等于多少, 明明⼀个⾮常简单的问题, 却隐藏了⼀个⾮常⼤的问题, 稍不留神就踩坑,代码如下所⽰
double a = 9.8;
double b = 0.1;
System.out.println(a+b); // 9.9
System.out.println(a-b); // 9.700000000000001
System.out.println(a*b); // 0.9800000000000001
System.out.println(a/b); // 9.9
为什么相减的答案不是9.7呢? 这就涉及到了计算机底层运算原理了
数据在计算机内存中是以⼆进制的形式存在的, ⼗进制数在转换成⼆进制数的时候, 是可以精准的转换的, 但是浮点数不⾏, ⽐如
0.98 = 0.111110101110000101000111101011100001010001111010111
所以浮点数很⼤概率上会在转换的时候发⽣精度丢失的问题, 这也就是为什么9.8 - 0.1 != 9.7
那么了解到这个坑后, 我们要怎么样才能避免这个问题呢?
为此Java给我们提供了⼀个类
BigDecimal
1. 简介
Java在java.math包中提供的API类BigDecimal,⽤来对超过16位有效位的数进⾏精确的运算。双精度浮点型变量double可以处理16位有效数。在实际应⽤中,需要对更⼤或者更⼩的数进⾏运算和处理。float和double只能⽤来做科学计算或者是⼯程计算,在商业计算中要⽤java.math.BigDecimal。BigDecimal所创建的是对象,我们不能使⽤传统的+、-、*、/等算术运算符直接对其对象进⾏数学运算,⽽必须调⽤其相对应的⽅法。⽅法中的参数也必须是BigDecimal的对象。构造器是类的特殊⽅法,专门⽤来创建对象,特别是带有参数的对象。
2. 构造⽅法
BigDecimal(int)    // 创建⼀个具有参数所指定整数值的对象
BigDecimal(double) // 创建⼀个具有参数所指定双精度值的对象 (不推荐)
BigDecimal(long)  // 创建⼀个具有参数所指定长整数值的对象
BigDecimal(String) // 创建⼀个具有参数所指定以字符串表⽰的数值的对象 (推荐)
3. 部分⽅法
add(BigDecimal)      // 相加
subtract(BigDecimal) // 相减
multiply(BigDecimal) // 相乘
divide(BigDecimal)  // 相除
4. 简单使⽤
double a = 9.8;
double b = 0.1;
BigDecimal x = new String(a)); // 使⽤String类型的构造⽅法
BigDecimal y = new String(b));
System.out.println(x.add(y));      // 9.9
System.out.println(x.subtract(y)); // 9.7bigdecimal除法保留小数
System.out.println(x.multiply(y)); // 0.98
System.out.println(x.divide(y));  // 9.7
可以看到Java帮我们封装了这么⼀个好⽤的类, 后⾯我们需要对浮点数进⾏运算的时候只需要直接使⽤就可以了
5. 部分问题
1. 为什么说不推荐使⽤double类型的构造⽅法
先来看代码
double a = 9.8;
double b = 0.1;
BigDecimal x = new BigDecimal(a); // 使⽤double类型的构造⽅法
BigDecimal y = new BigDecimal(b);
System.out.println(x); // 9.800000000000000710542735760100185871124267578125
System.out.println(y); // 0.1000000000000000055511151231257827021181583404541015625
可以看到在初始化对象的时候就出现了精度丢失的问题, API⽂档中也给出了相应的解释
1. 这个构造函数的结果可能有些不可预测。 可以假设在Java中写⼊new BigDecimal(0.1)创建⼀个BigDecimal ,它完全等于
0.1(⾮标尺值为1,⽐例为1),但实际上等于
0.1000000000000000055511151231257827021181583404541015625。 这是因为0.1不能像double (或者作为任
何有限长度的⼆进制分数)精确地表⽰。 因此,正在被传递给构造的值不是正好等于0.1,虽然表⾯上。
2. 该String构造,在另⼀⽅⾯,是完全可以预测的:写new BigDecimal("0.1")创建BigDecimal这正好等于0.1,正如⼈们所期望的
那样。 因此,⼀般建议使⽤String constructor优先于此。
3. 当double必须⽤作源为BigDecimal ,注意,此构造提供了⼀个精确的转换; 它不会将double转换为String使⽤
valueOf(double)⽅法。
所以为了避免这种情况的发⽣, 就建议使⽤成String类型的构造⽅法来进⾏初始化
2. 除法运算可能存在的坑
BigDecimal除法可能出现不能整除的情况,⽐如 4.5/1.3,这时会报错java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
其实divide⽅法有可以传三个参数:public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) 第⼀参数表⽰除数, 第⼆个参数表⽰⼩数点后保留位数,第三个参数表⽰舍⼊模式,只有在作除法运算或四舍五⼊时才⽤到舍⼊模式,有下⾯这⼏种
// x / y , 保留⼩数点后⾯4位, 舍⼊模式
System.out.println(x.divide( y ,4, RoundingMode.CEILING) );    // 向正⽆穷的⽅向舍⼊
System.out.println(x.divide( y ,4, RoundingMode.DOWN) );        // 向 0 的⽅向舍⼊
System.out.println(x.divide( y ,4, RoundingMode.FLOOR) );      // 向负⽆穷的⽅向舍⼊
System.out.println(x.divide( y ,4, RoundingMode.HALF_DOWN) );  // 向最近的⼀边舍⼊, 两边都相同时, 向下舍⼊
System.out.println(x.divide( y ,4, RoundingMode.HALF_EVEN) );  // 向最近的⼀边舍⼊, 两边都相同时, 保留位如果是奇数, 则向上, 反之向下System.out.println(x.divide( y ,4, RoundingMode.HALF_UP) );    // 向最近的⼀边舍⼊, 两边都相同时, 向上舍⼊
System.out.println(x.divide( y ,4, RoundingMode.UP) );          // 向远离0 的⽅向舍⼊
System.out.println(x.divide( y ,4, RoundingMode.UNNECESSARY) ); // 计算结果是进准的, 不需要舍⼊, 若除不尽依旧会报错
参考资料
1. 百度百科:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。