背景

在博客 恶心的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.
301353.9999999883584678173065185546875

上面我们自己写了一个工具类,实现了 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.9999999883584678173065185546875
=========================
301353.8958988954
301353.8958988954
301353.89589889545
301353.89589889545
301353.8958988954593002796173095703125
301353.895898895458954895989
=========================
301353.46899434
301353.46899434
301353.46899434
301353.46899434
301353.46899439862386286258697509765625
=========================
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"的更多相关文章

  1. java 常用类库:BigInteger大整数;BigDecimal大小数(解决double精度损失);

    大整数BigInteger package com.zmd.common_class_libraries; import java.math.BigInteger; /** * @ClassName ...

  2. Java中的BigDecimal类精度问题

    bigdecimal 能保证精度的原理是:BigDecimal的解决方案就是,不使用二进制,而是使用十进制(BigInteger)+小数点位置(scale)来表示小数,就是把所有的小数变成整数,记录小 ...

  3. Java中的BigDecimal类和int和Integer总结

    前言 我们都知道浮点型变量在进行计算的时候会出现丢失精度的问题.如下一段代码: System.out.println(0.05 + 0.01); System.out.println(1.0 - 0. ...

  4. Java中的Bigdecimal类型运算

    Java中的Bigdecimal类型运算 双精度浮点型变量double可以处理16位有效数.在实际应用中,需要对更大或者更小的数进行运算和处理.Java在java.math包中提 供的API类BigD ...

  5. Java中是否可以调用一个类中的main方法?

    前几天面试的时候,被问到在Java中是否可以调用一个类中的main方法?回来测试了下,答案是可以!代码如下: main1中调用main2的主方法 package org.fiu.test; impor ...

  6. (转)Java中使用正则表达式的一个简单例子及常用正则分享

    转自:http://www.jb51.net/article/67724.htm 这篇文章主要介绍了Java中使用正则表达式的一个简单例子及常用正则分享,本文用一个验证Email的例子讲解JAVA中如 ...

  7. 【笔试题】Java 中如何递归显示一个目录下面的所有目录和文件?

    笔试题 Java 中如何递归显示一个目录下面的所有目录和文件? import java.io.File; public class Test { private static void showDir ...

  8. java中String,int,Integer,char、double类型转换

    java中String,int,Integer,char.double类型转换----https://www.cnblogs.com/kangyu222/p/5866025.html

  9. js double 精度损失 bugs

    js double 精度损失 bugs const arr = [ 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01 ]; // [ ...

随机推荐

  1. python网络编程socket /socketserver

    提起网络编程,不同于web编程,它主要是C/S架构,也就是服务器.客户端结构的.对于初学者而言,最需要理解的不是网络的概念,而是python对于网络编程都提供了些什么模块和功能.不同于计算机发展的初级 ...

  2. 分析SIX锁和锁分区导致的死锁

    什么是SIX锁? 官方文档锁模式中说到: 意向排他共享 (SIX):保护针对层次结构中某些(而并非所有)低层资源请求或获取的共享锁以及针对某些(而并非所有)低层资源请求或获取的意向排他锁. 顶级资源允 ...

  3. PP66 EEPPPPMM SSyysstteemm AAddmmiinniissttrraattiioonn GGuuiiddee 16 R1

    ※★◆●PP66 EEPPPPMM SSyysstteemm AAddmmiinniissttrraattiioonn GGuuiiddee 16 R1AApprriill 22001166Conte ...

  4. 使用innerHTML获取HTML代码时,HTML标记属性的双引号好多都消失不见了,原来是属性值中包含空格才会保留双引号

    最近搞的一个项目中所使用的方式比较奇怪,用Label显示HTML内容,然后不断地使用JS把Label的innerHTML复制到TextBox中. 但是,昨天发现了一个问题,获取元素值的时候,有时候正常 ...

  5. 实用笔记-EF中直接运行SQL命令

    在EF4.1,API的名字 有了些许改变,DbContext.Database就是对应于数据库端信息的封装.执行SQL命令也自然从Database类型开始.对应于ExecuteStoreCommand ...

  6. Python入门笔记(20):Python函数(3):关于lambda

    一.lambda函数 1.lambda函数基础: lambda函数也叫匿名函数,即,函数没有具体的名称,而用def创建的方法是有名称的.如下: """命名的foo函数&q ...

  7. 修改memcached服务的端口号

    windows下修改memcached服务的端口号(默认端口:11211) 如果不是作为服务启动memcached的话,memcached -p 端口号就可以了. 通过修改注册表可以简单实现 运行:r ...

  8. 关闭Eclipse中自动弹出console的功能

    当我们使用Eclipse编写代码的时候,一般都会使界面最大化,如果这时tomcat服务器处在运行的状态,那么当后台有打印内容,比如日志输出,代码程序报错输出时,console就会自动弹出,很不方便. ...

  9. STL---Codeforces675D Tree Construction(二叉树节点的父亲节点)

    Description During the programming classes Vasya was assigned a difficult problem. However, he doesn ...

  10. 泛函编程(9)-异常处理-Option

    Option是一种新的数据类型.形象的来描述:Option就是一种特殊的List,都是把数据放在一个管子里:然后在管子内部对数据进行各种操作.所以Option的数据操作与List很相似.不同的是Opt ...