Java中浮点型数据Float和Double进行精确计算的问题
Java中浮点型数据Float和Double进行精确计算的问题
来源 https://www.cnblogs.com/banxian/p/3781130.html
一、浮点计算中发生精度丢失
大概很多有编程经验的朋友都对这个问题不陌生了:无论你使用的是什么编程语言,在使用浮点型数据进行精确计算时,你都有可能遇到计算结果出错的情况。来看下面的例子。
// 这是一个利用浮点型数据进行精确计算时结果出错的例子,使用Java编写,有所省略。
double a = (1.2 - 0.4) / 0.1;
System.out.println(a);
如果你认为这个程序的输出结果是“8”的话,那你就错了。实际上,程序的输出结果是“7.999999999999999”。好,问题来了。到底是哪里出了错?
浮点型数据进行精确计算时,该类问题并不少见。我们姑且称其为“精度丢失”吧。大家可以试着改一下上面的程序,你会发现一些有趣的现象:
1、如果你直接使用一个数字代替括号里的表达式,如“0.8 / 0.1”或者“1.1 /0.1”,那么似乎,注意只是似乎不会出现问题;
2、我们可能会做第二个测试,就是对比“0.8 / 1.1”和“(1.2 - 0.4) / 1.1”的结果,没错,我就是这样做的。那么你会发现,前者的结果是“0.7272727272727273”(四舍五入后的结果),而后者的结果是 “0.7272727272727272”(没有进行四舍五入)。可以推测,经过一次计算后,精度丢失了;
3、很好,我觉得我们已经很接近真相了,但是接下来的第三个测试或许会让你泄气,我是这样做的:对比“(2.4 - 0.1) / 0.1”、“(1.2 - 0.1) / 0.1”以及“(1.1 - 0.1) / 0.1”的结果,第一个是“22.999999999999996”,第二个是“10.999999999999998”,第三个是“10.0”。似乎完 全推翻了我们的想法;
4、你可能还不死心,因为在上面的测试里,第三个表达式括号中的结果实在太诡异了,正好是“1.0”。那我们再来对比一下“(2.4 - 0.2) / 0.1”和“(2.4 - 0.3) / 0.1”,前者结果是“21.999999999999996”,后者结果是“21.0”。恭喜你,做到这里,你终于可以放弃这个无聊的测试了。
最后,我们还可以来推翻一下我们第一个测试的假设:当使用“2.3 / 0.1”时,结果为“22.999999999999996”,出现精度丢失。也就是说,所谓“经过一次计算后,精度丢失”的假设是不成立的。
二、为何会出现精度丢失
那么为什么会出现精度丢失呢?在查阅了一些资料以后,我稍微有了一些头绪,下面是本人的愚见,仅供参考。
首先得从计算机本身去讨论这个问题。我们知道,计算机并不能识别除了二进制数据以外的任何数据。无论我们使用何种编程语言,在何种编译环境下工作,都要先 把源程序翻译成二进制的机器码后才能被计算机识别。以上面提到的情况为例,我们源程序里的2.4是十进制的,计算机不能直接识别,要先编译成二进制。但问 题来了,2.4的二进制表示并非是精确的2.4,反而最为接近的二进制表示是2.3999999999999999。原因在于浮点数由两部分组成:指数和 尾数,这点如果知道怎样进行浮点数的二进制与十进制转换,应该是不难理解的。如果在这个转换的过程中,浮点数参与了计算,那么转换的过程就会变得不可预 知,并且变得不可逆。我们有理由相信,就是在这个过程中,发生了精度的丢失。而至于为什么有些浮点计算会得到准确的结果,应该也是碰巧那个计算的二进制与 十进制之间能够准确转换。而当输出单个浮点型数据的时候,可以正确输出,如
double d = 2.4;
System.out.println(d);
输出的是2.4,而不是2.3999999999999999。也就是说,不进行浮点计算的时候,在十进制里浮点数能正确显示。这更印证了我以上的想法,即如果浮点数参与了计算,那么浮点数二进制与十进制间的转换过程就会变得不可预知,并且变得不可逆。
事实上,浮点数并不适合用于精确计算,而适合进行科学计算。这里有一个小知识:既然float和double型用来表示带有小数点的数,那为什么我们不称 它们为“小数”或者“实数”,要叫浮点数呢?因为这些数都以科学计数法的形式存储。当一个数如50.534,转换成科学计数法的形式为5.053e1,它 的小数点移动到了一个新的位置(即浮动了)。可见,浮点数本来就是用于科学计算的,用来进行精确计算实在太不合适了。
三、如何使用浮点数进行精确计算
那么能够使用浮点数进行精确计算吗?直接计算当然是不行啦,但是我们当然也可以通过一些方法和技巧来解决这个问题。由于浮点数计算的结果跟正确结果非常接近,你很可能想到使用四舍五入来处理结果,以得到正确的答案。这是个不错的思路。
那么如何实现四舍五入呢?你可能会想到Math类中的round方法,但是有个问题,round方法不能设置保留几位小数,如果我们要保留两位小数,我们只能像这样实现:
public double round(double value){
return Math.round(value*100)/100.0;
}
如果这能得到正确的结果也就算了,大不了我们再想方法改进。但是非常不幸,上面的代码并不能正常工作,如果给这个方法传入4.015,它将返回4.01而不是4.02。
java.text.DecimalFormat也不能解决这个问题,来看下面的例子:
System.out.println(new java.text.DecimalFormat("0.00").format(4.025));
它的输出是4.02,而非4.03。
难道没有解决方法了吗?当然有的。在《Effective Java》这本书中就给出了一个解决方法。该书中也指出,float和double只能用来做科学计算或者是工程计算,在商业计算等精确计算中,我们要用java.math.BigDecimal。
BigDecimal类一个有4个方法,我们只关心对我们解决浮点型数据进行精确计算有用的方法,即
BigDecimal(double value) // 将double型数据转换成BigDecimal型数据
思路很简单,我们先通过BigDecimal(double value)方法,将double型数据转换成BigDecimal数据,然后就可以正常进行精确计算了。等计算完毕后,我们可以对结果做一些处理,比如 对除不尽的结果可以进行四舍五入。最后,再把结果由BigDecimal型数据转换回double型数据。
这个思路很正确,但是如果你仔细看看API里关于BigDecimal的详细说明,你就会知道,如果需要精确计算,我们不能直接用double,而非要用 String来构造BigDecimal不可!所以,我们又开始关心BigDecimal类的另一个方法,即能够帮助我们正确完成精确计算的 BigDecimal(String value)方法。
// BigDecimal(String value)能够将String型数据转换成BigDecimal型数据
那么问题来了,想像一下吧,如果我们要做一个浮点型数据的加法运算,需要先将两个浮点数转为String型数据,然后用 BigDecimal(String value)构造成BigDecimal,之后要在其中一个上调用add方法,传入另一个作为参数,然后把运算的结果(BigDecimal)再转换为浮 点数。如果每次做浮点型数据的计算都要如此,你能够忍受这么烦琐的过程吗?至少我不能。所以最好的办法,就是写一个类,在类中完成这些繁琐的转换过程。这 样,在我们需要进行浮点型数据计算的时候,只要调用这个类就可以了。网上已经有高手为我们提供了一个工具类Arith来完成这些转换操作。它提供以下静态 方法,可以完成浮点型数据的加减乘除运算和对其结果进行四舍五入的操作:
public static double add(double v1,double v2)
public static double sub(double v1,double v2)
public static double mul(double v1,double v2)
public static double div(double v1,double v2)
public static double div(double v1,double v2,int scale)
public static double round(double v,int scale)
import java.math.BigDecimal; /**
* 由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精
* 确的浮点数运算,包括加减乘除和四舍五入。
*/
class DoubleArithUtil {
//默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
//这个类不能实例化
private DoubleArithUtil(){
} /**
* 提供精确的加法运算。
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2).doubleValue();
} /**
* 提供精确的减法运算。
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2).doubleValue();
} /**
* 提供精确的乘法运算。
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2).doubleValue();
} /**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位,以后的数字四舍五入。
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
public static double div(double v1,double v2){
return div(v1,v2,DEF_DIV_SCALE);
} /**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入。
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double div(double v1,double v2,int scale) throws ArithmeticException {
if(scale<0){
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
} /**
* 提供精确的小数位四舍五入处理。
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static double round(double v,int scale) throws ArithmeticException { if(scale<0){
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(Double.toString(v));
BigDecimal one = new BigDecimal("1");
return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}
} public class HelloWorld {
public static void main(String[] args) {
double x1 = DoubleArithUtil.add(3.4, 2.3);
double x2 = DoubleArithUtil.div(3.5, 6.7);
double x3 = DoubleArithUtil.mul(10.2, 12.7);
double x4 = DoubleArithUtil.sub(5.6, 4.5);
System.out.print("x1: " + x1 + "\n" +
"x2: " + x2 + "\n" +
"x3: " + x3 + "\n" +
"x4: " + x4 + "\n");
}
}
结果输出:
>javac -encoding utf-8 --source-path .\src --class-path .\src -d .\src -Xlint:deprecation -g .\src\HelloWorld.java
.\src\HelloWorld.java:76: 警告: [deprecation] BigDecimal中的ROUND_HALF_UP已过时
return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
^
.\src\HelloWorld.java:76: 警告: [deprecation] BigDecimal中的divide(BigDecimal,int,int)已过时
return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
^
.\src\HelloWorld.java:93: 警告: [deprecation] BigDecimal中的ROUND_HALF_UP已过时
return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
^
.\src\HelloWorld.java:93: 警告: [deprecation] BigDecimal中的divide(BigDecimal,int,int)已过时
return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
^
4 个警告 >java -XX:+PrintGCDetails -Dfile.encoding=utf-8 -classpath .\src HelloWorld
x1: 5.7
x2: 0.5223880597
x3: 129.54
x4: 1.1 >
================= End
Java中浮点型数据Float和Double进行精确计算的问题的更多相关文章
- 14、Java中用浮点型数据Float和Double进行精确计算时的精度问题
一.浮点计算中发生精度丢失 大概很多有编程经验的朋友都对这个问题不陌生了:无论你使用的是什么编程语言,在使用浮点型数据进行精确计算时,你都有可能遇到计算结果出错的情况.来看下面的例子. // 这是一个 ...
- JavaScript 中的所有数据都是以 64 位浮点型数据(float) 来存储。浮点型数据使用注意事项。全局变量特殊之处
JavaScript 中的所有数据都是以 64 位浮点型数据(float) 来存储. 所有的编程语言,包括 JavaScript,对浮点型数据的精确度都很难确定: <!DOCTYPE html& ...
- java中商业数据计算时用到的类BigDecimal和DecimalFormat
1.引言 借用<Effactive Java>这本书中的话,float和double类型的主要设计目标是为了科学计算和工程计算.他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确 ...
- Java中静态数据的初始化顺序
Java的类中的数据成员中包含有静态成员(static)时,静态数据成员的初始化顺序是怎样的呢? [程序实例1] import java.util.*; import java.lang.*; imp ...
- Java中XML数据
Java中XML数据 XML解析——Java中XML的四种解析方式 XML是一种通用的数据交换格式,它的平台无关性.语言无关性.系统无关性.给数据集成与交互带来了极大的方便.XML在不同的语言环境中解 ...
- java中浮点数的比较(double, float)(转)
问题的提出:如果我们编译运行下面这个程序会看到什么? public static void main(String args[]){ System.out.println(0.05+0.01); Sy ...
- JAVA中的数据存储空间简述
在 JAVA 中,有六个不同的地方可以存储数据: 1. 寄存器( register ): 最快的存储区,因为它位于不同于其他存储区——处理器内部.但是寄存器的数量极其有限,所以寄存器由编译器根据需求进 ...
- java中如何使用BigDecimal使得Double类型保留两位有效数字
一.场景:从数据表中读出Decimal类型的数据直接塞给Double类型的对象时,并不会有什么异常. 如果要再此基础上计算,就会发生异常. 比如:读出数据为0.0092,将其乘以100,则变成了0.9 ...
- JAVA中的数据存储(堆及堆栈)
转自:http://www.iteye.com/topic/6345301.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制.2. 栈:存放基本类型的变量数据和对象的引用,但对象 ...
随机推荐
- Identity(四)
本文摘自:ASP.NET MVC 随想录——探索ASP.NET Identity 身份验证和基于角色的授权,中级篇 探索身份验证与授权 在这一小节中,我将阐述和证明ASP.NET 身份验证和授权的工作 ...
- SQL Server中的Merge关键字(转载)
简介 Merge关键字是一个神奇的DML关键字.它在SQL Server 2008被引入,它能将Insert,Update,Delete简单的并为一句.MSDN对于Merge的解释非常的短小精悍:”根 ...
- SPOJ GSS(Can you answer the Queries)系列 7/8
GSS1 线段树最大子段和裸题,不带修改,注意pushup. 然而并不会猫树之类的东西 #include<bits/stdc++.h> #define MAXN 50001 using n ...
- EZ 2018 06 10 NOIP2018 模拟赛(十八)
好久没写blog&&比赛题解了,最近补一下 这次还是很狗的,T3想了很久最后竟然连并查集都忘写了,然后T2map莫名爆炸. Rating爆减......链接不解释 好了我们开始看题. ...
- ASP.NET Core使用TopShelf部署Windows服务
asp.net core很大的方便了跨平台的开发者,linux的开发者可以使用apache和nginx来做反向代理,windows上可以用IIS进行反向代理. 反向代理可以提供很多特性,固然很好.但是 ...
- C# 全屏坐标及区域坐标获取。自定义光标及系统光标描边捕捉显示。
最近手头工作比较轻松了一点就继续研究和完善之前的录屏软件,使用AForge最大的问题在于:最原始的只能够录全屏,而自定义的录屏需要更改非常多的细节:like follows: 1.需要支持区域化录屏: ...
- mysql 通过慢查询日志查写得慢的sql语句
MySQL通过慢查询日志定位那些执行效率较低的SQL 语句,用--log-slow-queries[=file_name]选项启动时,mysqld 会写一个包含所有执行时间超过long_query_t ...
- ubuntu16.04在GTX1070环境下安装 cuda9.1
设备要求 系统:Ubuntu16.04 显卡:GTX 1070 驱动:nvidia系列,显卡驱动的版本必须大于等于cuda的sh文件名里面的版本号 驱动可从 此处 下载,我已经整理好了 检查安装驱动 ...
- @Pointcut的用法
在Spring 2.0中,Pointcut的定义包括两个部分:Pointcut表示式(expression)和Pointcut签名(signature).让我们先看看execution表示式的格式: ...
- vuejs基础
**### 数据与方法 // 我们的数据对象 var data = { a: 1 } // 该对象被加入到一个 Vue 实例中 var vm = new Vue({ data: data }) // ...