Jonsson Yan' Blog

怕什么真理无穷,进一步有进一步的欢喜。

0%

Java中使用BigDecimal进行浮点数计算

为什么 0.1+0.2 != 0.3?

先看一个诡异的代码 0.1+0.2!=0.3(js)

浮点数的编码方式

首先我们需要知道在计算机的世界里,0.1+0.2 为什么不等于 0.3 的,大家可以自己尝试一下,这里就牵扯到了浮点数的编码方式,浮点数在计算机中的存储方式遵循 IEEE 754 浮点数计数标准,可以表示为 采用尾数 + 阶码的编码方式,符号(S)、阶码部分(E)、尾数部分(M)三个确定下来,就可以确定一个浮点数。

  1. 符号部分(S)

0-正 1-负

  1. 阶码部分(E)(指数部分):

对于 float 型浮点数,指数部分 8 位,考虑可正可负,因此可以表示的指数范围为-127 ~ 128 对于 double 型浮点数,指数部分 11 位,考虑可正可负,因此可以表示的指数范围为-1023 ~ 1024

  1. 尾数部分(M):

浮点数的精度是由尾数的位数来决定的:

对于 float 型浮点数,尾数部分 23 位,换算成十进制就是 2^23=8388608,所以十进制精度只有 6 ~ 7 位; 对于 double 型浮点数,尾数部分 52 位,换算成十进制就是 2^52 = 4503599627370496,所以十进制精度只有 15 ~ 16 位

所以浮点数交给计算机存储时可能会出现精度丢失的情况。记住 float 的精度上限是 67 位,double 的精度上限是 1516 位。

浮点数十进制转二进制

举个例子 0.875 转换为二进制该怎么做?

  1. 以小数点将浮点数拆分为整数部分和小数部分

  2. 整数部分使用:除 2 取余法 即可,如果为 0,则无需操作

  3. 小数部分使用:乘 2 取整法 即可,计算过程如下

  1. 合并结果:整数部分+小数部分

最终得到二进制结果为 0.111 以下是 0.1 和 0.2 的二进制表示

1
2
0.1 的二进制 0.0001100110011001...(无限循环)
0.2 的二进制 0.0011001100110011...(无限循环)

此时我们知道我们看到的 0.1 并不是真正的 0.1,与此同时计算得到的结果也是无限不循环的。原来是因为计算机不能精确表示 0.1,0.2 这样的浮点数,计算时使用的是带有舍入误差的数,也就造成浮点数在进制转换中造成了精度丢失。

推荐使用 BigDecimal

既然浮点数精度会丢失,在实际业务中又经常需要精确到小数点后几位去计算,特别是还支付类的产品中通常都要精确到分,这时候我们就可以使用 Java 自带的 BigDecimal 类型,使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作,以下是 BigDecimal 使用的几个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 减法
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

// 比较大小
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.compareTo(b));

// 保留几位小数
BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN);
System.out.println(n);

// 除法并可以取余数
BigDecimal n = new BigDecimal("12.345");
BigDecimal m = new BigDecimal("0.12");
BigDecimal[] dr = n.divideAndRemainder(m);
System.out.println(dr[0]); // 102
System.out.println(dr[1]); // 0.105

使用 BigDecimal 的注意事项

《阿里巴巴 Java 开发手册》