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.
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"的更多相关文章
- java 常用类库:BigInteger大整数;BigDecimal大小数(解决double精度损失);
大整数BigInteger package com.zmd.common_class_libraries; import java.math.BigInteger; /** * @ClassName ...
- Java中的BigDecimal类精度问题
bigdecimal 能保证精度的原理是:BigDecimal的解决方案就是,不使用二进制,而是使用十进制(BigInteger)+小数点位置(scale)来表示小数,就是把所有的小数变成整数,记录小 ...
- Java中的BigDecimal类和int和Integer总结
前言 我们都知道浮点型变量在进行计算的时候会出现丢失精度的问题.如下一段代码: System.out.println(0.05 + 0.01); System.out.println(1.0 - 0. ...
- Java中的Bigdecimal类型运算
Java中的Bigdecimal类型运算 双精度浮点型变量double可以处理16位有效数.在实际应用中,需要对更大或者更小的数进行运算和处理.Java在java.math包中提 供的API类BigD ...
- Java中是否可以调用一个类中的main方法?
前几天面试的时候,被问到在Java中是否可以调用一个类中的main方法?回来测试了下,答案是可以!代码如下: main1中调用main2的主方法 package org.fiu.test; impor ...
- (转)Java中使用正则表达式的一个简单例子及常用正则分享
转自:http://www.jb51.net/article/67724.htm 这篇文章主要介绍了Java中使用正则表达式的一个简单例子及常用正则分享,本文用一个验证Email的例子讲解JAVA中如 ...
- 【笔试题】Java 中如何递归显示一个目录下面的所有目录和文件?
笔试题 Java 中如何递归显示一个目录下面的所有目录和文件? import java.io.File; public class Test { private static void showDir ...
- java中String,int,Integer,char、double类型转换
java中String,int,Integer,char.double类型转换----https://www.cnblogs.com/kangyu222/p/5866025.html
- 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 ]; // [ ...
随机推荐
- Java魔法堂:深入正则表达式API
目录 一.前言 二.正则表达式的使用诉求 三.java.util.regex包 四.java.lang.String实例 五.最短路径实现诉求 六.Java支持的正则表达式功能语法 七.总结 八.参考 ...
- hive的内部表与外部表创建
最近才接触Hive.学到了一些东西,就先记下来,免得以后忘了. 1.创建表的语句:Create [EXTERNAL] TABLE [IF NOT EXISTS] table_name [(col_na ...
- JavaScript一些函数
1.prompt()函数:有两个参数,一个是显示用户输入框上面的标签,另一个是输入框的初始值.用来接收用户输入的值,然后把它返回到代码中: 例如: <doctype html> <h ...
- C#设计模式——适配器模式(Adapter Pattern)
一.概述在软件开发中,常常会想要复用一个已经存在的组件,但该组件的接口却与我们的需要不相符,这时我们可以创建一个适配器,在需复用的组件的接口和我们需要的接口间进行转换,从而能够正常的使用需复用的组件. ...
- 设计模式--外观(Facade)模式
Insus.NET在去年有写过一篇<软件研发公司,外观设计模式(Facade)>http://www.cnblogs.com/insus/archive/2013/02/27/293606 ...
- (二)Protobuf的C#使用
[转]http://blog.csdn.net/shantsc/article/details/50729402 protobuf c#版本分成两个版本,一个是protobuf-net,另一个是pr ...
- C#函数、参数数组(例子)★
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- [moka同学笔记]bootstrap基础
1.导航栏的制作 <nav class="nav navbar-default navbar-fixed-top" role="navigation"&g ...
- Java与线程
导语 我们知道,new一个thread,调用它的start的方法,就可以创建一个线程,并且启动该线程,然后执行该线程需要执行的业务逻辑, 那么run方法是怎么被执行的呢? Java线程和os线程 os ...
- quartz使用(一)
在项目中经常会碰到定时任务,quartz是一款非常优秀的开源框架, 提供了定时任务的支持,还支持任务的持久化,并且提供了对数据库的支持.下面首先对quartz做一个简单介绍,并附上一个小例子. 1.下 ...