金融行业是如何丢失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改了红色标记的部分,但是得到的结果就是精确到值。

总结:

  1. 我们遇到浮点型变量则使用BigDecimal(String val)进行初始化之后再运算;
  2. 如果原值不是字符串则使用public static BigDecimal valueOf(val) 进行转换得到BigDecimal 类型的对象;

欢迎大家指教和点评!

 

 

 

posted @ 2020-03-23 16:31  dengyongchang  阅读(209)  评论(0编辑  收藏  举报