zoukankan      html  css  js  c++  java
  • BigDecimal setScale()设置无效 scale()取得的值不是setScale()设置的值

    最近查看rebate数据时,发现一个bug,主要现象是,当扣款支付宝的账号款项时,返回的是数字的金额为元,而数据库把金额存储为分,这中间要做元与分的转化,这个转化规则很简单,就是*100的,所以一开始代码很简单,如下。

     

    1. Float f =  Float.valueOf(s);  
    2. f =f*100;  
    3. Long result = f.longValue();  
    Float f =  Float.valueOf(s);
    f =f*100;
    Long result = f.longValue();

    s=”9.86”时,杯具出现了,result的结果为985而不是986float的精度损失导致float(985.99994)转化为整形时,丢掉小数部分成为985,简单的方法,我们可以提高精度使用双精度的double类型,提高精度,比如

    1. Double d =  Double.valueOf(s);  
    2. d = d*100;  
    3. Long result = d.longValue();  
    Double d =  Double.valueOf(s);
    d = d*100;
    Long result = d.longValue();

    当s=”9.86”时,确实能够得到正确结果,但是当s=”1219.86”时,这时候由于精度问题导致最终的result为121985为不是121986。当时以为使用double解决的问题,其实隐藏更隐蔽的bug。 针对这样的问题,如果使用C/C++语言,那么通用解决方案可以这样。

    1. Double d =  Double.valueOf(s);  
    2. d = d*100+0.5;// 注意这里,我们使用的是+0.5的形式。  
    3. Long result = d.longValue();  
    Double d =  Double.valueOf(s);
    d = d*100+0.5;// 注意这里,我们使用的是+0.5的形式。
    Long result = d.longValue();

    但是,我们使用的java语言,java应该有更优雅的解决方案,那就是BigDecimal。

    使用BigDecimal的解决方案成这个样子

    1. Double dd= Double.valueOf(s);  
    2. BigDecimal bigD = new BigDecimal(dd);  
    3. bigD = bigD.multiply(new BigDecimal(100));  
    4. Long result = bigD.longValue();  
    Double dd= Double.valueOf(s);
    BigDecimal bigD = new BigDecimal(dd);
    bigD = bigD.multiply(new BigDecimal(100));
    Long result = bigD.longValue();

    狂晕,输出结果是985为不是986,打印bigD

    1. System.out.println(bigD.toString());  
    System.out.println(bigD.toString());

    输出如下

    1. 985.9999999999999431565811391919851303100585937500  
    985.9999999999999431565811391919851303100585937500

    不会再加上一个BigDecimal(0.5)吧。我相信在使用过BigDecimal过程中,肯定有那里不对的地方,multiply方法中可以传入精度,那就构造MathContext对象,修改如下。

    1. Double dd= Double.valueOf(s);  
    2. BigDecimal bigD = new BigDecimal(dd);  
    3. MathContextmc = new MathContext(4,RoundingMode.HALF_UP);  
    4. //4表示取四位有效数字,RoundingMode.HALF_UP表示四舍五入  
    5. bigD= bigD.multiply(new BigDecimal(100),mc);  
    6. Long result = bigD.longValue();  
    Double dd= Double.valueOf(s);
    BigDecimal bigD = new BigDecimal(dd);
    MathContextmc = new MathContext(4,RoundingMode.HALF_UP);
    //4表示取四位有效数字,RoundingMode.HALF_UP表示四舍五入
    bigD= bigD.multiply(new BigDecimal(100),mc);
    Long result = bigD.longValue();

    最后结果输出为986,貌似已经找到完成解决方案,其实不然,注意到MathContext中的4了嘛?这是因为我们保留4位有效数字,假如我们输入的数字是大于4的,比如1219.86,最终输出结果是122000,这是因为1219.86保留4位有效数字时,第四位的9四舍五入,除去精确位补零,所以最终结果成了122000。问题就成了,我们必须知道元变分后的最终有效位数,”9.86”,有效位数是4,”19.86”有效位数是5,把字符串s的长度传过去就可以了,那么代码如下

    1. Double dd =Double.valueOf(s);  
    2. BigDecimalbigD = new BigDecimal(dd);  
    3. MathContextmc = new MathContext(s.length(),RoundingMode.HALF_UP);  
    4. //4表示取四位有效数字,RoundingMode.HALF_UP表示四舍五入  
    5. bigD= bigD.multiply(new BigDecimal(100),mc);  
    6. Long result = bigD.longValue();  
    Double dd =Double.valueOf(s);
    BigDecimalbigD = new BigDecimal(dd);
    MathContextmc = new MathContext(s.length(),RoundingMode.HALF_UP);
    //4表示取四位有效数字,RoundingMode.HALF_UP表示四舍五入
    bigD= bigD.multiply(new BigDecimal(100),mc);
    Long result = bigD.longValue();

    至此,已经可以得到一个正确的元转分的代码,但是这里的s.length()终归不让人感觉舒服,接下来,我们探索BigDecimal原理,尝试用更优雅的方法解决这个问题。 BigDecimal,不可变的、任意精度的有符号十进制数。BigDecimal 由任意精度的整数非标度值 和 32 位的整数标度(scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负 scale 次幂。因此,BigDecimal 表示的数值是 (unscaledValue × 10-scale)。我们知道BigDecimal有三个主要的构造函数

    1

    public  BigDecimal(double val)

    将double表示形式转换为BigDecimal

    2

    public  BigDecimal(int val)

    将int表示形式转换为BigDecimal

    3

    public  BigDecimal(String val)

    将字符串表示形式转换为BigDecimal

    通过这三个构造函数,可以把double类型,int类型,String类型构造为BigDecimal对象,在BigDecimal对象内通过BigIntegerintVal存储传递对象数字部分,通过int scale;记录小数点位数,通过int precision;记录有效位数(默认为0)。 BigDecimal的加减乘除就成了BigInteger与BigInteger之间的加减乘除,浮点数的计算也转化为整形的计算,可以大大提供性能,并且通过BigInteger可以保存大数字,从而实现真正大十进制的计算,在整个计算过程中,还涉及scale的判断和precision判断从而确定最终输出结果。 我们先看一个例子

    1. BigDecimal d1 = new BigDecimal(0.6);  
    2. BigDecimal d2 = new BigDecimal(0.4);  
    3. BigDecimal d3 = d1.divide(d2);  
    4. System.out.println(d3);  
    BigDecimal d1 = new BigDecimal(0.6);
    BigDecimal d2 = new BigDecimal(0.4);
    BigDecimal d3 = d1.divide(d2);
    System.out.println(d3);

    大家猜一下,以上输出结果是?再接着看下面的代码

    1. BigDecimal d1 = new BigDecimal(“0.6”);  
    2. BigDecimal d2 = new BigDecimal(“0.4”);  
    3. BigDecimal d3 = d1.divide(d2);  
    4. System.out.println(d3);  
    BigDecimal d1 = new BigDecimal(“0.6”);
    BigDecimal d2 = new BigDecimal(“0.4”);
    BigDecimal d3 = d1.divide(d2);
    System.out.println(d3);

    看似相似的代码,其结果完全不同,第一个例子中,抛出异常。第二个例子中,输出打印结果为1.5。造成这种差异的主要原因是第一个例子中的创建BigDecimal时,0.60.4是浮动类型的,浮点型放入BigDecimal内,其存储值为

    1. 0.59999999999999997779553950749686919152736663818359375  
    2. 0.40000000000000002220446049250313080847263336181640625  
    0.59999999999999997779553950749686919152736663818359375
    0.40000000000000002220446049250313080847263336181640625

    这两个浮点数相除时,由于除不尽,而又没有设置精度和保留小数点位数,导致抛出异常。而第二个例子中0.60.4是字符串类型,由于BigDecimal存储特性,通过BigInteger记录BigDecimal的值,所以,0.6和0.4可以非常正确的记录为

    1. 0.6  
    2. 0.4  
    0.6
    0.4

    两者相除得出1.5来。 对于第一个例子,如果我们想得到正确结果,可以这样来

    1. BigDecimal d1 = new BigDecimal(0.6);  
    2. BigDecimal d2 = new BigDecimal(0.4);  
    3. BigDecimal d3 = d1.divide(d2, 1, BigDecimal.ROUND_HALF_UP);  
    BigDecimal d1 = new BigDecimal(0.6);
    BigDecimal d2 = new BigDecimal(0.4);
    BigDecimal d3 = d1.divide(d2, 1, BigDecimal.ROUND_HALF_UP);

    现在看我们留下的那个问题,使用更优雅的方式解决元转化为分的方式,上一个问题中,我们通过传递s.length()从而获得精度,如果之前的s是double类型的,那边这样的方式就会有问题,通过上面的例子,我们可以调整为一下的通用方式

    1. Double dd= Double.valueOf(s);  
    2. BigDecimal bigD = new BigDecimal(dd);  
    3. bigD = bigD.multiply(newBigDecimal(100)). divide(1, 1, BigDecimal.ROUND_HALF_UP);  
    4. Long result = bigD.longValue();  
    Double dd= Double.valueOf(s);
    BigDecimal bigD = new BigDecimal(dd);
    bigD = bigD.multiply(newBigDecimal(100)). divide(1, 1, BigDecimal.ROUND_HALF_UP);
    Long result = bigD.longValue();

    我们通过/1,然后设置保留小数点方式,以及设置数字保留模式,从而得到两个数乘积的小数部分。还有以下模式

    枚举常量摘要  ROUND_CEILING             向正无限大方向舍入的舍入模式。 ROUND_DOWN             向零方向舍入的舍入模式。 ROUND_FLOOR             向负无限大方向舍入的舍入模式。 ROUND_HALF_DOWN             向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向下舍入。 ROUND_HALF_EVEN             向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。 ROUND_HALF_UP             向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向上舍入。 ROUND_UNNECESSARY             用于断言请求的操作具有精确结果的舍入模式,因此不需要舍入。(默认模式) ROUND_UP             远离零方向舍入的舍入模式。

    总结: 1:尽量避免传递double类型,有可能话,尽量使用intString类型。 2:做乘除计算时,一定要设置精度和保留小数点位数。 3BigDecimal计算时,单独放到try catch内。

    public static BigDecimal double2BigDecimal(double d, int scale){

      BigDecimal db = new BigDecimal();

      db.setScale(scale, BigDecimal.ROUND_HALF_UP);

      return db;

    }

    以上方法返回的BigDecimal.scale()方法并不是我指定的值,于是修改成以下方法解决:

    public static BigDecimal double2BigDecimal(double d, int scale){

      BigDecimal db = new BigDecimal();

      return db.divide(new BigDecimal(1), scale, BigDecimal.ROUND_HALF_UP);  ;

    }

     

     

    参考资料 IEEE 754简介: http://baike.baidu.com/view/1698149.htm IEEE 754官方协议:http://grouper.ieee.org/groups/754/ BigDecimal函数列表:http://hi.baidu.com/logan9999/item/eeaea014677323fd9c778abd 浮点数与IEEE 754: http://www.cnblogs.com/kingwolfofsky/archive/2011/07/21/2112299.html MathContext:http://doc.java.sun.com/DocWeb/api/all/java.math.MathContext BigDecimal:http://doc.java.sun.com/DocWeb/api/all/java.math.BigDecimal

  • 相关阅读:
    Codeforces Round #518 Div. 1没翻车记
    BZOJ4310 跳蚤(后缀数组+二分答案)
    后缀数组备忘
    洛谷 P3573 [POI2014]RAJ-Rally 解题报告
    洛谷 P1503 鬼子进村 解题报告
    洛谷 P2375 [NOI2014]动物园 解题报告
    洛谷 P2797 Facer的魔法 解题报告
    【模板】三分法
    那些神奇的DP建模
    洛谷 P1136 迎接仪式 解题报告
  • 原文地址:https://www.cnblogs.com/firstdream/p/8796270.html
Copyright © 2011-2022 走看看