一、浮点计算中发生精度丢失

大概很多有编程经验的朋友都对这个问题不陌生了:无论你使用的是什么编程语言,在使用浮点型数据进行精确计算时,你都有可能遇到计算结果出错的情况。来看下面的例子。

// 这是一个利用浮点型数据进行精确计算时结果出错的例子,使用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)

下面会附上Arith的源代码,大家只要把它编译保存好,要进行浮点数计算的时候,在你的源程序中导入Arith类就可以使用以上静态方法来进行浮点数的精确计算了。

附录:Arith源代码

import java.math.BigDecimal;

/**
* 由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精
* 确的浮点数运算,包括加减乘除和四舍五入。
*/
public class Arith {
//默认除法运算精度
private static final int DEF_DIV_SCALE = ;
//这个类不能实例化
private Arith(){
} /**
* 提供精确的加法运算
* @param d1 被加数
* @param d2 加数
* @return 两数之和
*/
public static double add(double d1,double d2){
BigDecimal b1=new BigDecimal(Double.toString(d1));
BigDecimal b2=new BigDecimal(Double.toString(d2));
return b1.add(b2).doubleValue();
} /**
* 提供精确的减法运算
* @param d1 被减数
* @param d2 减数
* @return 返回d1-d2的差
*/
public static double sub(double d1,double d2){
BigDecimal b1=new BigDecimal(Double.toString(d1));
BigDecimal b2=new BigDecimal(Double.toString(d2));
return b1.subtract(b2).doubleValue();
} /**
* 提供精确的乘法运算
* @param d1 被乘数
* @param d2 乘数
* @return 返回d1*d2的积
*/
public static double mul(double d1,double d2){
BigDecimal b1=new BigDecimal(Double.toString(d1));
BigDecimal b2=new BigDecimal(Double.toString(d2));
return b1.multiply(b2).doubleValue();
} /**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入。
* @param d1 被除数
* @param d2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 返回d1/d2的商
*/
public static double div(double d1,double d2,int scale){
if(scale<){
throw new IllegalArgumentException("the scale must be a positive Integer or zero");
}
BigDecimal b1=new BigDecimal(Double.toString(d1));
BigDecimal b2=new BigDecimal(Double.toString(d2));
return b1.divide(b2, scale,BigDecimal.ROUND_HALF_UP).doubleValue(); } /**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位,以后的数字四舍五入。
* @param d1 被除数
* @param d2 除数
* @return 返回d1/d2的商
*/
public static double div(double d1,double d2){
return div(d1,d2,DEF_DIV_SCALE);
}
}

14、Java中用浮点型数据Float和Double进行精确计算时的精度问题的更多相关文章

  1. Java中浮点型数据Float和Double进行精确计算的问题

    Java中浮点型数据Float和Double进行精确计算的问题 来源  https://www.cnblogs.com/banxian/p/3781130.html 一.浮点计算中发生精度丢失     ...

  2. JavaScript 中的所有数据都是以 64 位浮点型数据(float) 来存储。浮点型数据使用注意事项。全局变量特殊之处

    JavaScript 中的所有数据都是以 64 位浮点型数据(float) 来存储. 所有的编程语言,包括 JavaScript,对浮点型数据的精确度都很难确定: <!DOCTYPE html& ...

  3. 浮点型 float和double类型的内存结构和精度问题

    首先引用一个例子在java中可能你会遇到这样的问题: 例:0.99999999f==1f //true 0.9999999f==1f //false 这是超出精度造成的,为了知道为什么会造成这样的问题 ...

  4. 实型(浮点型):float、double

    #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib. ...

  5. Java语言中:float、double数据类型在内存中是如何存储的

    引用参考 https://www.cnblogs.com/chenmingjun/p/8415464.html#4291528 https://blog.csdn.net/yansmile1/arti ...

  6. 在java中,怎样把一个double数转换为字符串时,不用科学计数法表示。

    解决方法1: 对Double类型的数字进行 格式化输出 ,相对来说不是很精确 import java.text.DecimalFormat;   public class TestDouble_Str ...

  7. 第二章 Java浮点数精确计算

    1.实际意义 在实际开发中,如果需要进行float或double的精确计算(尤其是财务计算),直接使用float或double是不行的(具体的例子看下边的代码的main方法的测试结果),需要使用Big ...

  8. 关于 BigDecimal处理float、double数据

    Big Decimal 在java中,对于float与double中的数据,总会因为精度问题而丢失数据的准确性,也就是说对于两者所处理的得到的值是无限接近于那个数,而并非一个精确数字,而对于电商中所涉 ...

  9. 不要在精确计算中使用float和double类型

    http://blog.csdn.net/androiddevelop/article/details/8478879 一  问题描述 float和double类型不能用于精确计算,其主要目的是为了科 ...

随机推荐

  1. JAVA自定义注解SpringAOP

    原文:https://my.oschina.net/wangnian/blog/801348 前言:Annotation(注解)是JDK5.0及以后版本引入的,它的作用就是负责注解其他注解.现在开发过 ...

  2. Extjs文件选择器

    Ext.hoo.component.FileBrowserComponent.js /** * Ext.hoo.component.FileBrowserWindow 系统文件浏览选择组件,可以选定电 ...

  3. RobotFramework自动化1-环境搭建

    前言 Robot Framework是一款python编写的功能自动化测试框架.具备良好的可扩展性,支持关键字驱动,可以同时测试多种类型的客户端或者接口,可以进行分布式测试执行. Robot Fram ...

  4. ubuntu下如何查看自己的外网IP

    1.1 安装使用curl命令实现      sudo apt-get install curl1.2 输入命令      curl ifconfig.me

  5. restful处理

    重写/覆盖   HTTP 方法 一些HTTP客户端仅能处理简单的的GET和POST请求,为照顾这些功能有限的客户端,API需要一种方式来重写HTTP方法. 尽管没有一些硬性标准来做这事,但流行的惯例是 ...

  6. 《软件开发与创新:ThoughtWorks文集:续集》

    <软件开发与创新:ThoughtWorks文集:续集> 基本信息 原书名:The thoughtWorks anthology, volume 2:More essays on softw ...

  7. [SQLite] SQLite学习手册(数据库和事务)

    转载地址:http://www.cnblogs.com/stephen-liu74/archive/2012/02/18/2322575.html 一.Attach数据库: ATTACH DATABA ...

  8. spoj Goblin Wars(简单bfs)

    J - Goblin Wars Time Limit:432MS    Memory Limit:1572864KB    64bit IO Format:%lld & %llu Submit ...

  9. Servlet字符编码过滤器

    在Java Web程序开发中,由于Web容器内部使用编码格式并不支持中文字符集,所以,处理浏览器请求中的中文数据就会出现乱码的现象.由于Web容器使用了ISO-8859-1的编码格式,所以在Web应用 ...

  10. 混沌数学之离散点集图形DEMO

    最近看了很多与混沌相关的知识,并写了若干小软件.混沌现象是个有意思的东西,同时混沌也能够生成许多有意思的图形.混沌学的现代研究使人们渐渐明白,十分简单的数学方程完全可以模拟系统如瀑布一样剧烈的行为.输 ...