关于Java中用Double型运算时精度丢失的问题

注:转自 https://blog.csdn.net/bleach_kids/article/details/49129943

在使用Java,double 进行运算时,经常出现精度丢失的问题,总是在一个正确的结果左右偏0.0000**1。 特别在实际项目中,通过一个公式校验该值是否大于0,如果大于0我们会做一件事情,小于0我们又处理其他事情。 这样的情况通过double计算出来的结果去和0比较大小,尤其是有小数点的时候,经常会因为精度丢失而导致程序处理流程出错。

BigDecimal
在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用 java.math.BigDecimal。BigDecimal一共有4个够造方法,我们不关心用BigInteger来够造的那两个,那么还有两个, 它们是:
BigDecimal(double val) 
          Translates a double into a BigDecimal. 
BigDecimal(String val) 
          Translates the String repre sentation of a BigDecimal into a BigDecimal.
上面的API简要描述相当的明确,而且通常情况下,上面的那一个使用起来要方便一些。我们可能想都不想就用上了,会有什么问题呢?等到出了问题的时候,才发现上面哪个够造方法的详细说明中有这么一段:
Note: the results of this constructor can be somewhat unpredictable. One might assume that new BigDecimal(.1) is exactly equal to .1, but it is actually equal to .1000000000000000055511151231257827021181583404541015625. This is so because .1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the long value that is being passed in to the constructor is not exactly equal to .1, appearances nonwithstanding. 
The (String) constructor, on the other hand, is perfectly predictable: new BigDecimal(".1") is exactly equal to .1, as one would expect. Therefore, it is generally recommended that the (String) constructor be used in preference to this one.
原来我们如果需要精确计算,非要用String来够造BigDecimal不可!在《Effective Java》一书中的例子是用String来够造BigDecimal的,但是书上却没有强调这一点,这也许是一个小小的失误吧。
 
解决方案
现在我们已经可以解决这个问题了,原则是使用BigDecimal并且一定要用String来够造。
但是想像一下吧,如果我们要做一个加法运算,需要先将两个浮点数转为String,然后够造成BigDecimal,在其中一个上调用add方法,传入另 一个作为参数,然后把运算的结果(BigDecimal)再转换为浮点数。你能够忍受这么烦琐的过程吗?下面我们提供一个工具类Arith来简化操作。它 提供以下静态方法,包括加减乘除和四舍五入:
public static double add(double v1,double v2)
public static double sub(double v1,double v2)
public static double mul(double v1,double v2)
public static double div(double v1,double v2)
public static double div(double v1,double v2,int scale)
public static double round(double v,int scale)

 

所以一般对double类型进行运算时,做好对结果进行处理,然后拿这个值去做其他事情。 

使用如下:

 1  /**   
 2      * 对double数据进行取精度.   
 3      * @param value  double数据.   
 4      * @param scale  精度位数(保留的小数位数).   
 5      * @param roundingMode  精度取值方式.   
 6      * @return 精度计算后的数据.   
 7      */   
 8     public static double round(double value, int scale,  
 9              int roundingMode) {    
10         BigDecimal bd = new BigDecimal(value);    
11         bd = bd.setScale(scale, roundingMode);    
12         double d = bd.doubleValue();    
13         bd = null;    
14         return d;    
15     }    
16 
17 
18      /** 
19      * double 相加 
20      * @param d1 
21      * @param d2 
22      * @return 
23      */ 
24     public double sum(double d1,double d2){ 
25         BigDecimal bd1 = new BigDecimal(Double.toString(d1)); 
26         BigDecimal bd2 = new BigDecimal(Double.toString(d2)); 
27         return bd1.add(bd2).doubleValue(); 
28     } 
29 
30 
31     /** 
32      * double 相减 
33      * @param d1 
34      * @param d2 
35      * @return 
36      */ 
37     public double sub(double d1,double d2){ 
38         BigDecimal bd1 = new BigDecimal(Double.toString(d1)); 
39         BigDecimal bd2 = new BigDecimal(Double.toString(d2)); 
40         return bd1.subtract(bd2).doubleValue(); 
41     } 
42 
43     /** 
44      * double 乘法 
45      * @param d1 
46      * @param d2 
47      * @return 
48      */ 
49     public double mul(double d1,double d2){ 
50         BigDecimal bd1 = new BigDecimal(Double.toString(d1)); 
51         BigDecimal bd2 = new BigDecimal(Double.toString(d2)); 
52         return bd1.multiply(bd2).doubleValue(); 
53     } 
54 
55 
56     /** 
57      * double 除法 
58      * @param d1 
59      * @param d2 
60      * @param scale 四舍五入 小数点位数 
61      * @return 
62      */ 
63     public double div(double d1,double d2,int scale){ 
64         //  当然在此之前,你要判断分母是否为0,    
65         //  为0你可以根据实际需求做相应的处理 
66 
67         BigDecimal bd1 = new BigDecimal(Double.toString(d1)); 
68         BigDecimal bd2 = new BigDecimal(Double.toString(d2)); 
69         return bd1.divide  
70                (bd2,scale,BigDecimal.ROUND_HALF_UP).doubleValue(); 
71     } 

这样,计算double类型的数据计算问题就可以处理了。 
另外补充一下 JavaScript 四舍五入的方法: 
小数点问题  

1 Math.round(totalAmount*100)/100 (保留 2 位) 
2 
3 function formatFloat(src, pos) 
4 { 
5   return Math.round(src*Math.pow(10, pos))/Math.pow(10, pos); 
6 }

 

四舍五入是我们小学的数学问题,这个问题对于我们程序猿来说就类似于1到10的加减乘除那么简单了。在讲解之间我们先看如下一个经典的案例:

1 public static void main(String[] args) {  
2         System.out.println("12.5的四舍五入值:" + Math.round(12.5));  
3         System.out.println("-12.5的四舍五入值:" + Math.round(-12.5));  
4     }  

Output:
12.5的四舍五入值:13
-12.5的四舍五入值:-12

这是四舍五入的经典案例,也是我们参加校招时候经常会遇到的(貌似我参加笔试的时候遇到过好多次)。从这儿结果中我们发现这两个绝对值相同的数字,为何近似值会不同呢?其实这与Math.round采用的四舍五入规则来决定。

      四舍五入其实在金融方面运用的非常多,尤其是银行的利息。我们都知道银行的盈利渠道主要是利息差,它从储户手里收集资金,然后放贷出去,期间产生的利息差就是银行所获得的利润。如果我们采用平常四舍五入的规则话,这里采用每10笔存款利息计算作为模型,如下:

      四舍:0.000、0.001、0.002、0.003、0.004。这些舍的都是银行赚的钱。

      五入:0.005、0.006、0.007、0.008、0.009。这些入的都是银行亏的钱,分别为:0.005、0.004、.003、0.002、0.001。

      所以对于银行来说它的盈利应该是0.000 + 0.001 + 0.002 + 0.003 + 0.004 - 0.005 - 0.004 - 0.003 - 0.002 - 0.001 = -0.005。从结果中可以看出每10笔的利息银行可能就会损失0.005元,千万别小看这个数字,这对于银行来说就是一笔非常大的损失。面对这个问题就产生了如下的银行家涉入法了。该算法是由美国银行家提出了,主要用于修正采用上面四舍五入规则而产生的误差。如下:

      舍去位的数值小于5时,直接舍去。

      舍去位的数值大于5时,进位后舍去。

      当舍去位的数值等于5时,若5后面还有其他非0数值,则进位后舍去,若5后面是0时,则根据5前一位数的奇偶性来判断,奇数进位,偶数舍去。

      对于上面的规则我们举例说明

         11.556 = 11.56 ------六入

         11.554 = 11.55 -----四舍

         11.5551 = 11.56 -----五后有数进位

         11.545 = 11.54 -----五后无数,若前位为偶数应舍去

         11.555 = 11.56 -----五后无数,若前位为奇数应进位

      下面实例是使用银行家舍入法:

1 public static void main(String[] args) {  
2         BigDecimal d = new BigDecimal(100000);      //存款  
3         BigDecimal r = new BigDecimal(0.001875*3);   //利息  
4         BigDecimal i = d.multiply(r).setScale(2,RoundingMode.HALF_EVEN);     //使用银行家算法   
5           
6         System.out.println("季利息是:"+i);  
7         }  

Output:  

季利息是:562.50

 

在上面简单地介绍了银行家舍入法,目前java支持7中舍入法:

         1、 ROUND_UP:远离零方向舍入。向绝对值最大的方向舍入,只要舍弃位非0即进位。

         2、 ROUND_DOWN:趋向零方向舍入。向绝对值最小的方向输入,所有的位都要舍弃,不存在进位情况。

         3、 ROUND_CEILING:向正无穷方向舍入。向正最大方向靠拢。若是正数,舍入行为类似于ROUND_UP,若为负数,舍入行为类似于ROUND_DOWN。Math.round()方法就是使用的此模式。

         4、 ROUND_FLOOR:向负无穷方向舍入。向负无穷方向靠拢。若是正数,舍入行为类似于ROUND_DOWN;若为负数,舍入行为类似于ROUND_UP。

         5、 HALF_UP:最近数字舍入(5进)。这是我们最经典的四舍五入。

         6、 HALF_DOWN:最近数字舍入(5舍)。在这里5是要舍弃的。

         7、 HAIL_EVEN:银行家舍入法。

      提到四舍五入那么保留位就必不可少了,在java运算中我们可以使用多种方式来实现保留位。

保留位

      方法一:四舍五入

1 double   f   =   111231.5585;  
2 BigDecimal   b   =   new   BigDecimal(f);  
3 double   f1   =   b.setScale(2,   RoundingMode.HALF_UP).doubleValue();  

在这里使用BigDecimal ,并且采用setScale方法来设置精确度,同时使用RoundingMode.HALF_UP表示使用最近数字舍入法则来近似计算。在这里我们可以看出BigDecimal和四舍五入是绝妙的搭配。

      方式二:

1 java.text.DecimalFormat   df   =new   java.text.DecimalFormat(”#.00″);  
2 df.format(你要格式化的数字);  

例:new java.text.DecimalFormat(”#.00″).format(3.1415926)

      #.00 表示两位小数 #.0000四位小数 以此类推…

      方式三:

1 double d = 3.1415926;  
2   
3 String result = String .format(”%.2f”);  

%.2f %. 表示 小数点前任意位数   2 表示两位小数 格式后的结果为f 表示浮点型。  

方式四:

      此外如果使用struts标签做输出的话,有个format属性,设置为format="0.00"就是保留两位小数

      例如:

1 <bean:write name="entity" property="dkhAFSumPl"  format="0.00" />  
2   
3 或者  
4   
5 <fmt:formatNumber type="number" value="${10000.22/100}" maxFractionDigits="0"/>  
6   
7 maxFractionDigits表示保留的位数  

BigDecimal.setScale 处理java小数点

BigDecimal.setScale()方法用于格式化小数点
setScale(1)表示保留一位小数,默认用四舍五入方式 
setScale(1,BigDecimal.ROUND_DOWN)直接删除多余的小数位,如2.35会变成2.3 
setScale(1,BigDecimal.ROUND_UP)进位处理,2.35变成2.4 
setScale(1,BigDecimal.ROUND_HALF_UP)四舍五入,2.35变成2.4
setScaler(1,BigDecimal.ROUND_HALF_DOWN)四舍五入,2.35变成2.3,如果是5则向下舍
 
注释:
1:
scale指的是你小数点后的位数。比如123.456则score就是3.
score()就是BigDecimal类中的方法啊。
比如:BigDecimal b = new BigDecimal("123.456");
b.scale(),返回的就是3.
2:
roundingMode是小数的保留模式。它们都是BigDecimal中的常量字段,有很多种。
比如:BigDecimal.ROUND_HALF_UP表示的就是4舍5入。
3:
pubilc BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
的意思是说:我用一个BigDecimal对象除以divisor后的结果,并且要求这个结果保留有scale个小数位,roundingMode表示的就是保留模式是什么,是四舍五入啊还是其它的,你可以自己选!

 

4:对于一般add、subtract、multiply方法的小数位格式化如下:

BigDecimal mData = new BigDecimal("9.655").setScale(2, BigDecimal.ROUND_HALF_UP);
        System.out.println("mData=" + mData);
 
----结果:----- mData=9.66

 

posted @ 2018-07-02 17:25  空涅  阅读(4071)  评论(0编辑  收藏  举报