一文带你读懂 BigDecimal 源码

点击上方「蓝字」关注我们

本章带来的是BigDecimal类的源码解读。BigDecimal类是 Java 在 java.math 包中提供的API类,用来对超过16位有效位的数进行精确的运算。除了复杂度设计和拓展性,里面的数学计算思维也很值得我们学习。对于用惯了float/double的同学,得好好仔细看看了。

JDK官方文档定义

Immutable,arbitrary precision signed decimal numbers.  A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale.

If zero or positive, the scale is the number of digits to the right of the decimal point. If negative, the unscaled value of the number is multiplied by ten to the power of the negation of the scale. The value of the number represented by the BigDecimal is therefore (unscaledValue × 10-scale).

翻译:BigDecimal 由任意精度的整数非标度值 32 位的整数标度 (scale) 组成。(精度,就是非标度值的数字个数)

如果整数部分为零或正数,则标度是小数点后的位数。

如果为负数,则将该数的非标度值乘以 10 的负scale 次幂。

因此,BigDecimal表示的数值是(unscaledValue × 10的[-scale]次方)。

举个栗子:

BigDecimal表示的数为: unscaledValue × 10的 -scale 次幂,

  • 要表示 0.01,unscaledValue

=1000000000000000020816681711721685132943093776702880859375,scale = 59;

  • 要表示100,unscaledValue = 100,scale = 0;

  • 要表示100.01,unscaledValue

=1000100000000000051159076974727213382720947265625,scale = 46;

  • 要表示 -100.01,unscaledValue = 

-1000100000000000051159076974727213382720947265625,scale = 46;

即是说,

如果scale为零或正数,最终的结果中,小数点后面的位数就等于scale标度

如果 scale 是负数,那最终的结果将会是乘以 10的|scale| 次方

Q

看到这里,有小伙伴要问了,为何输入的0.01,计算机理解到的却是(1000000000000000020816681711721685132943093776702880859375)呢?这里我先挖个坑,文章后面再填。

BigDecimal 的成员变量

这里主要介绍BigDecimal类的全局变量,如下列表:

成员变量
Modifier and TypeField and Description
static BigDecimalONE

值1,标度为0。

static intROUND_CEILING

圆形模式向正无穷大转弯。

static intROUND_DOWN

舍入模式向零舍入。

static intROUND_FLOOR

舍入模式向负无穷大转弯。

static intROUND_HALF_DOWN

四舍五入模式向“最近邻居”转弯,除非这两个邻居都是等距离的,在这种情况下,这是倒圆的。

static intROUND_HALF_EVEN

四舍五入模式向“最近邻居”转弯,除非两个邻居都是等距离的,在这种情况下,向着邻居方向转移。

static intROUND_HALF_UP

四舍五入模式向“最近邻居”转弯,除非两个邻居都是等距的,在这种情况下是圆括弧的。

static intROUND_UNNECESSARY

舍入模式来确定所请求的操作具有精确的结果,因此不需要舍入。

static intROUND_UP

舍入模式从零开始。

static BigDecimalTEN

值为10,标度为0。

static BigDecimalZERO

值为0,标度为0。


乍一看,都是静态变量,而且都是对某些特殊场景进行的声明。

  • ROUND_CEILING/ROUND_DOWN/ROUND_FLOOR/ROUND_HALF_DOWN/ROUND_HALF_EVEN/ROUND_HALF_UP/ROUND_UNNECESSARY/ROUND_UP 标识了BigDecimal特有的8种四舍五入模式。

  • ONE/ TEN / ZERO则是BigDecimal 特有的数值缓存数组zeroThroughTen[] 的元素之三。

  • 私有成员intVal就是非标度值,而scale就是标度。

BigDecimal  的构造器

这里介绍的是BigDecimal的构造方法。一个类需要被使用首先得创建出来吧,那么构造方法就是创建对象时第一个被调用的方法,BigDecimal类的构造器有多种重载实现,抓住关键词:非标度值、 标度、运算规则。

见下列表:

构造方法
Constructor and Description
BigDecimal(BigInteger val)

将 BigInteger转换成 BigDecimal 。

BigDecimal(BigInteger unscaledVal, int scale)

将BigInteger的 BigInteger值和 int等级转换为 BigDecimal 。

BigDecimal(BigInteger unscaledVal, int scale, MathContext mc)

将 BigInteger未缩放值和 int扩展转换为 BigDecimal ,根据上下文设置进行舍入。

BigDecimal(BigInteger val, MathContext mc)

根据上下文设置将 BigInteger转换为 BigDecimal舍入。

BigDecimal(char[] in)

一个转换的字符数组表示 BigDecimal成 BigDecimal ,接受字符作为的相同序列 BigDecimal(String)构造。

BigDecimal(char[] in, int offset, int len)

一个转换的字符数组表示 BigDecimal成 BigDecimal ,接受字符作为的相同序列 BigDecimal(String)构造,同时允许一个子阵列被指定。

BigDecimal(char[] in, int offset, int len, MathContext mc)

一个转换的字符数组表示 BigDecimal成 BigDecimal ,接受字符作为的相同序列 BigDecimal(String)构造,同时允许指定一个子阵列和用根据上下文设置进行舍入。

BigDecimal(char[] in, MathContext mc)

一个转换的字符数组表示 BigDecimal成 BigDecimal ,接受相同的字符序列作为 BigDecimal(String)构造与根据上下文设置进行舍入。

BigDecimal(double val)

将 double转换为 BigDecimal ,这是 double的二进制浮点值的精确十进制表示。

BigDecimal(double val, MathContext mc)

将 double转换为 BigDecimal ,根据上下文设置进行舍入。

BigDecimal(int val)

将 int成 BigDecimal 。

BigDecimal(int val, MathContext mc)

将 int转换为 BigDecimal ,根据上下文设置进行舍入。

BigDecimal(long val)

将 long成 BigDecimal 。

BigDecimal(long val, MathContext mc)

将 long转换为 BigDecimal ,根据上下文设置进行舍入。

BigDecimal(String val)

将BigDecimal的字符串表示 BigDecimal转换为 BigDecimal 。

BigDecimal(String val, MathContext mc)

一个转换的字符串表示 BigDecimal成 BigDecimal ,接受相同的字符串作为 BigDecimal(String)构造,利用根据上下文设置进行舍入。

粗略扫描一遍之后,我们发现BigDecimal源码设计可谓考虑周全。

  • BigDecimal 提供对int/long/string/BigInteger的数值构造,官方推荐的用法是使用字符串的形式初始化。

  • 另外每一种构造方法,都提供了一种设置舍入模式参数的方法重载(MathContext,它内部封装了RoundingMode对象,RoundingMode是指定能够丢弃精度的数值运算的舍入行为,每个舍入模式指示如何计算舍入结果的最低有效返回数字)。

BigDecimal 的常用方法

BigDecimal 的常用方法也就是我们小学中学经常用到的加减乘除了,但源码提供的能力可不仅仅局限于此,碍于篇幅,这里只讲述“加减”作为大家阅读源码的突破口,意在抛砖引玉。

  • 加法:add()提供了7种重载方法,这里对第一个方法进行源码分析

    • 源码解读是从 add(BigDecimal augend) 抽出的部分逻辑

  /**
   * <p>加法</p> 
   */
  private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
    // 1 比较两者的标量
    long sdiff = (long) scale1 - scale2;
    if (sdiff == 0) {
      // 2 标量一样可以直接相加运算
      return add(xs, ys, scale1);
    } else if (sdiff < 0) {
      // 3 加数的标量 < 被加数的标量 ,获取标量差异的绝对值(eg:0.1 + 0.01,那么此时的 sdiff 就是 1)
      int raise = checkScale(xs,-sdiff);
      // 将加数乘以10^n次方,因为其scale标量会设置为跟被加数的scale标量相同
      long scaledX = longMultiplyPowerTen(xs, raise);
      // 加数扩大之后的结果没有溢出(超过Long类型支持的最大值)
      if (scaledX != INFLATED) {
        // 相同标量的两者进行相加操作,并返回操作结果(注意返回值是new了一个对象进行存储!!)
        return add(scaledX, ys, scale2);
      } else {
        BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
        return ((xs^ys)>=0) ? // same sign test
            new BigDecimal(bigsum, INFLATED, scale2, 0)
            : valueOf(bigsum, scale2, 0);
      }
    } else {
      // 4 加数的标量 > 被加数的标量 ,获取标量差异的绝对值(eg:0.01 + 0.1,那么此时的 sdiff 就是 -1)
      int raise = checkScale(ys,sdiff);
      // 将被加数乘以10^n次方,(n此时为负数),因为其scale标量会设置为跟加数的scale标量相同
      long scaledY = longMultiplyPowerTen(ys, raise);
      // 被加数扩大之后的结果没有溢出(超过Long类型支持的最大值)
      if (scaledY != INFLATED) {
        // 相同标量的两者进行相加操作,并返回操作结果(注意返回值是new了一个对象进行存储!!)
        return add(xs, scaledY, scale1);
      } else {
        BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
        return ((xs^ys)>=0) ?
            new BigDecimal(bigsum, INFLATED, scale1, 0)
            : valueOf(bigsum, scale1, 0);
      }
    }
  }
  • 减法:提供了两种重载方法,为了更好理解源码的思想,这里选择第二个重载方法进行解读

    • 源码解读是从 subtract(BigDecimal subtrahend, MathContext mc)抽出的部分逻辑



  /**
   * <p>减法,注意运算结果也是封装在新的对象里,千万注意返回值!!</p>
   */
  public BigDecimal subtract(BigDecimal subtrahend, MathContext mc) {
    // MathContext 的 precision 属性:用于定义操作的位数;,即结果四舍五入到这个精度
    if (mc.precision == 0)
      // 精度等于0,表示可以直接运算得出结果
      return subtract(subtrahend);
    // 如果需要四舍五入
    return add(subtrahend.negate(), mc);
  }


  public BigDecimal subtract(BigDecimal subtrahend) {
    // 减法的本质就是与一个值相反的数进行相加
    if (this.intCompact != INFLATED) {
      if ((subtrahend.intCompact != INFLATED)) {
        // 取相反值进行加法操作,这里的加法操作跟上文对add的源码解读完全一致!!
        return add(this.intCompact, this.scale, -subtrahend.intCompact, subtrahend.scale);
      } else {
        return add(this.intCompact, this.scale, subtrahend.intVal.negate(), subtrahend.scale);
      }
    } else {
      if ((subtrahend.intCompact != INFLATED)) {
        // Pair of subtrahend values given before pair of
        // values from this BigDecimal to avoid need for
        // method overloading on the specialized add method
        return add(-subtrahend.intCompact, subtrahend.scale, this.intVal, this.scale);
      } else {
        return add(this.intVal, this.scale, subtrahend.intVal.negate(), subtrahend.scale);
      }
    }
  }


  public BigDecimal add(BigDecimal augend, MathContext mc) {
    // 精度等于0,表示可以直接运算得出结果
    if (mc.precision == 0)
      return add(augend);
    BigDecimal lhs = this;


    // If either number is zero then the other number, rounded and
    // scaled if necessary, is used as the result.
    {
      // 精度是否为0
      boolean lhsIsZero = lhs.signum() == 0;
      boolean augendIsZero = augend.signum() == 0;


      if (lhsIsZero || augendIsZero) {
        // 减数与被减数之一的精度为0
        int preferredScale = Math.max(lhs.scale(), augend.scale());
        BigDecimal result;


        if (lhsIsZero && augendIsZero)
          return zeroValueOf(preferredScale);
        // 减数精度为0,那么返回被减数四舍五入的结果,反之减数四舍五入结果
        result = lhsIsZero ? doRound(augend, mc) : doRound(lhs, mc);


        if (result.scale() == preferredScale)
          return result;
        else if (result.scale() > preferredScale) {
          return stripZerosToMatchScale(result.intVal, result.intCompact, result.scale, preferredScale);
        } else { // result.scale < preferredScale
          int precisionDiff = mc.precision - result.precision();
          int scaleDiff     = preferredScale - result.scale();
          if (precisionDiff >= scaleDiff)
            return result.setScale(preferredScale); // can achieve target scale
          else
            return result.setScale(result.scale() + precisionDiff);
        }
      }
    }


    // 减数与被减数的精度差
    long padding = (long) lhs.scale - augend.scale;
    if (padding != 0) { // scales differ; alignment needed
      // 两者精度差不为0,需要对两者的,返回值时一个BigDecimal数组,第一个元素是精度较大者
      BigDecimal arg[] = preAlign(lhs, augend, padding, mc);
      // 设置减数与被减数的标量一致
      matchScale(arg);
      lhs = arg[0];
      augend = arg[1];
    }
    // 运算结果
    return doRound(lhs.inflated().add(augend.inflated()), lhs.scale, mc);
  }


针对 BigDecimal 常用方法的测试用例

基于上面的分析,我们对常用方法提供测试用例了解基础使用:

public class TestBigDecimal {
  public static void main(String[] args) {
    test1();
  }


  private static void test1(){
    BigDecimal valueSec = new BigDecimal(1000000);
    BigDecimal valueFir = new BigDecimal(1000000);
    BigDecimal valueThi = new BigDecimal(-1000000);


    //尽量用字符串的形式初始化
    BigDecimal stringFir = new BigDecimal("0.005");
    BigDecimal stringSec = new BigDecimal("1000000");
    BigDecimal stringThi = new BigDecimal("-1000000");


    //加法
    BigDecimal addVal = valueFir.add(valueSec);
    System.out.println("加法用value结果:" + addVal);
    BigDecimal addStr = stringFir.add(stringSec);
    System.out.println("加法用string结果:" + addStr);


    //减法
    BigDecimal subtractVal = valueFir.subtract(valueSec);
    System.out.println("减法value结果:" + subtractVal);
    BigDecimal subtractStr = stringFir.subtract(stringSec);
    System.out.println("减法用string结果:" + subtractStr);


    //乘法
    BigDecimal multiplyVal = valueFir.multiply(valueSec);
    System.out.println("乘法用value结果:" + multiplyVal);
    BigDecimal multiplyStr = stringFir.multiply(stringSec);
    System.out.println("乘法用string结果:" + multiplyStr);


    //绝对值
    BigDecimal absVal = valueThi.abs();
    System.out.println("绝对值用value结果:" + absVal);
    BigDecimal absStr = stringThi.abs();
    System.out.println("绝对值用string结果:" + absStr);


    //除法
    BigDecimal divideVal = valueSec.divide(valueFir, 20, BigDecimal.ROUND_HALF_UP);
    System.out.println("除法用value结果:" + divideVal);
    BigDecimal divideStr = stringSec.divide(stringFir, 20, BigDecimal.ROUND_HALF_UP);
    System.out.println("除法用string结果:" + divideStr);


  }


}



输出结果:

加法用value结果:2000000
加法用string结果:1000000.005
减法value结果:0
减法用string结果:-999999.995
乘法用value结果:1000000000000
乘法用string结果:5000.000
绝对值用value结果:1000000
绝对值用string结果:1000000
除法用value结果:1.00000000000000000000
除法用string结果:200000000.00000000000000000000

BigDecimal 和 基础数据类型的使用区别?

文章一开头就有小伙伴要问了,为何输入的0.01,计算机理解到的却是

1000000000000000020816681711721685132943093776702880859375呢?

Good,那么这里回答下小伙伴的疑问。

  • 因为BigDecimal的参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中写入newBigDecimal(0.1)所创建的BigDecimal正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于

    0.1000000000000000055511151231257827021181583404541015625。

    这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样导致了,传入到BigDecimal 构造方法的值不会正好等于 0.1(即使虽然表面上等于该值)。

  • 更确切的原因是,计算机是二进制的。浮点数没有办法是用二进制进行精确表示。我们的CPU表示浮点数由两个部分组成:指数和尾数,这样的表示方法一般都会失去一定的精确度,有些浮点数运算也会产生一定的误差。

热心老铁

float和double类型的主要设计目标是为了科学计算和工程计算。他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们没有提供完全精确的结果,所以不应该被用于要求精确结果的场合。商业计算往往要求结果精确(银行/金融/风控/支付结算等)就必须选择BigDecimal了。

话虽如此,使用BigDecimal还是有两个缺点的哦。一是与使用基本数据类型相比,BigDecimal的使用不方便,二是BigDecimal对代码性能造成一定的影响。使用BigDecimal的一个好处是,它允许你完全控制舍入,每当一个操作涉及舍入时,它允许你从8种舍入模式中选择。
So,如果性能非常关键,而且你又不介意自己记录十进制小数点,而且所涉及到的数值不大,可以使用int或者long。但如果数值可能超过了18位数字,就必须使用BigDecimal了。

以上是这次分享的 BigDecimal 源码剖析的全部内容了,欢迎加入群聊进行讨论或者后天留言~~~

—END—

点个“在看”表示朕

已阅

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值