背景

在博客 恶心的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. SQL Server里因丢失索引造成的死锁

    在今天的文章里我想演示下SQL Server里在表上丢失索引如何引起死锁(deadlock)的.为了准备测试场景,下列代码会创建2个表,然后2个表都插入4条记录. -- Create a table ...

  2. 查找表或其他对象在某个Server上的存在

    EXEC sp_MSforeachdb 'use ? ;  IF EXISTS(SELECT top 1 1 FROM sys.syscomments WHERE text LIKE ''%test% ...

  3. Winform开发框架之客户关系管理系统(CRM)的开发总结系列2-基于框架的开发过程

    在上篇随笔<Winform开发框架之客户关系管理系统(CRM)的开发总结系列1-界面功能展示>中介绍了我的整个CRM系统的概貌,本篇继续本系列的文章,介绍如何基于我的<winform ...

  4. java 接口学习

    你应该知道接口是一种契约,它与实现方式无关 但是类,即使是抽象类,你都能自定义成员变量,而成员变量往往就与实现方式有关. 这一点的实际意义不大. 但是有一点,类会暴露太多不必要,甚至不能暴露的东西,你 ...

  5. ASP.NET几种页面数据绑定的用法及区别: <%#、 <%=、 <% 、<%@

    < %#... %>: 是在绑定控件DataBind()方法执行时被执行,用于数据绑定 如: < %# Container.DataItem("tit") %&g ...

  6. js generator数据类型

    1. 概述 generator 是ES6引入的新的数据类型, 看上去像一个函数,除了使用return返回, yield可以返回多次. generator 由function* 定义, (注意*号), ...

  7. 那些教程没有的php4-composer依赖管理工具

    phpcomposer PHP 5.3.2+ Composer 不是一个包管理器,但它在每个项目的基础上进行管理,在你项目的某个目录中(例如 vendor)进行安装.默认情况下它不会在全局安装任何东西 ...

  8. Linux下安装配置Nexus

    一.安装和运行nexus 1.下载nexus:http://www.sonatype.org/nexus/go 可选择tgz和zip格式,以及war,选择tgz或zip时不同版本可能在启动时存在一定问 ...

  9. 可怜的js居然没有块级作用域

    js中在一个函数中定义一个for循环:for(var i=0;i<5;i++) 其中的i并不会随着for循环的结束就销毁,i会一直存在该函数中,这就是js和其他语言的区别,也就是js没有块级作用 ...

  10. 关于C#中Environment.OSVersion判断操作系统及Win10上的问题

    我们都知道在C#中可以通过Environment.OSVersion来判断当前操作系统,下面是操作系统和主次版本的对应关系: 操作系统 主版本.次版本 Windows 10 10.0* Windows ...