Java BigDecimal详解
  C5bk9rm9TFUo 2023年11月02日 33 0


一、引言

        借用《Effactive Java》这本书中的话,float和double类型的主要设计目标是为了科学计算和工程计算。他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们没有提供完全精确的结果,所以不应该被用于要求精确结果的场合。但是,货币计算往往要求结果精确,这时候可以使用int、long或BigDecimal。本文主要讲述BigDecimal使用过程中的一些陷阱、建议和技巧。

二、不可变性

        BigDecimal是不可变类,每一个操作(加减乘除等)都会返回一个新的对象, 下面以加法操作为例。

 

BigDecimal a =          new          BigDecimal(          "1.22"          );         


          System.out.println(          "construct with a String value: "          + a);         


          BigDecimal b =          new          BigDecimal(          "2.22"          );         


          a.add(b);         


          System.out.println(          "a plus b is : "          + a);



        我们很容易会认为会输出:

         construct with a String value: 1.22

        a plus b is :3.44

        但实际上a plus b is : 1.22

        下面我们就来分析一下加法操作的源码

 


public          BigDecimal add(BigDecimal augend) {         


                    long          xs =          this          .intCompact;           //整型数字表示的BigDecimal,例a的intCompact值为122         


                    long          ys = augend.intCompact;          //同上         


                    BigInteger fst = (          this          .intCompact !=INFLATED) ?          null          :          this          .intVal;          //初始化BigInteger的值,intVal为BigDecimal的一个BigInteger类型的属性         


                    BigInteger snd =(augend.intCompact !=INFLATED) ?          null          : augend.intVal;         


                    int          rscale =          this          .scale;          //小数位数         


                    


                    long          sdiff = (          long          )rscale - augend.scale;          //小数位数之差         


                    if          (sdiff !=           0          ) {          //取小数位数多的为结果的小数位数         


                    if          (sdiff <           0          ) {         


                    int          raise =checkScale(-sdiff);         


                    rscale =augend.scale;         


                    if          (xs ==INFLATED ||(xs = longMultiplyPowerTen(xs,raise)) ==INFLATED)         


                    fst =bigMultiplyPowerTen(raise);         


                    }          else          {         


                    int          raise =augend.checkScale(sdiff);         


                    if          (ys ==INFLATED ||(ys =longMultiplyPowerTen(ys,raise)) ==INFLATED)         


                    snd = augend.bigMultiplyPowerTen(raise);         


                    }         


                    }         


                    if          (xs !=INFLATED && ys !=INFLATED) {         


                    long          sum = xs + ys;         


                    if          ( (((sum ^ xs) &(sum ^ ys))) >= 0L)          //判断有无溢出         


                    return          BigDecimal.valueOf(sum,rscale);          //返回使用BigDecimal的静态工厂方法得到的BigDecimal实例         


                    


                    }         


                    


                    if          (fst ==          null          )         


                    fst =BigInteger.valueOf(xs);          //BigInteger的静态工厂方法         


                    if          (snd ==          null          )         


                    snd =BigInteger.valueOf(ys);         


                    BigInteger sum =fst.add(snd);         


                    return          (fst.signum == snd.signum) ?          new          BigDecimal(sum,INFLATED, rscale,           0          ) :         


          new          BigDecimal(sum,compactValFor(sum),rscale,           0          );          //返回通过其他构造方法得到的BigDecimal对象         


          }



       因为BigInteger与BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以a.add(b)虽然做了加法操作,但是a并没有保存加操作后的值,正确的用法应该是a=a.add(b); 减乘除操作也是一样的返回一个新的BigDecimal对象。     

三、构造函数和valueOf方法

        首先看如下一段代码: 

 

// use constructor BigDecimal(double)         


          BigDecimal aDouble =          new          BigDecimal(          1.22          );         


          System.out.println(          "construct with a double value: "          + aDouble);         


                    


          // use constructor BigDecimal(String)         


          BigDecimal aString =           new          BigDecimal(          "1.22"          );         


          System.out.println(          "construct with a String value: "          + aString);         


                    


          // use constructor BigDecimal.valueOf(double)         


          BigDecimal aValue = BigDecimal.valueOf(          1.22          );         


          System.out.println(          "use valueOf method: "          + aValue);




        你认为输出结果会是什么呢?如果你认为第一个会输出1.22,那么恭喜你答错了,输出结果如下:

        construct with a double value: 1.2199999999999999733546474089962430298328399658203125

        construct with a String value: 1.22

        use valueOf method: 1.22

        为什么会这样呢?JavaDoc对于BigDecimal(double)有很详细的说明:

 

1、参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中new BigDecimal(0.1)所创建的BigDecimal的值正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。 

 2、另一方面,String 构造方法是完全可预知的:new BigDecimal("0.1") 将创建一个 BigDecimal,它的值正好等于期望的0.1。因此,比较而言,通常建议优先使用String构造方法。 

 3、当 double 必须用作BigDecimal的来源时,请注意,此构造方法提供了一个精确转换;它不提供与以下操作相同的结果:先使用Double.toString(double)方法将double转换为String,然后使用BigDecimal(String)构造方法。要获取该结果,使用static valueOf(double)方法。



           BigDecimal.valueOf(double) 使用由  Double.toString(double)方法提供的  double的标准化字符串表示形式( canonical string representation)  将  double  转换成  BigDecimal  。这也是比较推荐的一种方式。



          BigDecimal.valueOf(double)还有一个重载的方法  BigDecimal.valueOf(long),对于某些常用值(0到10)  BigDecimal在内部做了缓存,  如果传递的参数值范围为[0, 10], 这个方法直接返回缓存中相应的BigDecimal对象。



        Java源码如下:



/**         


                    * Translates a {@code long} value into a {@code BigDecimal}         


                    * with a scale of zero. This {@literal "static factory method"}         


                    * is provided in preference to a ({@code long}) constructor         


                    * because it allows for reuse of frequently used         


                    * {@code BigDecimal} values.         


                    *         


                    * @param val value of the {@code BigDecimal}.         


                    * @return a {@code BigDecimal} whose value is {@code val}.         


                    */         


                    public          static          BigDecimal valueOf(          long          val) {         


                    if          (val >=           0          && val < zeroThroughTen.length)         


                    return          zeroThroughTen[(          int          )val];         


                    else          if          (val != INFLATED)         


                    return          new          BigDecimal(          null          , val,           0          ,           0          );         


                    return          new          BigDecimal(INFLATED_BIGINT, val,           0          ,           0          );         


          }         


                    


          // Cache of common small BigDecimal values.         


          private          static          final          BigDecimal zeroThroughTen[] = {         


                    new          BigDecimal(BigInteger.ZERO,           0          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.ONE,           1          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.valueOf(          2          ),           2          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.valueOf(          3          ),           3          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.valueOf(          4          ),           4          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.valueOf(          5          ),           5          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.valueOf(          6          ),           6          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.valueOf(          7          ),           7          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.valueOf(          8          ),           8          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.valueOf(          9          ),           9          ,           0          ,           1          ),         


                    new          BigDecimal(BigInteger.TEN,           10          ,           0          ,           2          ),         


          };



    附上相应的测试代码:

 


BigDecimal a1 = BigDecimal.valueOf(          10          );         


          BigDecimal a2 = BigDecimal.valueOf(          10          );         


          System.out.println(a1 == a2);           // true         


                    


          BigDecimal a3 = BigDecimal.valueOf(          11          );         


          BigDecimal a4 = BigDecimal.valueOf(          11          );         


          System.out.println(a3 == a4);           // false



四、equals方法

       BigDecimal.equals方法是有问题的.仅当你确定比较的值有着相同的标度时才可使用. 因此,当你校验相等性时注意 - BigDecimal有一个标度,用于相等性比较. 而compareTo方法则会忽略这个标度(scale).

       BigDecimal的equals方法源码如下:

 

@Override         


          public          boolean          equals(Object x) {         


                    // 必须是BigDecimal实例         


                    if          (!(x           instanceof          BigDecimal))         


                    return          false          ;         


                    BigDecimal xDec = (BigDecimal) x;         


                    if          (x ==           this          )         


                    return          true          ;         


                    // 标度必须相同         


                    if          (scale != xDec.scale)         


                    return          false          ;         


                    long          s =           this          .intCompact;         


                    long          xs = xDec.intCompact;         


                    if          (s != INFLATED) {         


                    if          (xs == INFLATED)         


                    xs = compactValFor(xDec.intVal);         


                    return          xs == s;         


                    }           else          if          (xs != INFLATED)         


                    return          xs == compactValFor(          this          .intVal);         


                    


                    return          this          .inflated().equals(xDec.inflated());         


          }



        参见以下测试代码:

 

// 打印false         


          System.out.println(          new          BigDecimal(          "0.0"          ).equals(          new          BigDecimal(          "0.00"          )));         


                    


          // 打印false          


          System.out.println(          new          BigDecimal(          "0.0"          ).hashCode() == (          new          BigDecimal(          "0.00"          )).hashCode());         


                    


          // 打印0          


          System.out.println(          new          BigDecimal(          "0.0"          ).compareTo(          new          BigDecimal(          "0.00"          )));



五、对除法使用标度



BigDecimal对象的精度没有限制。如果结果不能终止,divide方法将会抛出ArithmeticException, 如1 / 3 = 0.33333...。所以强烈推荐使用重载方法divide(BigDecimal d, int scale, int roundMode)指定标度和舍入模式来避免以上异常。



      参见以下测试代码:

 


//java.lang.ArithmeticException: Non-terminating decimal expansion;         


          //no exact representable decimal result.         


          try          {         


                    BigDecimal.valueOf(          1          ).divide(BigDecimal.valueOf(          3          ));         


          }           catch          (ArithmeticException ex) {         


                    System.out.println(ex.getMessage());         


          }         


          // always use a scale and the rounding mode of your choice         


          // 0.33         


          System.out.println(BigDecimal.valueOf(          1          ).divide(BigDecimal.valueOf(          3          ),           2          , BigDecimal.ROUND_HALF_UP));



六、总结

(1)商业计算使用BigDecimal。

        (2)使用参数类型为String的构造函数,将double转换成BigDecimal时用BigDecimal.valueOf(double),做除法运算时使用重载的方法divide(BigDecimal d, int scale, int roundMode)。

        (3)BigDecimal是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。

        (4)尽量使用compareTo方法比较两个BigDecimal对象的大小。

七、参考资料

         《Effective Java》

         http://www.stichlberger.com/software/java-bigdecimal-gotchas/

         http://stackoverflow.com/questions/7186204/bigdecimal-to-use-new-or-valueof

         http://www.javaworld.com/article/2073176/caution--double-to-bigdecimal-in-java.html

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  xaeiTka4h8LY   2024年05月17日   51   0   0 数据库JavaSQL
  2iBE5Ikkruz5   2023年12月12日   92   0   0 JavaJavaredisredis
C5bk9rm9TFUo