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

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

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

// 这是一个利用浮点型数据进行精确计算时结果出错的例子,使用Java编写,有所省略。 编程

double a = (1.2 - 0.4) / 0.1;
System.out.println(a); 编程语言

    若是你认为这个程序的输出结果是“8”的话,那你就错了。实际上,程序的输出结果是“7.999999999999999”。好,问题来了。究竟是哪里出了错? ide

    浮点型数据进行精确计算时,该类问题并很多见。咱们姑且称其为“精度丢失”吧。你们能够试着改一下上面的程序,你会发现一些有趣的现象: 工具

    一、若是你直接使用一个数字代替括号里的表达式,如“0.8 / 0.1”或者“1.1 /0.1”,那么彷佛,注意只是彷佛不会出现问题; 测试

    二、咱们可能会作第二个测试,就是对比“0.8 / 1.1”和“(1.2 - 0.4) / 1.1”的结果,没错,我就是这样作的。那么你会发现,前者的结果是“0.7272727272727273”(四舍五入后的结果),然后者的结果是 “0.7272727272727272”(没有进行四舍五入)。能够推测,通过一次计算后,精度丢失了; .net

    三、很好,我以为咱们已经很接近真相了,可是接下来的第三个测试或许会让你泄气,我是这样作的:对比“(2.4 - 0.1) / 0.1”、“(1.2 - 0.1) / 0.1”以及“(1.1 - 0.1) / 0.1”的结果,第一个是“22.999999999999996”,第二个是“10.999999999999998”,第三个是“10.0”。彷佛完 全推翻了咱们的想法; 翻译

    四、你可能还不死心,由于在上面的测试里,第三个表达式括号中的结果实在太诡异了,正好是“1.0”。那咱们再来对比一下“(2.4 - 0.2) / 0.1”和“(2.4 - 0.3) / 0.1”,前者结果是“21.999999999999996”,后者结果是“21.0”。恭喜你,作到这里,你终于能够放弃这个无聊的测试了。 orm

    最后,咱们还能够来推翻一下咱们第一个测试的假设:当使用“2.3 / 0.1”时,结果为“22.999999999999996”,出现精度丢失。也就是说,所谓“通过一次计算后,精度丢失”的假设是不成立的。 ip

2、为什么会出现精度丢失

    那么为何会出现精度丢失呢?在查阅了一些资料之后,我稍微有了一些头绪,下面是本人的愚见,仅供参考。

    首先得从计算机自己去讨论这个问题。咱们知道,计算机并不能识别除了二进制数据之外的任何数据。不管咱们使用何种编程语言,在何种编译环境下工做,都要先 把源程序翻译成二进制的机器码后才能被计算机识别。以上面提到的状况为例,咱们源程序里的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,它 的小数点移动到了一个新的位置(即浮动了)。可见,浮点数原本就是用于科学计算的,用来进行精确计算实在太不合适了。

3、如何使用浮点数进行精确计算

    那么可以使用浮点数进行精确计算吗?直接计算固然是不行啦,可是咱们固然也能够经过一些方法和技巧来解决这个问题。因为浮点数计算的结果跟正确结果很是接近,你极可能想到使用四舍五入来处理结果,以获得正确的答案。这是个不错的思路。

    那么如何实现四舍五入呢?你可能会想到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 = 10;
    //这个类不能实例化
    private Arith(){
    }

    /**
     * 提供精确的加法运算。
     * @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){
        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){

        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();     } };

相关文章
相关标签/搜索