Java浮点数运算精度丢失问题

本文探讨了为何9.8-0.1不等于9.7,深入剖析了计算机底层的浮点数精度问题,并介绍了Java BigDecimal类如何解决运算精度损失。通过实例说明了BigDecimal的构造方法、运算方法及避免除法误差的技巧。
摘要由CSDN通过智能技术生成

问题

今天被老师问到了一个问题: 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 BigDecimal(Double.toString(a)); // 使用String类型的构造方法
BigDecimal y = new BigDecimal(Double.toString(b));
System.out.println(x.add(y));      // 9.9
System.out.println(x.subtract(y)); // 9.7
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使用Double.toString(double)方法,然后使用BigDecimal(String)构造函数相同的结果。 要获得该结果,请使用static 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. 百度百科: BigDecimal_百度百科

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高傲的小煌人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值