浮点精度(float、double)运算不精确的原因
结论:十进制小数与二进制的相互转换,时小数部分在转换过程中陷入了无线循环状态
一,问题描述
为什么浮点精度运算会有问题,我们平常使用的编程语言大多都有一个问题——浮点型精度运算会不准确。比如
double num = 0.1 + 0.1 + 0.1;// 输出结果为 0.30000000000000004
double num2 = 0.65 - 0.6;// 输出结果为 0.05000000000000004那么精度运算不准确这是为什么呢?我们接下来就需要从计算机所有数据的表现形式二进制说起了。如果大家很了解二进制与十进制的相互转换,那么就能轻易的知道精度运算不准确的问题原因是什么了。
如果不知道就让我们一起回顾一下十进制整数与二进制的相互转换流程。很熟的同学可以略过。
- 一般情况下二进制转为十进制我们所使用的是按权相加法。
- 十进制转二进制是除2取余,逆序排列法。
// 二进制到十进制
10010 = 0 * 2^0 + 1 * 2^1 + 0 * 2^2 + 0 * 2^3 + 1 * 2^4 = 18// 十进制到二进制 8 —> 10010
18 / 2 = 9 .... 0
9 / 2 = 4 .... 1
4 / 2 = 2 .... 0
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1
0 / 2 = 0 .... 0
// 逆序排列后得到 010010那么,问题来了十进制小数和二进制是如何相互转换的呢?
- 二进制小数到十进制小数还是使用按权相加法。
- 十进制小数到二进制小数一般是整数部分除 2 取余,逆序排列,小数部分使用乘 2 取整数位,小数部分继续使用乘 2 取整数位,顺序排列。
// 二进制到十进制
10.01 = 1 * 2^-2 + 0 * 2^-1 + 0 * 2^0 + 1 * 2^1 = 2.25// 十进制到二进制 2.25 ->10.01
// 整数部分 除2取余
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1
0 / 2 = 0 .... 0
// 小数部分 乘2取整数位
0.25 * 2 = 0.5 .... 0
0.5 * 2 = 1 .... 1
// 结果 整数部分的逆序排列 + 小数部分的顺序排列
10.01转小数我们也了解了,接下来我们回归正题,为什么浮点运算会有精度不准确的问题。接下来我们看一个简单的例子 2.1 这个十进制数转成二进制是什么样子的
// 十进制到二进制 2.1 -> 10.0001100110011........
// 整数部分 除2取余
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1
0 / 2 = 0 .... 0
// 小数部分
0.1 * 2 = 0.2 .... 0
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
....落入无限循环结果为 10.0001100110011........ , 我们的计算机在存储小数时肯定是有长度限制的,所以会进行截取部分小数进行存储,从而导致计算机存储的数值只能是个大概的值,而不是精确的值。从这里看出来我们的计算机根本就无法使用二进制来精确的表示 2.1 这个十进制数字的值,连表示都无法精确表示出来,计算肯定是会出现问题的。
二,问题解决
精度运算丢失的解决办法,现有三种办法
- 如果业务不是必须非常精确的要求可以采取四舍五入的方法来忽略这个问题。
- 转成整型再进行计算。
- 使用 BCD 码存储和运算二进制小数(感兴趣的同学可自行搜索学习)。
一般每种语言都用高精度运算的解决方法(比一般运算耗费性能),比如Java 的 BigDecimal,但是一定要把小数转成字符串传入构造,不然还是有坑。
BigDecimal add = new BigDecimal("0.1")
.add(new BigDecimal("0.1"))
.add(new BigDecimal("0.1"));
System.out.println(add);三,拓展:详解浮点型
上面既然提到了浮点型的存储是有限制,那么我们看一下我们的计算机是如何存储浮点型的,是不是真的正如我们上面提到的有小数长度的限制。那我们就以 Float 的数据存储结构来说,根据 IEEE 标准浮点型分为符号位,指数位和尾数位三部分(各部分大小详情见下图)。

一般情况下我们表示一个很大或很小的数通常使用科学记数法。例如:
- 1000.00001 我们一般表示为 1.00000001 * 10^3;
- 0.0001001 一般表示为 1.001 * 10^-4。
3.1 符号位
- 0 是正数
- 1 是负数
3.2 指数位
指数很有意思因为它需要表示正负,它规定指数部分的最大值 / 2 - 1 表示指数为 0。我们使用单精度浮点型举个例子,单精度浮点型指数位一共有八位,表示的十进制数最大就是 255。那么 255 / 2 - 1 = 127,127 就代表指数为 0。如果是 1.00000001 * 10^3,那么指数就是 127 + 3= 200 ,如果是1.001 * 10^-4,那么指数就是127 - 4= 123。
3.3 尾数位
比如上述例子中 1.00000001 和 1.001 就属于尾数,但是为什么叫尾数呢?因为在二进制中使用科学计数法表示后,比如 1.00000001 * 10^3和1.001 * 10^-4,小数点前面的 1 是永远存在的,存了也是浪费空间不如多存一位小数,所以尾数位只会存储小数部分,也就是 00000001 以及 001 存储这样的数据。