zoukankan      html  css  js  c++  java
  • Java中关于 BigDecimal 的一个导致double精度损失的"bug"

    背景

         在博客 恶心的0.5四舍五入问题 一文中看到一个关于 0.5 不能正确的四舍五入的问题。主要说的是 double 转换到 BigDecimal 后,进行四舍五入得不到正确的结果:

    复制代码
    public class BigDecimalTest {
        public static void main(String[] args){
            double d = 301353.05;
            BigDecimal decimal = new BigDecimal(d);
            System.out.println(decimal);//301353.0499999999883584678173065185546875
            System.out.println(decimal.setScale(1, RoundingMode.HALF_UP));//301353.0
        }
    }
    复制代码

    输出的结果为:

    301353.0499999999883584678173065185546875
    301353.0

    这个结果显然不是我们所期望的,我们希望的是得到 301353.1 。

    原因

         允许明眼人一眼就看出另外问题所在——BigDecimal的构造函数 public BigDecimal(double val) 损失了double 参数的精度,最后才导致了错误的结果。所以问题的关键是:BigDecimal的构造函数 public BigDecimal(double val) 损失了double 参数的精度。

    解决之道

    因为上面找到了原因,所以也就很好解决了。只要防止了 double 到 BigDecimal 的精度的损失,也就不会出现问题。

    1)很容易想到第一个解决办法:使用BigDecimal的以String为参数的构造函数:public BigDecimal(String val)  来替代。

    复制代码
    public class BigDecimalTest {
        public static void main(String[] args){
            double d = 301353.05;
            System.out.println(new BigDecimal(new Double(d).toString()));
            System.out.println(new BigDecimal("301353.05"));
            System.out.println(new BigDecimal("301353.895898895455898954895989"));
        }
    }
    复制代码

    输出结果:

    301353.05
    301353.05
    301353.895898895455898954895989

    我们看到了没有任何的精度损失,四舍五入也就肯定不会出错了。

    2)BigDecimal的构造函数 public BigDecimal(double val) 会损失了double 参数的精度,这个也许应该可以算作是 JDK 中的一个 bug 了。既然存在bug,那么我们就应该解决它。上面的办法是绕过了它。现在我们实现自己的 double 到 BigDecimal 的转换,并且保证在某些情况下可以完全不损失 double 的精度。

    复制代码
    import java.math.BigDecimal;
    
    public class BigDecimalUtil {
        
        public static BigDecimal doubleToBigDecimal(double d){
            String doubleStr = String.valueOf(d);
            if(doubleStr.indexOf(".") != -1){
                int pointLen = doubleStr.replaceAll("\d+\.", "").length();    // 取得小数点后的数字的位数
                pointLen = pointLen > 16 ? 16 : pointLen;    // double最大有效小数点后的位数为16
                double pow = Math.pow(10, pointLen);
           long tmp = (long)(d * pow); return new BigDecimal(tmp).divide(new BigDecimal(pow)); } return new BigDecimal(d); } public static void main(String[] args){ // System.out.println(doubleToBigDecimal(301353.05)); // System.out.println(doubleToBigDecimal(-301353.05)); // System.out.println(doubleToBigDecimal(new Double(-301353.05))); // System.out.println(doubleToBigDecimal(301353)); // System.out.println(doubleToBigDecimal(new Double(-301353))); double d = 301353.05;//5898895455898954895989; System.out.println(doubleToBigDecimal(d)); System.out.println(d); System.out.println(new Double(d).toString()); System.out.println(new BigDecimal(new Double(d).toString())); System.out.println(new BigDecimal(d)); } }
    复制代码

    输出结果:

    301353.05
    301353.05
    301353.05
    301353.05
    301353.0499999999883584678173065185546875

    上面我们自己写了一个工具类,实现了 double 到 BigDecimal 的“无损失”double精度的转换。方法是将小数点后有有效数字的double先转换到小数点后没有有效数字的double,然后在转换到 BigDecimal ,之后使用BigDecimal的 divide 返回之前的大小。

    上面的结果看起来好像十分的完美,但是其实是存在问题的。上面我们也说到了“某些情况下可以完全不损失 double 的精度”,我们先看一个例子:

    复制代码
        public static void main(String[] args){
            double d = 301353.05;
            System.out.println(doubleToBigDecimal(d));
            System.out.println(d);
            System.out.println(new Double(d).toString());
            System.out.println(new BigDecimal(new Double(d).toString()));
            System.out.println(new BigDecimal(d));
            
            System.out.println("=========================");
            d = 301353.895898895455898954895989;
            System.out.println(doubleToBigDecimal(d));
            System.out.println(d);
            System.out.println(new Double(d).toString());
            System.out.println(new BigDecimal(new Double(d).toString()));
            System.out.println(new BigDecimal(d));
            System.out.println(new BigDecimal("301353.895898895455898954895989"));
            
            System.out.println("=========================");
            d = 301353.46899434;
            System.out.println(doubleToBigDecimal(d));
            System.out.println(d);
            System.out.println(new Double(d).toString());
            System.out.println(new BigDecimal(new Double(d).toString()));
            System.out.println(new BigDecimal(d));
            
            System.out.println("=========================");
            d = 301353.45789666;
            System.out.println(doubleToBigDecimal(d));
            System.out.println(d);
            System.out.println(new Double(d).toString());
            System.out.println(new BigDecimal(new Double(d).toString()));
            System.out.println(new BigDecimal(d));
        }
    复制代码

    输出结果:

    301353.05
    301353.05
    301353.05
    301353.05
    301353.0499999999883584678173065185546875
    =========================
    301353.89589889544
    301353.89589889545
    301353.89589889545
    301353.89589889545
    301353.895898895454593002796173095703125
    301353.895898895455898954895989
    =========================
    301353.46899434
    301353.46899434
    301353.46899434
    301353.46899434
    301353.4689943399862386286258697509765625
    =========================
    301353.45789666
    301353.45789666
    301353.45789666
    301353.45789666
    301353.4578966600238345563411712646484375
    我们可以看到:我们自己实现的 doubleToBigDecimal 方法只有在 double 的小数点后的数字位数比较少时(比如只有5,6位),才能保证完全的不损失精度

    在 double 的小数点后的数字位数比较多时,d * pow 会存在精度损失,所以最终的结果也会存在精度损失。所以如果小数点后的位数比较多时,还是使用 BigDecimal的 String 参数的构造函数为好,只有在小数点后的位数比较少时,才可以采用自己实现的 doubleToBigDecimal 方法。

    因为我们看到原始的double的转换之后的BigDecimal的数字的最后一位一个时5,一个是4,原因是在上面的转换方法中:

    long tmp = (long)(d * pow);

    这一步可能存在很小的精度损失,因为 d 是一个 double ,d * pow 之后还是一个 double(但是小数点之后都是0了,所以到long的转换没有精度损失) ,所以会存在很小的精度损失(double的计算总是有可能存在精度损失的)。但是这个精度损失和 BigDecimal的构造函数 public BigDecimal(double val) 的精度损失相比而言,不会显得那么的突兀(也许我们自己写的doubleToBigDecimal也是存在问题的,欢迎指点)。

    总结

    如果需要保证精度,最好是不要使用BigDecimal的double参数的构造函数,因为存在损失double参数精度的可能,最好是使用BigDecimal的String参数的构造函数。最好是杜绝使用BigDecimal的double参数的构造函数。

    后记:

         其实说这是BigDecimal的一个bug,有标题党的嫌疑,最多可以算作是BigDecimal的一个“坑”。

    转自:Java中关于 BigDecimal 的一个导致double精度损失的"bug" - digdeep - 博客园 (cnblogs.com)

  • 相关阅读:
    垂死挣扎还是涅槃重生 -- Delphi XE5 公布会归来感想
    自考感悟,话谈备忘录模式
    [每日一题] OCP1z0-047 :2013-07-26 alter table set unused之后各种情况处理
    Java实现 蓝桥杯 算法提高 p1001
    Java实现 蓝桥杯 算法提高 拿糖果
    Java实现 蓝桥杯 算法提高 拿糖果
    Java实现 蓝桥杯 算法提高 求arccos值
    Java实现 蓝桥杯 算法提高 求arccos值
    Java实现 蓝桥杯 算法提高 因式分解
    Java实现 蓝桥杯 算法提高 因式分解
  • 原文地址:https://www.cnblogs.com/wwssgg/p/14817482.html
Copyright © 2011-2022 走看看