慎用 BigDecimal 的 toString() — 谈谈 BigDecimal 的 toString() 和 toPlainString() 的区别

1,020 阅读3分钟

背景

最近工作里需要处理数字的展示,数字会被计算成 double 型数据,再转换到VO里,作为 XX.Y 展示出去。

问题

之前的处理逻辑如下面代码,当遇到 10.0 这样的数字时,展示的是 1E1 这样的科学计数法,这与期望的 10 不一样。

BigDecimal.valueOf(number).stripTrailingZeros())

原因

在 Java 中,BigDecimal 的输出格式是受其内部表示方式和 toString 方法的影响。 这里的关键点是:

  1. 浮点数的表示:当你使用 10.0(一个 double 值)来构造 BigDecimal 时,BigDecimal.valueOf(double) 将其转化为字符串,而在这个过程中,浮点数可能会被以科学计数法表示。如果这个值被解析为 10.0,那么它将被表示为 1E+1
  2. 内部数据结构BigDecimal 用整型和幂的组合来表示数字,因此对于 10.0,它实际上被认为是 1 乘以 10 的幂(即 10^1),所以它使用科学计数法的结构。

解法

用 BigDicemal 的 toPlainString() 方法替代

 public String toPlainString() {
        if(scale==0) {
            if(intCompact!=INFLATED) {
                return Long.toString(intCompact);
            } else {
                return intVal.toString();
            }
        }
        if(this.scale<0) { // No decimal point
            if(signum()==0) {
                return "0";
            }
            int tailingZeros = checkScaleNonZero((-(long)scale));
            StringBuilder buf;
            if(intCompact!=INFLATED) {
                buf = new StringBuilder(20+tailingZeros);
                buf.append(intCompact);
            } else {
                String str = intVal.toString();
                buf = new StringBuilder(str.length()+tailingZeros);
                buf.append(str);
            }
            for (int i = 0; i < tailingZeros; i++)
                buf.append('0');
            return buf.toString();
        }
        String str ;
        if(intCompact!=INFLATED) {
            str = Long.toString(Math.abs(intCompact));
        } else {
            str = intVal.abs().toString();
        }
        return getValueString(signum(), str, scale);
    }
  1. 判断是否有小数部分
    if(scale==0) {
        if(intCompact!=INFLATED) {
            return Long.toString(intCompact);
        } else {
            return intVal.toString();
        }
    }
  • 如果 scale 为 0,表示没有小数部分。
  • 如果 intCompact 不是 INFLATED(可能表示它是一个较小的整数),则直接将其转换为字符串返回。
  • 否则使用 intVal 的字符串表示返回。
  1. 处理没有小数点的情况
    if(this.scale<0) { // No decimal point
        if(signum()==0) {
            return "0";
        }
        int tailingZeros = checkScaleNonZero((-(long)scale));
        StringBuilder buf;
        if(intCompact!=INFLATED) {
            buf = new StringBuilder(20+tailingZeros);
            buf.append(intCompact);
        } else {
            String str = intVal.toString();
            buf = new StringBuilder(str.length()+tailingZeros);
            buf.append(str);
        }
        for (int i = 0; i < tailingZeros; i++)
            buf.append('0');
        return buf.toString();
    }
  • 如果 scale 为负数,表示没有小数点且有多个尾随零。 - 如果数值的符号是 0,直接返回 "0"。 - 调用 checkScaleNonZero 方法确定需要补充的零的数量。 - 创建一个字符缓冲区 buf,根据 intCompact 的值选择初始化字符串长度。 - 将实际数值(可能是长整型或者大整数)添加到 buf 中。 - 在 buf 后面追加必要的零。 - 最后返回构建的字符串。
  1. 处理有小数点的情况
    String str ;
    if(intCompact!=INFLATED) {
        str = Long.toString(Math.abs(intCompact));
    } else {
        str = intVal.abs().toString();
    }
    return getValueString(signum(), str, scale);
    ```
  • 如果 scale 大于 0,表示数值有小数点。
  • 判断 intCompact 是否是 INFLATED,选择获取数值的绝对值字符串。
  • 通过调用 getValueString 方法传入符号、绝对值字符串和小数位数来生成最终的字符串表示。

总结

BigDecimal.toPlainString() 这个方法的作用是根据数值的符号、整数和小数部分的不同情况,生成一个纯文本格式的字符串表示。这是一个典型的数值转换方法,适用于需要精确表示数值(如财务数据)的场合,不使用科学计数法形式。