题目
在实际开发中为什么不能用浮点类型(float、double)来存金钱的数据,浮点数不精确的本质是什么
答案
@[toc]
---
浮点数不精确的经典例子是 0.1 + 0.2 != 0.3
这个问题的症结在于:**在现今的计算机中,数字的最终存储(表示)格式是二进制,是整数乘以 2 的幂。所以一切分母不是 2 的幂的有理数(例如 0.1,即 1/10)都是无法被计算机精确表示的**。
下面来详细分析:
## 十进制小数与二进制的转换
整数十进制转二进制大伙都知道(除 2 取余法),我们来看看小数是怎么转二进制的
小数部分的转换不同于整数部分,它采用的是**乘 2 取整法**:将十进制中的小数部分乘以 2,得到一个数,将这个数的整数位作为二进制的一位,然后继续取其小数部分乘以 2 作为下一位,直到不存在小数为止
举个例子:3.14(十进制)
整数部分:
- 3(十进制)-> `0011`(二进制)
小数部分:
- 0.14 x 2 = 0.28(将十进制 `3` 中的小数部分 `0.14` 乘以 2,得到一个数 `0.28`,将这个数的整数位 `0` 作为二进制的一位,然后继续取其小数部分乘以 2 作为下一位,直到不存在小数为止),此时小数部分的二进制表示是 `0xxx xxxx`
- 0.28 x 2 = 0.56(将十进制 `0.28` 中的小数部分 `0.28` 乘以 2,得到一个数 `0.56`,将这个数的整数位 `0` 作为二进制的一位,然后继续取其小数部分乘以 2 作为下一位,直到不存在小数为止),此时小数部分的二进制表示是 `00xx xxxx`
- 0.56 x 2 = 1.12(将十进制 `0.56` 中的小数部分 `0.56` 乘以 2,得到一个数 `1.12`,将这个数的整数位 `1` 作为二进制的一位,然后继续取其小数部分乘以 2 作为下一位,直到不存在小数为止),此时小数部分的二进制表示是 `001x xxxx`
- .......
问题就出现了~
**如果小数部分一直没法变成 0,那么小数部分的二进制表示将是无穷无尽的**。
由于计算机的资源是有限的,所以没办法用二进制精确的表示这些小数部分一直没法变成 0 的数,只能用【近似值】来表示(现在基本采用的都是 IEEE 754 标准,https://en.wikipedia.org/wiki/IEEE_754#Basic_and_interchange_formats),这不可避免地就会造成精度缺失
## Working with Floats in Programming
在平常写代码的时候,这个精度问题意味着我们需要使用舍入函数将浮点数四舍五入到我们需要的小数位,然后再显示它们。
还需要用【允许一定差距(allow some amount of tolerance)的比较】来替换浮点数之间的相等比较 `==`,这意味着:
不要使用:`if (x == y) { ... }`
而是使用:`if (abs(x - y) < myToleranceValue) { ... }`.
## BigDecimal
解决办法:
在程序中存储金额的数据类型用:`java.math.BigDecimal`,在数据库中存储金额的数据类型用:`decimal`。
SQL:
```sql
'amount' DECIMAL(10, 2) DEFAULT 0.00 NOT NULL COMMENT '金额'
```
> 注意默认值,一定写成 0.00,不要用默认的 NULL,否则在进行加减排序等操作时,会带来转换的麻烦。
Java 中的操作示例如下:
```java
public static void main(String[] args){
BigDecimal x = new BigDecimal("1.3");
BigDecimal y = new BigDecimal("2.5");
// 加法 --> 3.8
BigDecimal add = x.add(y);
System.out.println(add);
// 减法 --> -1.2
BigDecimal subtract = x.subtract(y);
System.out.println(subtract);
// 乘法 --> 3.25
BigDecimal multiply = x.multiply(y);
System.out.println(multiply);
// 除法 --> 0.5 ,RoundingMode.HALF_UP 四舍五入
BigDecimal divide = x.divide(y, RoundingMode.HALF_UP);
System.out.println(divide);
}
```
