欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

java常用类之BigDecimal

程序员文章站 2022-05-18 20:23:25
BigDecimal 小数计算丢失精度问题 在计算机中,所有文件都是以二进制存储的,数字运算也是使用二进制进行计算的,因为计算机中不存在小数点,所以我们通常说的浮点数如 、`double`都是计算机使用二进制模拟出来的,但我们在计算机中运行以下代码获得的结果并不是正确的。 为什么呢? 我们先来看十进 ......

bigdecimal

小数计算丢失精度问题

在计算机中,所有文件都是以二进制存储的,数字运算也是使用二进制进行计算的,因为计算机中不存在小数点,所以我们通常说的浮点数如floatdouble都是计算机使用二进制模拟出来的,但我们在计算机中运行以下代码获得的结果并不是正确的。

double a = 0.1;
double b = 0.3;
system.out.println(b-a);

// 结果为0.19999999999999998

为什么呢?
我们先来看十进制小数如何转化为二进制数的

二进制小数 0.1111

第一位1表示十进制1/2
第二位1表示十进制1/4
第三位1表示十进制1/8
第四位1表示十进制1/16
......

所以十进制小数转二进制小数,算法是乘以2直到没有了小数为止。举个例子,0.9表示成二进制数

0.9*2=1.8   取整数部分 1
0.8(1.8的小数部分)*2=1.6    取整数部分 1
0.6*2=1.2   取整数部分 1
0.2*2=0.4   取整数部分 0
0.4*2=0.8   取整数部分 0
0.8*2=1.6 取整数部分 1
0.6*2=1.2   取整数部分 0
.........      0.9二进制表示为(从上往下): 1100100100100......

注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的 。其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10。这也就解释了为什么浮点型减法出现了"减不尽"的精度丢失问题。

bigdecimal简介

因为丢失精度问题,所以java引入了bigdecimal类来解决这一问题。

bigdecimal 由任意精度的整数非标度值 和32 位的整数标度 (scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负scale 次幂。因此,bigdecimal表示的数值是(unscaledvalue × 10-scale)。

bigdecimal函数介绍

构造函数

构造函数如下
java常用类之BigDecimal

我们使用参数为double的构造方法创建对象,并打印输出代码如下

bigdecimal b1 =new bigdecimal(0.1);
system.out.println(b1);

//结果为0.1000000000000000055511151231257827021181583404541015625

我们期望的结果为0.1,可是结果还是损失精度的。难道bigdecimal没用吗?

使用下面方式声明bigdecimal时,会出现精度问题

bigdecimal b = new bigdecimal(0.1);

推荐用法

bigdecimal bd1 = new bigdecimal("0.1");
bigdecimal bd2 = bigdecimal.valueof(0.1);

用以上方法就不会出问题精度问题
bigdecimal.valueof() 查看源码就可以知道,也是使用的new bigdecimal("0.1")构造函数

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

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

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

常用方法

  • 1.bigdecimal(string val):构造方法,将string类型转换成bigdecimal类型数据。
  • 2.bigdecimal(double val):构造方法,将double类型转换成bigdecimal类型数据。
  • 3.bigdecimal(int val):构造方法,将int类型转换成bigdecimal类型数据。
  • 4.bigdecimal add(bigdecimal value):加法,求两个bigdecimal类型数据的和。
  • 5.bigdecimal subtract(bigdecimal value):减法,求两个bigdecimal类型数据的差。
  • 6.bigdecimal multiply(bigdecimal  value):乘法,求两个bigdecimal类型数据的积。
  • 7.bigdecimal divide(bigdecimal divisor):除法,求两个bigdecimal类型数据的商。
  • 8.bigdecimal remainder(bigdecimal divisor):求余数,求bigdecimal类型数据除以divisor的余数。
  • 9.bigdecimal max(bigdecimal value):最大数,求两个bigdecimal类型数据的最大值。
  • 10.bigdecimal min(bigdecimal value):最小数,求两个bigdecimal类型数据的最小值。
  • 11.bigdecimal abs():绝对值,求bigdecimal类型数据的绝对值。
  • 12.bigdecimal negate():相反数,求bigdecimal类型数据的相反数。

代码演示

        bigdecimal  a=new bigdecimal ("4.5");
        bigdecimal  b=new bigdecimal ("1.5");
        bigdecimal  c=new bigdecimal ("-10.5");
 
        bigdecimal  add_result=a.add(b);
        bigdecimal  subtract_result=a.subtract(b);
        bigdecimal  multiply_result=a.multiply(b);
        bigdecimal  divide_result=a.divide(b);
        bigdecimal  remainder_result=a.remainder(b);
        bigdecimal  max_result=a.max(b);
        bigdecimal  min_result=a.min(b);
        bigdecimal  abs_result=c.abs();
        bigdecimal  negate_result=a.negate();
 
        log.d("tag","4.5+1.5="+add_result);
        log.d("tag","4.5-1.5="+subtract_result);
        log.d("tag","4.5*1.5="+multiply_result);
        log.d("tag","4.5/1.5="+divide_result);
        log.d("tag","4.5/1.5余数="+remainder_result);
        log.d("tag","4.5和1.5最大数="+max_result);
        log.d("tag","4.5和1.5最小数="+min_result);
        log.d("tag","-10.5的绝对值="+abs_result);
        log.d("tag","4.5的相反数="+negate_result);

结果为

4.5+1.5=6.0
4.5-1.5=3.0
4.5*1.5=6.75
4.5/1.5=3
4.5/1.5余数=0.0
4.5和1.5最大数=4.5
4.5和1.5最小数=1.5
-10.5的绝对值=10.5
4.5的相反数=-4.5

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

bigdecimalscale()表示小数位数,例如:

bigdecimal d1 = new bigdecimal("123.45");
bigdecimal d2 = new bigdecimal("123.4500");
bigdecimal d3 = new bigdecimal("1234500");
system.out.println(d1.scale()); // 2,两位小数
system.out.println(d2.scale()); // 4
system.out.println(d3.scale()); // 0

通过bigdecimalstriptrailingzeros()方法,可以将一个bigdecimal格式化为一个相等的,但去掉了末尾0的bigdecimal

bigdecimal d1 = new bigdecimal("123.4500");
bigdecimal d2 = d1.striptrailingzeros();
system.out.println(d1.scale()); // 4
system.out.println(d2.scale()); // 2,因为去掉了00

bigdecimal d3 = new bigdecimal("1234500");
bigdecimal d4 = d1.striptrailingzeros();
system.out.println(d3.scale()); // 0
system.out.println(d4.scale()); // -2

如果一个bigdecimalscale()返回负数,例如,-2,表示这个数是个整数,并且末尾有2个0。

可以对一个bigdecimal设置它的scale,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:

public class main {
    public static void main(string[] args) {
        bigdecimal d1 = new bigdecimal("123.456789");
        bigdecimal d2 = d1.setscale(4, roundingmode.half_up); // 四舍五入,123.4568
        bigdecimal d3 = d1.setscale(4, roundingmode.down); // 直接截断,123.4567
        system.out.println(d2);
        system.out.println(d3);
    }
}

bigdecimal做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:

bigdecimal d1 = new bigdecimal("123.456");
bigdecimal d2 = new bigdecimal("23.456789");
bigdecimal d3 = d1.divide(d2, 10, roundingmode.half_up); // 保留10位小数并四舍五入

在比较两个bigdecimal的值是否相等时,要特别注意,使用equals()方法不但要求两个bigdecimal的值相等,还要求它们的scale()相等:

bigdecimal d1 = new bigdecimal("123.456");
bigdecimal d2 = new bigdecimal("123.45600");
system.out.println(d1.equals(d2)); // false,因为scale不同
system.out.println(d1.equals(d2.striptrailingzeros())); // true,因为d2去除尾部0后scale变为2
system.out.println(d1.compareto(d2)); // 0

必须使用compareto()方法来比较,它根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。

总是使用compareto()比较两个bigdecimal的值,不要使用equals()
如果查看bigdecimal的源码,可以发现,实际上一个bigdecimal是通过一个biginteger和一个scale来表示的,即biginteger表示一个完整的整数,而scale表示小数位数:

public class bigdecimal extends number implements comparable<bigdecimal> {
    private final biginteger intval;
    private final int scale;
}

bigdecimal也是从number继承的,也是不可变对象。

源码分析

为什么bigdecimal使用string不会出现精度问题
现在我们明白为什么用double,float 出现出精度问题。现在我们要看一下bigdecimal对string做了什么不会出现精度问题

通过debug构造方法,我们可以了解,bigdecimal底层数据结构主要是由下面四个属性值组成.

int scale; //有多少位小数(即小数点后有多少位)
int precision; //总工有多少位数字
long intcompact; //字符串去掉小数点后,转为long的值,只有当传的字符串长度小于18时才使用该言
biginteger intval; //当传的字符串长度大于等于18时才使用biginteger表示数字

new bigdecimal("12.12")为例

scale值为2
precision值为4
intcompact值为1212
intval值为空。
之所以为空是因为字符串长度没有超18位,所以不启用biginteger表示

看到这进而其实大家应该明白了,bigdecimal将string转为了long或biginteger来进行计算。

valueof(doubleval)方法

java常用类之BigDecimal

valueof实际上是调用相应包装类的tostring方法然后使用bigdecimal的参数为string的构造方法创建对象。

add(bigdecimal augend)方法

     

    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对象
      }

以上只是对加法源码的分析,减乘除其实最终都返回的是一个新的bigdecimal对象,因为biginteger与bigdecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以a.add(b);虽然做了加法操作,但是a并没有保存加操作后的值,正确的用法应该是a=a.add(b);

总结

(1)商业计算使用bigdecimal。

(2)尽量使用参数类型为string的构造函数。

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

(4)我们往往容易忽略jdk底层的一些实现细节,导致出现错误,需要多加注意。

参考内容:
,
,