首页 > 编程知识 正文

他说(都说)

时间:2023-05-04 17:04:54 阅读:98639 作者:4265

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 ~ 1024

3、尾数部分(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这种大数类的运算效率肯定是不如原生类型效率高,代价还是比较昂贵的,是否选用需要根据实际场景来评估。


版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。