参考链接,若侵删
https://zhuanlan.zhihu.com/p/164568001


1、Java 中 double 类型操作精度丢失问题

在使用 Java 中 double 进行运算时,经常出现精度丢失的问题,总是在一个正确的结果左右偏 0.0000**1。

先来看 Java 中 double 类型数值加、减、乘、除计算式实例:

public class Test{
    public static void main(String [] args){
        System.out.println(0.06+0.01);
        System.out.println(1.0-0.42);
        System.out.println(4.015*100);
        System.out.println(303.1/1000);    
    }
}

运行结果:
在这里插入图片描述

我们发现,计算出来的值和我们预期结果不一致。原因在于我们的计算机是二进制的。浮点数没有办法使用二进制进行精确表示

计算机的 CPU 表示浮点数由两个部分组成:指数和尾数,这样的表示方法一般都会失去一定的精确度,有些浮点数运算也会产生一定的误差。

浮点运算很少是精确的,只要是超过精度能表示的范围就会产生误差。往往产生误差不是因为数的大小,而是因为数的精度。因此,产生的结果接近但不等于想要的结果。尤其在使用 float 和 double 作精确运算的时候要特别小心

2、BigDecimal 类基本介绍

那么我们如何才能够获取我们想要的预期结果呢?特别是在处理金额交易计算上。

其实 Java 的浮点数只能用来进行科学计算或工程计算,在大多数的商业计算中,一般采用java.math.BigDecimal 类来进行精确计算

使用步骤:
用 float 或者 double 变量构建 BigDecimal 对象。通常使用 BigDecimal 的构造方法或者静态方法的 valueOf() 方法把基本类型的变量构建成 BigDecimal 对象。
通过调用 BigDecimal 的加,减,乘,除等相应的方法进行算术运算。
把 BigDecimal 对象转换成 float,double,int 等类型。
BigDecimal 类 的构造函数:

BigDecimal(int var)  //创建一个具有参数所指定整数值的对象。
BigDecimal(double var) //创建一个具有参数所指定双精度值的对象。
BigDecimal(long var)  //创建一个具有参数所指定长整数值的对象。
BigDecimal(String var) //创建一个具有参数所指定以字符串表示的数值的对象。

BigDecimal 类的加,减,乘,除等相应的方法:

BigDecimal add(BigDecimal augend)  //加法运算
BigDecimal subtract(BigDecimal subtrahend) //减法运算
BigDecimal multiply(BigDecimal multiplicand) //乘法运算
BigDecimal divide(BigDecimal divisor) //除法运算

3、浮点数精度丢失的解决方案

既然我们知道方法了,那么我们直接套用上面的方法来解决一下上述的问题。

菜鸟误区:

修改以上示例代码,如下:

import java.math.*;

public class Test{
    public static void main(String [] args){
        double d1 = 0.06;
        double d2 = 0.01;
        BigDecimal b1 = new BigDecimal(d1);
        BigDecimal b2 = new BigDecimal(d2);
        
        System.out.println(b1.add(b2).doubleValue());
        
        double d3 = 1.0;
        double d4 = 0.42;
        BigDecimal b3 = new BigDecimal(d3);
        BigDecimal b4 = new BigDecimal(d4);
        System.out.println(b3.subtract(b4).doubleValue());
        
        double d5 = 4.015;
        double d6 = 100;
        BigDecimal b5 = new BigDecimal(d5);
        BigDecimal b6 = new BigDecimal(d6);
        System.out.println(b5.multiply(b6).doubleValue());
        
        double d7 = 303.1;
        double d8 = 1000;
        BigDecimal b7 = new BigDecimal(d7);
        BigDecimal b8 = new BigDecimal(d8);
        System.out.println(b7.divide(b8).doubleValue());    
    }
}

我们发现结果还是不对。
在这里插入图片描述

上述实例我们调用的构造方法为 BigDecimal(double var)。

注意: 1、BigDecimal(double var) 构造方法的结果有一定的不可预知性。有人可能认为在 Java 中写入 new BigDecimal(0.1) 所创建的 BigDecimal
正好等于 0.1,但是它实际上等于
0.1000000000000000055511151231257827021181583404541015625。这是因为 0.1 无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于
0.1(虽然表面上等于该值)。 2、BigDecimal(String var) 的 String 参数构造方法是完全可预知的:写入 new BigDecimal(“0.1”) 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言,通常建议优先使用
String 构造方法。

解决方案:

通过上面的示例我们发现:使用 BigDecimal(double val) 构造函数时仍会存在精度丢失问题,建议使用 BigDecimal(String val)。这就需要先把 double 转换为字符串然后在作为BigDecimal(String val) 构造函数的参数。转换为 BigDecimal 对象之后再进行加减乘除操作,这样精度就不会出现问题了。

import java.math.*;

public class Test{
    public static void main(String [] args){
        double d1 = 0.06;
        double d2 = 0.01;
        BigDecimal b1 = new BigDecimal(Double.toString(d1));
        BigDecimal b2 = new BigDecimal(Double.toString(d2));
        
        System.out.println(b1.add(b2).doubleValue());
        
        double d3 = 1.0;
        double d4 = 0.42;
        BigDecimal b3 = new BigDecimal(Double.toString(d3));
        BigDecimal b4 = new BigDecimal(Double.toString(d4));
        System.out.println(b3.subtract(b4).doubleValue());
        
        double d5 = 4.015;
        double d6 = 100;
        BigDecimal b5 = new BigDecimal(Double.toString(d5));
        BigDecimal b6 = new BigDecimal(Double.toString(d6));
        System.out.println(b5.multiply(b6).doubleValue());
        
        double d7 = 303.1;
        double d8 = 1000;
        BigDecimal b7 = new BigDecimal(Double.toString(d7));
        BigDecimal b8 = new BigDecimal(Double.toString(d8));
        System.out.println(b7.divide(b8).doubleValue());    
    }
}

运行结果 ,终于对了
在这里插入图片描述

总结

(1)需要精确的表示两位小数时我们需要把他们转换为 BigDecimal 对象,然后再进行运算
(2)使用 BigDecimal(double val) 构造函数时仍会存在精度丢失问题,建议使用 BigDecimal(String val)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐