BigDecimal精度问题

BigDecimal精度问题

在近来项目展示价格的时候,遇到了一个问题,一个价格为99999.999的商品在购物车中的展示却是100000.00。

原因归结于在原项目代码中,是采用BigDecimal的格式,然而前端需要展示的时候,采用的却是String类型。

在类型转换的过程中,出现了精度损失。于是潜下心来对BigDecimal高精度的展示做了一番研究。

总结来看,BigDecimal转换成String主要有两种方式,且对于价格999999.999,两种方法的输出并不一致。具体代码如下:

BigDecimal price = new BigDecimal("999999.999");  //价格信息

//方法一
String format1 = String.format("%.2f", price); 
System.out.println("stringFormat:  "+format1); 	// 1000000.00

//方法二
String format2 = price.setScale(2, RoundingMode.DOWN).toPlainString();
System.out.println("scale:  "+ format2); 	// 999999.99

String.format方式

方法一,即String.format方式,是项目中最初使用的代码。为了一探究竟,追进format源码查看,可以看到代码如下:

public Formatter format(Locale l, String format, Object ... args) {
  ensureOpen();
  int last = -1;
  int lasto = -1;
  FormatString[] fsa = parse(format); // 首先解析字符串中特殊的标识符号
  for (int i = 0; i < fsa.length; i++) {
    FormatString fs = fsa[i]; 
    int index = fs.index(); // 获取到对应标识符号的下标
    try {
      switch (index) {
        case -2:  
          fs.print(null, l);
          break;
        case -1:  
          if (last < 0 || (args != null && last > args.length - 1))
            throw new MissingFormatArgumentException(fs.toString());
          fs.print((args == null ? null : args[last]), l);
          break;
        case 0:  
          lasto++;
          last = lasto;
          if (args != null && lasto > args.length - 1)
            throw new MissingFormatArgumentException(fs.toString());
          fs.print((args == null ? null : args[lasto]), l); // 关键代码
          break;
        default:  
          last = index - 1;
          if (args != null && last > args.length - 1)
            throw new MissingFormatArgumentException(fs.toString());
          fs.print((args == null ? null : args[last]), l);
          break;
      }
    } catch (IOException x) {
      lastException = x;
    }
  }
  return this;
}

可以看到关键方法其实是fs.print(),这里追进去查看print的源码。可以看到print源码首先会根据当前的类型做个判断,这里我们采用的是BigDecimal,因此会走到printFloat方法中。

    if (dt) {
        printDateTime(arg, l);
        return;
    }
    switch(c) {
    case Conversion.DECIMAL_INTEGER:
    case Conversion.OCTAL_INTEGER:
    case Conversion.HEXADECIMAL_INTEGER:
        printInteger(arg, l);
        break;
    case Conversion.SCIENTIFIC:
    case Conversion.GENERAL:
    case Conversion.DECIMAL_FLOAT:
    case Conversion.HEXADECIMAL_FLOAT: 
        printFloat(arg, l); // 方法会走到这里.
        break;
    case Conversion.CHARACTER:
    case Conversion.CHARACTER_UPPER:
        printCharacter(arg);
        break;
    case Conversion.BOOLEAN:
        printBoolean(arg);
        break;
    case Conversion.STRING:
        printString(arg, l);
        break;
    case Conversion.HASHCODE:
        printHashCode(arg);
        break;
    case Conversion.LINE_SEPARATOR:
        a.append(System.lineSeparator());
        break;
    case Conversion.PERCENT_SIGN:
        a.append('%');
        break;
    default:
        assert false;
    }
}

追入printFloat方法查看,可以看到,进一步的,printFloat方法又会对当前需要处理的对象进行类型的判断,并选取合适的方法进行处理。

private void printFloat(Object arg, Locale l) throws IOException {
  if (arg == null)
    print("null");
  else if (arg instanceof Float)
    print(((Float)arg).floatValue(), l);
  else if (arg instanceof Double)
    print(((Double)arg).doubleValue(), l);
  else if (arg instanceof BigDecimal)
    print(((BigDecimal)arg), l); // 关键方法
  else
    failConversion(c, arg);
}

追进BigDecimal对应的方法中,查看相应的print方法,可以看到关键方法其实是print(),因此我们继续追入查看相应代码。

private void print(BigDecimal value, Locale l) throws IOException {
  if (c == Conversion.HEXADECIMAL_FLOAT)
    failConversion(c, value);
  StringBuilder sb = new StringBuilder();
  boolean neg = value.signum() == -1; // 判断当前的数字符号是正数还是负数
  BigDecimal v = value.abs(); // 取其绝对值
  leadingSign(sb, neg); // 设置首个符号,'+' 或 ' ' 或 '(' 或 '-'
  print(sb, v, l, f, c, precision, neg); // 关键方法
  trailingSign(sb, neg); // 去除多余的符号
  a.append(justify(sb.toString()));
}
private void print(StringBuilder sb, BigDecimal value, Locale l,Flags f, char c, int precision, boolean neg)throws IOException{
  if(c == Conversion.SCIENTIFIC){
    ......
  } else if (c == Conversion.DECIMAL_FLOAT) {
    int prec = (precision == -1 ? 6 : precision); // 指的是我们当前设置的需要保留的精度,例子中为保留2位小数。
    int scale = value.scale(); // scale指的是当前数字的精度范围,例子中为3位小数因此是3。

    if (scale > prec) { // 如果当前位数大于要保留的位数
      int compPrec = value.precision();//这里comPrec指的是整个数的位数,例子中为9.
      if (compPrec <= scale) {
        value = value.setScale(prec, RoundingMode.HALF_UP);
      } else {
        compPrec -= (scale - prec); 
        // 重新计算减少后位数的值,随后调用BigDecimal方法重新整理位数
        // 而BigDecimal方法本身采用的RoundMode.HalfUp,因此在构造后会发生进位,导致结果出现差异。
        value = new BigDecimal(value.unscaledValue(), 
                               scale,
                               new MathContext(compPrec));
      }
    }
    BigDecimalLayout bdl = new BigDecimalLayout(
      value.unscaledValue(),
      value.scale(),
      BigDecimalLayoutForm.DECIMAL_FLOAT);

    char mant[] = bdl.mantissa();
    int nzeros = (bdl.scale() < prec ? prec - bdl.scale() : 0);

    if (bdl.scale() == 0 && (f.contains(Flags.ALTERNATE) || nzeros > 0))
      mant = addDot(bdl.mantissa());

    mant = trailingZeros(mant, nzeros);

    localizedMagnitude(sb, mant, f, adjustWidth(width, f, neg), l);
  }
}

至此,我们明白了第一种方法的弊端所在,即其是默认采用的RoundMode.halfUp方法,会自动进位导致数据出现偏差。

price.setScale方式

第二种方式,是修复后的方式,该方式的好处在于,可以自定义设置进位、舍入的策略,从而得到更符合逻辑的结果。具体源代码如下:

public BigDecimal setScale(int newScale, int roundingMode) {
  if (roundingMode < ROUND_UP || roundingMode > ROUND_UNNECESSARY)
    throw new IllegalArgumentException("Invalid rounding mode");
  int oldScale = this.scale;
  if (newScale == oldScale)        // 新旧位数一致则直接返回
    return this;
  if (this.signum() == 0)            // 0返回任意位数均可
    return zeroValueOf(newScale);
  if(this.intCompact!=INFLATED) { // 如果当前不超过-2^63(Long类型的上限),则进行计算
    long rs = this.intCompact; //rs为当前不带小数点的数值,eg. 999.99 ==> 99999 
    if (newScale > oldScale) { 
      // 如果新保留位数大于原位数
        int raise = checkScale((long) newScale - oldScale);
        if ((rs = longMultiplyPowerTen(rs, raise)) != INFLATED) {
          return valueOf(rs,newScale);
        }
        BigInteger rb = bigMultiplyPowerTen(raise);
        return new BigDecimal(rb, INFLATED, newScale, (precision > 0) ? precision + raise : 0);
      } else {	
      	// 否则计算需要丢弃的位数,并对应的创建新的BigDecimal对象
        int drop = checkScale((long) oldScale - newScale);
        if (drop < LONG_TEN_POWERS_TABLE.length) {
          // 关键方法
          return divideAndRound(rs, LONG_TEN_POWERS_TABLE[drop], newScale, roundingMode, newScale);
        } else {
          return divideAndRound(this.inflated(), bigTenToThe(drop), newScale, roundingMode, newScale);
        }
      }
  } else {
    if (newScale > oldScale) {
      int raise = checkScale((long) newScale - oldScale);
      BigInteger rb = bigMultiplyPowerTen(this.intVal,raise);
      return new BigDecimal(rb, INFLATED, newScale, (precision > 0) ? precision + raise : 0);
    } else {
      int drop = checkScale((long) oldScale - newScale);
      if (drop < LONG_TEN_POWERS_TABLE.length)
        return divideAndRound(this.intVal, LONG_TEN_POWERS_TABLE[drop], newScale, roundingMode,
                              newScale);
      else
        return divideAndRound(this.intVal,  bigTenToThe(drop), newScale, roundingMode, newScale);
    }
  }
}

紧接着我们追入divideAndRound方法中,

private static BigDecimal divideAndRound(long ldividend, long ldivisor, int scale, int roundingMode,
                                         int preferredScale) {
  int qsign; // 符号
  long q = ldividend / ldivisor; // 约到相应的位数,eg. 999999999/10 == > 99999999 
  if (roundingMode == ROUND_DOWN && scale == preferredScale)
    //如果是舍去的方案,则直接返回对应的值即可。
    return valueOf(q, scale); // 该方法会用对应的值创造一个BigDecimal的对象。 
  long r = ldividend % ldivisor; 
  qsign = ((ldividend < 0) == (ldivisor < 0)) ? 1 : -1;
  if (r != 0) {
    boolean increment = needIncrement(ldivisor, roundingMode, qsign, q, r);
    return valueOf((increment ? q + qsign : q), scale);
  } else {
    if (preferredScale != scale)
      return createAndStripZerosToMatchScale(q, scale, preferredScale);
    else
      return valueOf(q, scale);
  }
}

总结

总结来看,String.format方式默认采用的是Round_HalfUp的方式创建对应的对象,导致了数字出现进位的情况。而setScale方法,可以自由的选择对应的舍入方式,总体上看更为灵活。配合toPlainString方法可以很好的实现所有需要的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值