JAVA 浮点类型的坑

背景

今天在项目里踩到了个坑,就是浮点类型的四则运算后,发现结果不是预期的。

上代码:

public class DateTypeApplication {
    public static void main(String[] args) {
        System.out.println(2.43 +0.031);
    }
}

预期结果:2.461
实际结果:2.4610000000000003

原因分析

2.43 +0.031 两个数字都是double类型的,执行 “ + ”操作的时候。系统处理的流程如下
1:把 2.43 和0.031都转换成二制数字
2:然后执行 “ +” 操作。
3:再结果转成 十进制数。

众所周知,10进度的小数部分转成二进度是有精度丢失风险的。就好像 1/3 的 计算一样,结果有可能会是个无穷数,最后只能截断,然后导致丢失精度。
那么在第1步的时候就已经把精度丢失了,后面就会一直错下去。。貌似解释通了。

解决方案

Java提供了 BigDecimal类型为解决这个问题。

public class DateTypeApplication {
    public static void main(String[] args) {
        System.out.println(BigDecimal.valueOf(2.43).add(BigDecimal.valueOf(0.031)));
    }
}

结果
2.461

疑问与吐槽

因为目前的使用习惯是十进制。而JAVA却反其道而行,显示用的是十进制,计算却用的是二进制。
另外,虽然JAVA也提供了解决的方案BigDecimal,但我想说这是屎一样的设计,屎一般的解决方案。。
站在开发者的角度,你告诉我2.43 +0.031 直接用是错的,应该用: BigDecimal.valueOf(2.43).add(BigDecimal.valueOf(0.031))。
总结差异包括以下:
1:思维方式不够直接。当然写多了还是可以习惯。我每写一次我就想吐槽一次。
2:代码量多了,又有数据类型的转换。
3:代码的可读性不好。
最后,我还是想吐槽,干么不直接解决 double的问题,而是重新搞一套?

原因与发展过程

做过了一些语言的横向对比,包括C++,javascript, C#都会有类似的问题,其中C#的double类型微软做过优化,精度的问题比较少,但重复多次的试验之后,也照样的问题。
既然都有问题,那么这应该是个行业准备,是的。遵循的是IEEE 754标准。

IEEE 754标准

什么是IEEE 754标准?
最权威的解释是IEEE754标准本身ANSI/IEEE Std 754-1985《IEEE Standard for Binary Floating-Point Arithmetic》,网上有PDF格式的文件,一下,下载即可。标准文本是英文的,总共才23页,有耐心的话可以仔细阅读。IEEE754

IEEE 754 规定:

a) 两种基本浮点格式:单精度和双精度。
IEEE单精度格式具有24位有效数字,并总共占用32 位。IEEE双精度格式具有53位有效数字精度,并总共占用64位。
说明:基本浮点格式是固定格式,相对应的十进制有效数字分别为7位和17位。基本浮点格式对应的C/C++类型为float和double。
b) 两种扩展浮点格式:单精度扩展和双精度扩展。
此标准并未规定扩展格式的精度和大小,但它指定了最小精度和大小。例如,IEEE 双精度扩展格式必须至少具有64位有效数字,并总共占用至少79 位。
说明:虽然IEEE 754标准没有规定具体格式,但是实现者可以选择符合该规定的格式,一旦实现,则为固定格式。例如:x86 FPU是80位扩展精度,而Intel安腾FPU是82位扩展精度,都符合IEEE 754标准的规定。C/C++对于扩展双精度的相应类型是long double,但是,Microsoft Visual C++ 6.0版本以上的编译器都不支持该类型,long double和double一样,都是64位基本双精度,只能用其它C/C++编译器或汇编语言。
c) 浮点运算的准确度要求:加、减、乘、除、平方根、余数、将浮点格式的数舍入为整数值、在不同浮点格式之间转换、在浮点和整数格式之间转换以及比较。
求余和比较运算必须精确无误。其他的每种运算必须向其目标提供精确的结果,除非没有此类结果,或者该结果不满足目标格式。对于后一种情况,运算必须按照下面介绍的规定舍入模式的规则对精确结果进行最低限度的修改,并将经过此类修改的结果提供给运算的目标。
说明:IEEE 754没有规定基本算术运算(+、-、×、/ 等)的结果必须精确无误,因为对于IEEE 754的二进制浮点数格式,由于浮点格式长度固定,基本运算的结果几乎不可能精确无误。这里用三位精度的十进制加法来说明:
例1:a = 3.51,b = 0.234,求a+b = ?
a与b都是三位有效数字,但是,a+b的精确结果为3.744,是四位有效数字,对于该浮点格式只有三位精度,a+b的结果无法精确表示,只能近似表示,具体运算结果取决于舍入模式(见舍入模式的说明)。同理,由于浮点格式固定,对于其他基本运算,结果也几乎无法精确表示。

还是有点疑惑

标准说了说了那么多,到底说十进度浮点数转二进制丢失精度是必然的,而丢失精度又是依有据合理的。但这么说我又不服了,为什么要用这个标准。。不是可以做到准确吗?干么又不用替换掉不准确的。我又疑惑了。
于是我又寻找相关资料。

性能

二进度是硬件级别支持的,而十进制则不支持,用软件实现的十进制浮点计算比硬件实现的二进制浮点计算要慢100-1000倍。
以下我做个实验。

import org.springframework.util.StopWatch;

import java.math.BigDecimal;

public class DateTypePerformanceTestApplication {

    public static void main(String[] args) {

        StopWatch watch =new StopWatch();
        watch.start();
        double d1=2.6;

        for(int i=0;i<10000000;i++)
        {
            d1+=0.01;
        }
        watch.stop();
        System.out.println("double计算花耗时间:"+watch.getTotalTimeMillis() +",最终值:"+d1);
        
        watch =new StopWatch();
        watch.start();
        BigDecimal  d2= BigDecimal.valueOf(2.6) ;
        for(int i=0;i<10000000;i++)
        {
            d2.add( new BigDecimal(0.1));
        }
        watch.stop();
        System.out.println("BigDecimal计算花耗时间:"+watch.getTotalTimeMillis()+",最终值:"+d2);

    }
}

最终结果

double计算花耗时间:12,最终值:100002.59998630833
BigDecimal计算花耗时间:3461,最终值:2.6

实际上测试的性能是二制度比十进制超过300多倍。多次累计操作的结果也相差甚远。

写在最后

1:性能与精准不能同理兼顾。
2:开发人员需要根据不同应用场所去使用合适的方法。
1)金融,财务类的还是需要使用BigDecimal。如果大批量的统计尽量放到数据库去处理。
2)页面UI计算类的,可以直接使用double。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章