00-1010涉及浮点数据的处理,如浮点或双精度。偶尔,总会出现一些奇怪的现象。不知道你有没有注意到。给我几个常见的栗子:
典型现象(1):条件判断超出预期。
system . out . println(1f==0.9999999 f);//打印:假
system . out . println(1f==0.99999999 f);//打印:真纳尼?典型现象(2):数据转换超出预期。
floatf=1.1f
double=(double)f;
system . out . println(f);//打印:1.1
system . out . println(d);//打印:1.100000841858纳尼?典型现象(3):基础操作超预期。
system . out . println(0.20.7);
//打印:0.8999.9999999999999纳尼典型现象(4):数据自增超预期。
floatf1=8455263f
for(inti=0;i10I){ 0
system . out . println(f1);
f1;
}
//打印:8455263.0
//打印:8455264.0
//打印:8455265.0
//打印:8455266.0
//打印:8455267.0
//打印:8455268.0
//打印:8455269.0
//打印:8455270.0
//打印:8455271.0
//打印:8455272.0
floatf2=84552631f
for(inti=0;i10I){ 0
system . out . println(F2);
F2;
}
//打印:8.4552632E7纳尼是不是1?
//打印:8.4552632E7纳尼是不是1?
//打印:8.4552632E7纳尼是不是1?
//打印:8.4552632E7纳尼是不是1?
//打印:8.4552632E7纳尼是不是1?
//打印:8.4552632E7纳尼是不是1?
//打印:8.4552632E7纳尼是不是1?
//打印:8.4552632E7纳尼是不是1?
//打印:8.4552632E7纳尼是不是1?
//打印:8.4552632E7纳尼是不是1?看,这些简单场景中的用法很难满足我们的需求,所以有很多隐藏的漏洞等着我们用浮点数(包括双精度和浮点)来处理问题!
难怪技术总监说了一句狠话:凡是敢在处理商品金额、订单交易、币种计算等事情时使用双/浮数据的,就直接放我们走!
00-1010我们以第一个典型现象为例来分析一下:
system . out . println(1f==0.99999999 f);直接用代码对比1和0.99999999,居然打印出来真!
这是什么意思?这说明电脑根本分不清这两个数字。这是为什么?
让我们简单考虑一下:
我们知道这两个浮点数只是我们肉眼看到的具体数值,是我们通常理解的十进制数。但是,计算机的底层不是按照十进制计算的。学过基本的群体计数原理的人都知道,计算机的底层最终是基于010010011001100111100111011这样的0,1二进制系统。
所以为了了解实际情况,我们应该把这两个十进制浮点数转换成二进制空间看看。
十进制浮点数如何转换为二进制数以及如何计算,我想这应该属于计算机基础十进制转换的常识,想必在《计算机组成原理》的同类课上已经学过了,这里就不赘述了,直接给出结果(转换为IEEE 754 Single precision 32位,即浮点类型对应的精度)。
1.0(十进制)
00111111 10000000 00000000
00000000(二进制) ↓ 0x3F800000(十六进制) 0.99999999(十进制) ↓ 00111111 10000000 00000000 00000000(二进制) ↓ 0x3F800000(十六进制)果不其然,这两个十进制浮点数的底层二进制表示是一毛一样的,怪不得==的判断结果返回true!
但是1f == 0.9999999f返回的结果是符合预期的,打印false,我们也把它们转换到二进制模式下看看情况:
1.0(十进制) ↓ 00111111 10000000 00000000 00000000(二进制) ↓ 0x3F800000(十六进制) 0.9999999(十进制) ↓ 00111111 01111111 11111111 11111110(二进制) ↓ 0x3F7FFFFE(十六进制)哦,很明显,它俩的二进制数字表示确实不一样,这是理所应当的结果。
那么为什么0.99999999的底层二进制表示竟然是:00111111 10000000 00000000 00000000呢?
这不明明是浮点数1.0的二进制表示吗?
这就要谈一下浮点数的精度问题了。
浮点数的精度问题!
学过 《计算机组成原理》 这门课的小伙伴应该都知道,浮点数在计算机中的存储方式遵循IEEE 754 浮点数计数标准,可以用科学计数法表示为:
只要给出:符号(S)、阶码部分(E)、尾数部分(M) 这三个维度的信息,一个浮点数的表示就完全确定下来了,所以float和double这两种浮点数在内存中的存储结构如下所示:
1、符号部分(S)
0-正 1-负
2、阶码部分(E)(指数部分):
对于float型浮点数,指数部分8位,考虑可正可负,因此可以表示的指数范围为-127 ~ 128对于double型浮点数,指数部分11位,考虑可正可负,因此可以表示的指数范围为-1023 ~ 10243、尾数部分(M):
浮点数的精度是由尾数的位数来决定的:
对于float型浮点数,尾数部分23位,换算成十进制就是 2^23=8388608,所以十进制精度只有6 ~ 7位;对于double型浮点数,尾数部分52位,换算成十进制就是 2^52 = 4503599627370496,所以十进制精度只有15 ~ 16位所以对于上面的数值0.99999999f,很明显已经超过了float型浮点数据的精度范围,出问题也是在所难免的。
精度问题如何解决
所以如果涉及商品金额、交易值、货币计算等这种对精度要求很高的场景该怎么办呢?
方法一:用字符串或者数组解决多位数问题
校招刷过算法题的小伙伴们应该都知道,用字符串或者数组表示大数是一个典型的解题思路。
比如经典面试题:编写两个任意位数大数的加法、减法、乘法等运算。
这时候我们我们可以用字符串或者数组来表示这种大数,然后按照四则运算的规则来手动模拟出具体计算过程,中间还需要考虑各种诸如:进位、借位、符号等等问题的处理,确实十分复杂,本文不做赘述。
方法二:Java的大数类是个好东西
JDK早已为我们考虑到了浮点数的计算精度问题,因此提供了专用于高精度数值计算的大数类来方便我们使用。
在前文《不瞒你说,我最近跟Java源码杠上了》中说过,Java的大数类位于java.math包下:
可以看到,常用的BigInteger 和 BigDecimal就是处理高精度数值计算的利器。
BigDecimal num3 = new BigDecimal( Double.toString( 0.1f ) ); BigDecimal num4 = new BigDecimal( Double.toString( 0.99999999f ) ); System.out.println( num3 == num4 ); // 打印 false BigDecimal num1 = new BigDecimal( Double.toString( 0.2 ) ); BigDecimal num2 = new BigDecimal( Double.toString( 0.7 ) ); // 加 System.out.println( num1.add( num2 ) ); // 打印:0.9 // 减 System.out.println( num2.subtract( num1 ) ); // 打印:0.5 // 乘 System.out.println( num1.multiply( num2 ) ); // 打印:0.14 // 除 System.out.println( num2.divide( num1 ) ); // 打印:3.5当然了,像BigInteger 和 BigDecimal这种大数类的运算效率肯定是不如原生类型效率高,代价还是比较昂贵的,是否选用需要根据实际场景来评估。