zoukankan      html  css  js  c++  java
  • [ JAVA编程 ] double类型计算精度丢失问题及解决方法

    前言 

      如果你在测试金融相关产品,请务必覆盖交易金额为小数的场景。特别是使用Java语言的初级开发。

    Java基本实例

    先来看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);    
        }
    }

    运行结果如下:

    D:linyfeng>java Test
    0.06+0.01 = 0.06999999999999999
    1.0-0.42 = 0.5800000000000001
    4.015*100 = 401.49999999999994
    303.1/1000 = 0.30310000000000004

        我们发现,计算出来的值和我们预期结果不一致。原因在于我们的计算机是二进制的。浮点数没有办法使用二进制进行精确表示。计算机的CPU表示浮点数由两个部分组成:指数和尾数,这样的表示方法一般都会失去一定的精确度,有些浮点数运算也会产生一定的误差。如:2.4的二进制表示并非就是精确的2.4。反而最为接近的二进制表示是 2.3999999999999999。浮点数的值实际上是由一个特定的数学公式计算得到的。可参考http://blog.csdn.net/abing37/article/details/5332798

        那么我们如何才能够获取我们想要的预期结果呢?特别是在处理金额交易计算上。其实java的float只能用来进行科学计算或工程计算,在大多数的商业计算中,一般采用java.math.BigDecimal类来进行精确计算。在使用BigDecimal类来进行计算的时候,主要分为以下步骤:

      (1) 用float或者double变量构建BigDecimal对象。通常使用BigDecimal的构造方法或者静态方法的valueOf()方法把基本类型的变量构建成BigDecimal对象。

      (2) 通过调用BigDecimal的加,减,乘,除等相应的方法进行算术运算。

      (3) 把BigDecimal对象转换成float,double,int等类型。

    BigDecimal类基本介绍

        在修改实例之前,我们先简单了解一下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) //除法运算

      好,既然我们知道方法了,那么我们就用新方法来解决一下上述的问题。修改一下代码,如下:

    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());    
        }
    }

    运行结果:

    D:linyfeng>java Test
    0.06999999999999999
    0.5800000000000001
    401.49999999999994
    0.30310000000000004

      我们发现结果还是不对。从上述实例我们知道调用的构造方法为BigDecimal(double var)。 BigDecimal(double val)将 double 转换为 BigDecimal,后者是double的二进制浮点值准确的十进制表示形式。返回的BigDecimal的标度是使 (10scale × val) 为整数的最小值。这里也几个特别要注意的地方:

      (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 构造方法。
      (3)当 double 必须用作 BigDecimal 的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用 Double.toString(double) 方法,然后使用 BigDecimal(String) 构造方法,将 double 转换为 String。要获取该结果,请使用 static valueOf(double) 方法。

      根据上述描述,我们继续修改下例子,修改后如下:

    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());    
        }
    }

    运行结果如下:

    D:linyfeng>java Test
    0.07
    0.58
    401.5
    0.3031

      计算精度正确。

    总结

    (1)需要精确的表示两位小数时我们需要把他们转换为BigDecimal对象,然后再进行运算。
    (2)使用BigDecimal(double val)构造函数时仍会存在精度丢失问题,建议使用BigDecimal(String val)。这就需要先把double类型(调用Double.toString(double var))转换为字符串然后在作为BigDecimal(String val)构造函数的参数。转换为BigDecimal对象之后再进行加减乘除操作,这样精度就不会出现问题了。这也是为什么有关金钱数据存储都使用BigDecimal。

    参考文档

    1、使用BigDecimal进行精确运算

    2、java中double和float精度丢失问题及解决方法

    3、百度百科BigDecimal

  • 相关阅读:
    关于同余最短路
    【水】关于 __attribute__
    题解【AtCoder
    一些简单图论问题
    浅谈简单动态规划
    关于博客园主题(美化博客园)
    Algebra, Topology, Differential Calculus, and Optimization Theory For Computer Science and Machine Learning 第47章 读书笔记(待更新)
    Algebra, Topology, Differential Calculus, and Optimization Theory For Computer Science and Machine Learning 第46章 读书笔记(待更新)
    Algebra, Topology, Differential Calculus, and Optimization Theory For Computer Science and Machine Learning 第45章 读书笔记(待更新)
    Algebra, Topology, Differential Calculus, and Optimization Theory For Computer Science and Machine Learning 第44章 读书笔记(待更新)
  • 原文地址:https://www.cnblogs.com/linyfeng/p/6915249.html
Copyright © 2011-2022 走看看