金融行业是如何丢失1分钱的
问题:
不知道大家是否有遇到过这种情况,有时候明明两个准确的小数相加或者相减可以得到准确的值,但是不知道为什么会得到一个近似值,比如例1:
1 System.out.println("0.05 + 0.01: " + (0.05 + 0.01));
2 System.out.println("1.0 - 0.9: " + (1.0 - 0.9));
3 System.out.println("64.6 * 100: " + (64.6 * 100));
4 System.out.println("1.0 - 0.42: " + (1.0 - 0.42));
5 System.out.println("123.3 / 100: " + (123.3 / 100));
6 System.out.println("0.06 - 0.01: " + (0.06 - 0.01));
7
8 输出:
9
10 0.05 + 0.01: 0.060000000000000005
11 1.0 - 0.9: 0.09999999999999998
12 64.6 * 100: 6459.999999999999
13 1.0 - 0.42: 0.5800000000000001
14 123.3 / 100: 1.2329999999999999
15 0.06 - 0.01: 0.049999999999999996
上面都是浮点类型的数据计算就会出现这种情况,这种情况我们称为 “精度丢失” 。
如果用上面的方法进行计算的话呢,有可能我有1块钱,我买了0.9元的商品,再想买0.1元的商品就买不成功了。
原因:
这是怎么回事呢?
是因为计算机采用的是二进制进行运算,运算前需要将十进制的数值转换成二进制。但是有些数值是无法转成二进制的,所以只能用近似值的二进制进行运算,所以得到的结果也只能是近似值,这就导致了精度丢失。比如说 0.05 就是无法转成精确的二进制的:
1 0.05 * 2 = 0.1 取 0 2 0.1 * 2 = 0.2 取 0 3 0.2 * 2 = 0.4 取 0 4 0.4 * 2 = 0.8 取 0 5 0.8 * 2 = 1.6 取 1 6 0.6 * 2 = 1.2 取 1 7 0.2 * 2 = 0.4 取 0 8 0.4 * 2 = 0.8 取 0 9 0.8 * 2 = 1.6 取 1 10 0.6 * 2 = 1.2 取 1 11 0.2 * 2 = 0.4 取 0 12 0.4 * 2 = 0.8 取 0 13 0.8 * 2 = 1.6 取 1 14 0.6 * 2 = 1.2 取 1 15 0.2 * 2 = 0.4 取 0 16 0.4 * 2 = 0.8 取 0 17 0.8 * 2 = 1.6 取 1 18 0.6 * 2 = 1.2 取 1
从上面可以看出0.05的二进制只能是0.000011001100110011……无限循环 0011 下去,所以一直取不到精确的二进制,所以计算机只能取一个近似值进行运算。
解决办法:
在JAVA中有BigDecimal类,我们可以用它来定义小数的变量进行计算。首先我们介绍一下BigDecimal这个类:
常用构造函数:
BigDecimal(int val) -- 创建一个参数为整数的对象 BigDecimal(double val) -- 创建一个参数为双精度的对象 BigDecimal(long val) -- 创建一个参数为长整型数值的对象 BigDecimal(String val) -- 创建一个参数为字符串的对象
常用方法:
public BigDecimal add(BigDecimal augend) -- BigDecimal对象中的值相加,并返回这个对象
public BigDecimal subtract(BigDecimal subtrahend) -- BigDecimal对象中的值相减,并返回这个对象
public BigDecimal multiply(BigDecimal multiplicand) -- BigDecimal对象中的值相乘,并返回这个对象
public BigDecimal divide(BigDecimal divisor) -- BigDecimal对象中的值相除,并返回这个对象
public BigDecimal remainder(BigDecimal divisor) -- BigDecimal对象中的值取余,并返回这个对象
public BigDecimal[] divideAndRemainder(BigDecimal divisor) -- BigDecimal对象中的值相除并取余,并返回这个对象数组,[0]是商,[1]是余数
public static BigDecimal valueOf(double val) -- 参数为double类型的值转换成BigDecimal类型的对象,并返回转换后的对象
public static BigDecimal valueOf(long val) -- 参数为long类型的值转换成BigDecimal类型的对象,并返回转换后的对象
public long longValue() -- 参数为BigDecimal类型的值转换成long类型的值,并返回转换后的值
public int intValue() -- 参数为BigDecimal类型的值转换成int类型的值,并返回转换后的值
public double doubleValue() -- 参数为BigDecimal类型的值转换成double类型的值,并返回转换后的值
因为BigDecimal 的构造函数有很多,我们先用 BigDecimal(double val) 这个来初始化对象。例2:
1 double d1 = 0.01;
2 double d2 = 0.05;
3 BigDecimal b9 = new BigDecimal(d1);
4 BigDecimal b10 = new BigDecimal(d2);
5 System.out.println("d1: " + d1);
6 System.out.println("d2: " + d2);
7 System.out.println("b9: " + b9);
8 System.out.println("b10: " + b10);
9 System.out.println("b9.add(b10): " + b9.add(b10));
10
11 输出结果:
12
13 d1: 0.01
14 d2: 0.05
15 b9: 0.01000000000000000020816681711721685132943093776702880859375
16 b10: 0.05000000000000000277555756156289135105907917022705078125
17 b9.add(b10): 0.06000000000000000298372437868010820238851010799407958984375
发现用BigDecimal (double val) 实例化出来的对象还是近似值,我们看看构造函数的源码注释:
1 <b>Notes:</b>
2 <ol>
3 <li>
4 The results of this constructor can be somewhat unpredictable.
5 One might assume that writing {@code new BigDecimal(0.1)} in
6 Java creates a {@code BigDecimal} which is exactly equal to
7 0.1 (an unscaled value of 1, with a scale of 1), but it is
8 actually equal to
9 0.1000000000000000055511151231257827021181583404541015625.
10 This is because 0.1 cannot be represented exactly as a
11 {@code double} (or, for that matter, as a binary fraction of
12 any finite length). Thus, the value that is being passed
13 <i>in</i> to the constructor is not exactly equal to 0.1,
14 appearances notwithstanding.
15
16 <li>
17 The {@code String} constructor, on the other hand, is
18 perfectly predictable: writing {@code new BigDecimal("0.1")}
19 creates a {@code BigDecimal} which is <i>exactly</i> equal to
20 0.1, as one would expect. Therefore, it is generally
21 recommended that the {@linkplain #BigDecimal(String)
22 <tt>String</tt> constructor} be used in preference to this one.
23
24 <li>
注释的第一部分主要是说了因为0.1无法准确的用double形式表示并且也不能用有限长度的二进制表示,所以只能取近似值,因此这个0.1值浮点类型只能用0.1000000000000000055511151231257827021181583404541015625来表示;
注释的第二部分说的是因为BigDecimal(String val)构造函数能如人们所料的精确的表示0.1的值,所以通常更推荐使用该字符串为参数的构造函数来初始化对象。
所以接下来我们用BigDecimal(String val)来初始化对象并运算,例3:
1 String s1 = "0.01";
2 String s2 = "0.05";
3 BigDecimal b13 = new BigDecimal(s1);
4 BigDecimal b14 = new BigDecimal(s2);
5 System.out.println("s1: " + s1);
6 System.out.println("s2: " + s2);
7 System.out.println("b13: " + b13);
8 System.out.println("b14: " + b14);
9 System.out.println("b13.add(b14): " + b13.add(b14));
10
11 输出结果:
12
13 s1: 0.01
14 s2: 0.05
15 b13: 0.01
16 b14: 0.05
17 b13.add(b14): 0.06
由上面的结果我们可以看到,用BigDecimal(String val)是可以解决丢失精度的问题的。所以为了防止丢失进度问题,我们应该使用BigDecimal(String val)的构造函数来实例化对象。
但是可能有人会说,可能其他系统提供的类型不是String类型的怎么办?那我们就用上面列举的常用方法 valueOf(val) 进行转换,初始值是double类型的,例4:
1 double d1 = 0.01;
2 double d2 = 0.05;
3 BigDecimal b9 = BigDecimal.valueOf(d1);
4 BigDecimal b10 = BigDecimal.valueOf(d2);
5 System.out.println("d1: " + d1);
6 System.out.println("d2: " + d2);
7 System.out.println("b9: " + b9);
8 System.out.println("b10: " + b10);
9 System.out.println("b9.add(b10): " + b9.add(b10));
10
11 输出结果:
12
13 d1: 0.01
14 d2: 0.05
15 b9: 0.01
16 b10: 0.05
17 b9.add(b10): 0.06
其实例4也就是由例2改了红色标记的部分,但是得到的结果就是精确到值。
总结:
- 我们遇到浮点型变量则使用BigDecimal(String val)进行初始化之后再运算;
- 如果原值不是字符串则使用public static BigDecimal valueOf(val) 进行转换得到BigDecimal 类型的对象;
欢迎大家指教和点评!