Java BigDecimal的基本原理和使用

0 背景

Java中floatdouble类型的数值在进行运算时会有精度丢失的风险。

# 示例代码
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
float c = 0.024f + 0.076f;
System.out.println("a=" + a);    // a=0.100000024
System.out.println("b=" + b);    // b=0.099999905
System.out.println("c=" + c);    // c=0.099999994
System.out.println("(a==b) = " + (a == b));  // (a==b) = false
System.out.println("(a==c) = " + (a == c));  // (a==c) = false

double x = 2.0 - 1.9;
double y = 1.8 - 1.7;
double z = 0.024 + 0.076;
System.out.println("x=" + x);    // x=0.10000000000000009
System.out.println("y=" + y);    // y=0.10000000000000009
System.out.println("z=" + z);    // z=0.1
System.out.println("(x==y) = " + (x == y));  // (x==y) = true
System.out.println("(x==z) = " + (x == z));  // (x==z) = false

《阿里巴巴 Java 开发手册》也提到:对于浮点数之间的等值判断,其基本数据类型不能使用 == 进行比较,其包装数据类型不能使用 equals 进行判断。


image.png
# 《阿里巴巴Java开发手册》中浮点数使用的反例
float a = 1.0F - 0.9F; 
float b = 0.9F - 0.8F; 
if (a == b) {
    // 预期进入此代码块,执行其它业务逻辑
    // 但事实上 a == b 的结果为 false
}

Float x = Float.valueOf(a); 
Float y = Float.valueOf(b); 
if (x.equals(y)) {
    // 预期进入此代码块,执行其它业务逻辑
    // 但事实上 equals 的结果为 false
}

Java提供了BigDecimal类来实现对浮点数的运算,并且不会造成精度丢失。
通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过BigDecimal来做的。

1 前置知识

1.1 十进制数与二进制数

我们在日常生活中使用的十进制数在计算机内部实际上是采用二进制来进行编码和计算。
后文中,使用B(Binary)来表示二进制,用D(Decimal)来表示十进制。

1.1.1 二进制数转十进制数

  • 整数部分从低位到高位,由对应值(0或1)依次乘以2的相应次方(0, 1, 2, 3, .....)
  • 小数部分则是从高位到低位,由对应值(0或1)依次乘以2的相应次方(-1, -2, -3, -4, .....)
  • 最终把整数部分和小数部分计算出的值进行相加,得到最终的十进制结果
10101.01(B) = 1 * 2^4 + 0 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 + 0 * 2^-1 + 1 * 2^-2 = 21.25(D)

1.1.2 十进制数转二进制数

十进制数转换为二进制数,需要对整数部分小数部分分别进行转换。

  • 整数部分的转换规则:反复除以基数2进行求余运算,直到余数为0为止,每次除法得到的余数从下到上依次排列即是二进制数的最终结果。
image.png
  • 小数部分的转换规则:要转换的十进制小数乘以基数2,得到的数字小数部分继续与基数2相乘,直到某一步结果的小数部分为0为止。每次相乘得到的中间结果的整数部分从高到低依次排列即是最终结果。
0.6875 * 2 = 1.375  整数部分=1  (高位)
0.375 * 2 = 0.75    整数部分=0    ↓
0.75 * 2 = 1.5      整数部分=1    ↓
0.5 * 2 = 1.0       整数部分=1  (低位)

0.6875(D) = 0.1011(B)

很多时候,多次乘积后中间结果的小数部分永远不能为0,这种情况下最终只能得到一个近似值(相当于十进制的无限循环小数)。

  • 最终将整数部分和小数部分的转换结果组合起来就可以得到对应的二进制编码。
135.6875(D) = 10000111.1011(B)

1.2 定点数与浮点数

计算机处理的数据多带有小数点,小数点在计算机中可以有两种方法表示:

  • 定点数
  • 浮点数

1.2.1 定点数

1.2.1.1 简介

约定数值的小数点固定在计算机二进制编码中的某一位置,小数点前后的数字分别用二进制表示,称为定点表示法,简称为定点数。

1.2.2.2 表示方法

  1. 在有限的 bit 宽度下,先约定小数点的位置
  2. 整数部分和小数部分,分别转换为二进制表示
  3. 两部分二进制组合起来,即是结果

1.2.1.3 示例

以1个字节(8 bit)为例,我们可以约定前5位表示整数部分,后3位表示小数部分

数字1.5可表示为
1.5(D) = 00001 100(B)

数字25.125可表示为
25.125(D) = 11001 001(B)

1.2.1.4 不足

  • 数值的表示范围有限(小数点越靠左,整个数值范围越小)
  • 数值的精度范围有限(小数点越靠右,数值精度越低)
    所以,在现代计算机中,定点数通常用来表示整数,对于高精度的小数,通常用浮点数表示

1.2.2 浮点数

1.2.2.1 简介

与定点数固定小数点所在位置不同,利用小数点位置的漂浮不定来表示数值的方法,称为浮点表示法,简称为浮点数。

1.2.2.2 表示方法

计算机中浮点数的表示方法启发于科学技术法,如下:

8.345 = 8.345 * 10^0
83.45 = 8.345 * 10^1
0.8345 = 8.345 * 10^-1

使用同样的规则,对于二进制数,我们也可以用科学计数法表示,把基数 10 换成 2 即可。
因此浮点计数法的格式可以写成这样:

V = (-1)^S * M * R^E
  • S:符号位,取值 0 或 1,决定一个数字的符号,0 表示正,1 表示负
  • M:尾数,用小数表示,例如前面所看到的 8.345 * 10^0,8.345 就是尾数
  • R:基数,表示十进制数 R 就是 10,表示二进制数 R 就是 2
  • E:指数,用整数表示,例如前面看到的 10^-1,-1 即是指数
    在计算机中,使用浮点数表示一个数字,只需要确定这几个变量即可。

1.2.2.3 示例

假设现在我们用 32 bit 表示一个浮点数,把以上变量按照一定规则,填充到这些 bit 上就可以了:


image.png

假设我们定义如下规则来填充这些 bit:

  • 符号位 S 占 1 bit
  • 指数 E 占 10 bit
  • 尾数 M 占 21 bit
    按照这个规则,将十进制数 25.125 转换为浮点数,转换过程就是这样的(D代表十进制,B代表二进制):
  1. 整数部分:25(D) = 11001(B)
  2. 小数部分:0.125(D) = 0.001(B)
  3. 用二进制科学计数法表示:25.125(D) = 11001.001(B) = 1.1001001 * 2^4(B)
    所以符号位 S = 0,尾数 M = 1.001001(B),指数 E = 4(D) = 100(B)。

1.2.2.4 浮点数标准

指数E和尾数M分配的位数不同,会产生以下问题:

  1. 指数位越多,尾数位则越少,其表示的范围越大,但精度就会变差;反之,指数位越少,尾数位则越多,表示的范围越小,但精度就会变好。

  2. 一个数字的浮点数格式,会因为定义的规则不同,得到的结果也不同,表示的范围和精度也有差异。

  3. 早期不同的计算机厂商,例如IBM、微软等会定义自己的浮点数规则,一个程序在不同厂商下的计算机中做浮点数运算时,需要先转换成这个厂商规定的浮点数格式,才能再计算,这加重了计算的成本,也降低了程序的可移植性。

因此业界迫切需要一个统一的浮点数标准。

直到1985年,IEEE 组织推出了浮点数标准,就是我们经常听到的 IEEE754 浮点数标准,这个标准统一了浮点数的表示形式,并提供了 2 种浮点格式:

  • 单精度浮点数 :32 位,符号位 S 占 1 bit,指数 E 占 8 bit,尾数 M 占 23 bit

  • 双精度浮点数 :64 位,符号位 S 占 1 bit,指数 E 占 11 bit,尾数 M 占 52 bit

IEEE754浮点数标准的详细介绍见:IEEE 754百度百科介绍IEEE 754浮点数标准详解

1.2.2.5 精度损失

  1. 部分十进制小数本身就无法被精确转换为二进制形式导致精度损失

十进制小数在转换成二进制时可能会出现无限循环的情况,由于计算机中表示数值的bit有限导致二进制编码被截断.

如以下,十进制数0.2转换为二进制的过程。

# 十进制数0.2转换为二进制时出现循环
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...

因此
0.2(D) = 0.0011 0011 0011 … (B)
  1. 浮点数之间的运算也会导致精度损失
    浮点数进行加减法运算时遵循一个原则:先对齐,再运算。
    对齐指的是对齐指数位 e。两个浮点数相加时,如果 e 不一样,就要先把 e 对齐再做加法运算。对齐的原则就是把 e 都统一成其中较大的一个。


    image.png

2 Java BigDecimal

2.1 简介

image.png

BigDecimal是不可变的、任意精度的有符号十进制数。
BigDecimal是由任意精度的整数未缩放值和32位的整数标度(scale)组成。

  • 如果标度scale为0或正数,则scale表示该数小数点后的位数。
  • 如果标度scale为负数,则将该数的未缩放值乘以10的负scale次幂。
    总结:即BigDecimal对象表示的数值等于 unscaledValue × 10的-scale次幂
    Value_{BigDecimal}=unscaledValue \times 10^{-scale}

2.2 源码解析

2.2.1 类定义

# BigDecimal类定义
public class BigDecimal extends Number implements Comparable<BigDecimal>
  • 继承Number类,提供将表示的数值转换为byte、double、float、int、long和short数值的方法
  • 实现Comparable接口,提供比较数值大小的compareTo方法

2.2.2 主要变量

# BigDecimal 主要变量
// 如果BigDecimal对象未缩放值的绝对值小于或等于(<=)Long.MAX_VALUE,将其未缩放值放入这个变量中,并用于后续计算
private final transient long intCompact;
    
// 如果BigDecimal对象未缩放值的绝对值大于(>)Long.MAX_VALUE,将其未缩放值放入这个变量中,并用于后续计算
private final BigInteger intVal;
    
// intCompact的哨兵值,若等于这个值则表示未缩放值只能从变量intVal中获得
static final long INFLATED = Long.MIN_VALUE;

// BigDecimal对象的标度
private final int scale;  // Note: this may have any value, so
                              // calculations must be done in longs

// BigDecimal对象的精度(精度是指十进制未缩放值的位数)
private transient int precision;

// BigDecimal对象的字符串表示形式的缓存
private transient String stringCache;

// 所有的18位长度的十进制字符串都能转换为long类型的值,但19位长度的十进制字符串不一定能转换为long
private static final int MAX_COMPACT_DIGITS = 18;

为什么变量INFLATED要设置成Long.MIN_VALUE

System.out.println(Long.MIN_VALUE);     // -9223372036854775808
System.out.println(Long.MAX_VALUE);     // 9223372036854775807
System.out.println(Long.MAX_VALUE + 1); // -9223372036854775808

代码示例

public static void main(String[] args) {
    // a的未缩放值 <= Long.MAX_VALUE
    BigDecimal a = new BigDecimal("123456789.000");
    
    // b的未缩放值 > Long.MAX_VALUE
    BigDecimal b = new BigDecimal(Long.MAX_VALUE + "1.000");  

    System.out.println(a);
    System.out.println(b);
}
image.png

2.2.3 构造方法

  1. BigDecimal关键构造方法
/**
 * 将十进制的字符数组表示形式转换为BigDecimal对象,接受与BigDecimal(String)构造方法相同的字符序列,同时允许指定子数组.
 * 
 * @param in        待转换的字符数组
 * @param offset    待转换字符数组中的第一个字符的下标
 * @param len       要转换的字符长度
 */
public BigDecimal(char[] in, int offset, int len, MathContext mc) {
        // 对数组长度进行检查,防止越界
        if (offset + len > in.length || offset < 0)
            throw new NumberFormatException("Bad offset or len arguments for char[] input.");      

        // 对计算过程中所有字段值都使用局部变量,直到计算结束
        int prec = 0;                 // 记录BigDecimal对象的精度(即未缩放值的数字位数)
        int scl = 0;                  // 记录BigDecimal对象的标度值
        long rs = 0;                  // 记录BigDecimal对象的intCompact变量(long类型的未缩放值)
        BigInteger rb = null;         // 记录BigDecimal对象的intVal变量(BigInteger类型的未缩放值)
        
        try {
            // 1.正负符号的处理:false表示'+',true表示'-
            boolean isneg = false;          // 默认为false,即默认数值是正数
            if (in[offset] == '-') {
                isneg = true;               // 第一个字符为'-',表示为负数
                offset++;
                len--;
            } else if (in[offset] == '+') { // 第一个字符可为'+',表示为正数
                offset++;
                len--;
            }

            // 2.开始处理字符数组中的数字部分
            boolean dot = false;             // '.'如果数组中存在'.',则dot为false
            long exp = 0;                    // 指数(exponent)
            char c;                          // 当前字符(current character)
            
            // 字符数组剩余部分的长度是否小于等于MAX_COMPACT_DIGITS(即18),小于18则证明使用long即可表示当前未缩放值
            boolean isCompact = (len <= MAX_COMPACT_DIGITS); 
            
            // 未缩放值字符数组的索引,只有当不能使用long表示未缩放值时才会使用这个数组,该数组用于生成BigInteger类型的intVal变量
            int idx = 0;
            if (isCompact) {
                // 3.1 长度小于等于18的情况,一定能用long表示未缩放值
                for (; len > 0; offset++, len--) {
                    c = in[offset];  // 获取当前字符
                    if ((c == '0')) { 
                        // 3.1.1 当前字符是'0'
                        if (prec == 0)
                            prec = 1; // 数组中的第一个字符为'0',精度置为1
                        else if (rs != 0) {
                            rs *= 10; // 未缩放值目前不为0,表示当前数值不是冗余的前置0,按十进制计算方法向左进一位
                            ++prec; // 精度(未缩放值位数)+1
                        } // 否则当前字符是冗余的前置零
                        if (dot)
                            ++scl; // 如果存在小数点,说明当前处于小数部分,标度值scale+1
                    } else if ((c >= '1' && c <= '9')) { // have digit
                        // 3.1.2 当前字符处于 '1'~'9',即非零的十进制数字字符
                        int digit = c - '0';  // 计算当前字符对应的十进制数值大小
                        if (prec != 1 || rs != 0)
                            ++prec; // 如果以0开头,则精度不变,否则精度+1
                        rs = rs * 10 + digit; // 未缩放值按十进制计算方法向左进一位
                        if (dot)
                            ++scl; // 如果存在小数点,说明当前处于小数部分,标度值scale+1
                    } else if (c == '.') {   // have dot
                        // 3.1.3 当前字符是'.'
                        if (dot) // 如果已经存在小数点,则说明当前字符是第二个小数点,不符合规范
                            throw new NumberFormatException();
                        dot = true;
                    } else if (Character.isDigit(c)) { // slow path
                        // 3.1.4 当前字符是否十进制字符
                        int digit = Character.digit(c, 10);
                        if (digit == 0) {
                            if (prec == 0)
                                prec = 1;
                            else if (rs != 0) {
                                rs *= 10;
                                ++prec;
                            } // else digit is a redundant leading zero
                        } else {
                            if (prec != 1 || rs != 0)
                                ++prec; // prec unchanged if preceded by 0s
                            rs = rs * 10 + digit;
                        }
                        if (dot)
                            ++scl;
                    } else if ((c == 'e') || (c == 'E')) {
                        // 3.1.5 当前字符是'e'或'E',表示存在科学计数法形式("1.23E+4")
                        exp = parseExp(in, offset, len); // 解析科学计数法形式的指数
                        if ((int) exp != exp) // 溢出
                            throw new NumberFormatException();
                        break; 
                    } else {
                        throw new NumberFormatException();
                    }
                }
                if (prec == 0) // 精度为0,表示找不到数字字符
                    throw new NumberFormatException();
                // 如果存在科学计数法的指数,还需要调整标度值scale
                if (exp != 0) { // 存在科学计数法指数
                    scl = adjustScale(scl, exp);
                }
                rs = isneg ? -rs : rs; // 未缩放值正负号处理
                
                // 后续为精度处理
                int mcp = mc.precision;
                int drop = prec - mcp; // prec has range [1, MAX_INT], mcp has range [0, MAX_INT];
                                       // therefore, this subtract cannot overflow
                if (mcp > 0 && drop > 0) {  // do rounding
                    while (drop > 0) {
                        scl = checkScaleNonZero((long) scl - drop);
                        rs = divideAndRound(rs, LONG_TEN_POWERS_TABLE[drop], mc.roundingMode.oldMode);
                        prec = longDigitLength(rs);
                        drop = prec - mcp;
                    }
                }
            } else {
                // 3.2 长度大于18的情况,不一定能用long表示未缩放值
                char coeff[] = new char[len];  // 未缩放值的字符数组
                for (; len > 0; offset++, len--) {
                    c = in[offset];  // 获取当前字符
                    if ((c >= '0' && c <= '9') || Character.isDigit(c)) {
                        // 3.2.1 当前字符处于 '0'~'9',即非零的十进制数字字符
                        if (c == '0' || Character.digit(c, 10) == 0) {
                            if (prec == 0) {
                                coeff[idx] = c;  // 保存第一个'0',且idx不变,即idx=0
                                prec = 1;  // 数组中的第一个字符为'0',精度置为1
                            } else if (idx != 0) {
                                coeff[idx++] = c;  // 当前字符不是冗余的前置0
                                ++prec;  // 精度(未缩放值的数字位数)+1
                            } // 否则当前字符是冗余的前置零
                        } else {
                            if (prec != 1 || idx != 0)
                                ++prec; // 如果以0开头,则精度不变,否则精度+1
                            coeff[idx++] = c;  // 保存当前字符
                        }
                        if (dot)
                            ++scl; // 如果存在小数点,说明当前处于小数部分,标度值scale+1
                        continue;
                    }
                    if (c == '.') {
                        // 3.2.2 当前字符是'.'
                        if (dot) // 如果已经存在小数点,则说明当前字符是第二个小数点,不符合规范
                            throw new NumberFormatException();
                        dot = true;
                        continue;
                    }
                    // 3.2.3 当前字符是'e'或'E',表示存在科学计数法形式("1.23E+4")
                    if ((c != 'e') && (c != 'E'))
                        throw new NumberFormatException();
                    exp = parseExp(in, offset, len);  // 解析科学计数法形式的指数
                    if ((int) exp != exp) // 溢出
                        throw new NumberFormatException();
                    break; 
                }
                if (prec == 0) // 精度为0,表示找不到数字字符
                    throw new NumberFormatException();
                // 如果存在科学计数法的指数,还需要调整标度值scale
                if (exp != 0) { // 存在科学计数法指数
                    scl = adjustScale(scl, exp);
                }
                
                rb = new BigInteger(coeff, isneg ? -1 : 1, prec); // 基于上述过程产生的coeff数组生成BigInteger对象rb
                rs = compactValFor(rb); // 基于rb生成rs,若rs=Long.MIN_VALUE,表示无法使用long来表示未缩放值
                
                // 后续为精度处理
                int mcp = mc.precision;
                if (mcp > 0 && (prec > mcp)) {
                    if (rs == INFLATED) {
                        int drop = prec - mcp;
                        while (drop > 0) {
                            scl = checkScaleNonZero((long) scl - drop);
                            rb = divideAndRoundByTenPow(rb, drop, mc.roundingMode.oldMode);
                            rs = compactValFor(rb);
                            if (rs != INFLATED) {
                                prec = longDigitLength(rs);
                                break;
                            }
                            prec = bigDigitLength(rb);
                            drop = prec - mcp;
                        }
                    }
                    if (rs != INFLATED) {
                        int drop = prec - mcp;
                        while (drop > 0) {
                            scl = checkScaleNonZero((long) scl - drop);
                            rs = divideAndRound(rs, LONG_TEN_POWERS_TABLE[drop], mc.roundingMode.oldMode);
                            prec = longDigitLength(rs);
                            drop = prec - mcp;
                        }
                        rb = null;
                    }
                }
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new NumberFormatException();
        } catch (NegativeArraySizeException e) {
            throw new NumberFormatException();
        }
        
        // 4.最终解析出的BigDecimal对象的各个变量
        this.scale = scl;
        this.precision = prec;
        this.intCompact = rs;
        this.intVal = rb;
    }
  1. parseExp方法
private static long parseExp(char[] in, int offset, int len){
        long exp = 0;
        offset++;
        char c = in[offset];
        len--;
        // 科学计数法形式存在正负号,对其符号进行处理(1.23E+4、1.23E-4)
        boolean negexp = (c == '-');
        if (negexp || c == '+') {
            offset++;
            c = in[offset];
            len--;
        }
        if (len <= 0) // 处理正负号后,长度小于等于0,表示没有指数位数,不符合规范形式
            throw new NumberFormatException();
        // 跳过科学计数法形式中指数冗余的前置0(1.23E+0004),直到长度小于10或不存在'0'字符
        while (len > 10 && (c=='0' || (Character.digit(c, 10) == 0))) {
            offset++;
            c = in[offset];
            len--;
        }
        // 为什么长度的判断要设置小于10?int取值范围为-2147483648 ~ 2147483647
        if (len > 10) // 过多的指数位数
            throw new NumberFormatException();
        // 不存在溢出,开始解析科学技术法形式中指数的实际值
        for (;; len--) {
            int v;
            if (c >= '0' && c <= '9') {
                v = c - '0';
            } else {
                v = Character.digit(c, 10);
                if (v < 0) // 非十进制数字字符
                    throw new NumberFormatException();
            }
            exp = exp * 10 + v; // 十进制进位计算
            if (len == 1)
                break; // 最后一个字符,跳出循环
            offset++;
            c = in[offset];
        }
        if (negexp) // 数值的正负转换
            exp = -exp;
        return exp;
    }
  1. adjustScale方法
private int adjustScale(int scl, long exp) {
        // 以实际例子来展示为什么调整后的scale值:adjustedScale = scl - exp
        // 1.23E+4 = 12300 -> scl=2, exp=4, prec=123
        // adjustedScale = 2-4 = -2
        // 实际值=prec*[10^(-adjustedScale)] = 123*(10^2)=12300
        long adjustedScale = scl - exp; 
        if (adjustedScale > Integer.MAX_VALUE || adjustedScale < Integer.MIN_VALUE)
            throw new NumberFormatException("Scale out of range.");
        scl = (int) adjustedScale;
        return scl;
    }

2.2.4 加法运算

/**
 * 返回一个 BigDecimal,其值为(this + augend),其标度为max(this.scale(), augend.scale())
 * @param augend    被加数(BigDecimal对象)。
 * @return          this(加数) + augend(被加数)
 */
public BigDecimal add(BigDecimal augend) {
    if (this.intCompact != INFLATED) {
        // 1. 加数this的有效值未溢出long类型
        if ((augend.intCompact != INFLATED)) {
            // 1.1 被加数augend有效值未溢出long类型(二者均使用long类型的intCompact变量进行加法运算)
            return add(this.intCompact, this.scale, augend.intCompact, augend.scale);
        } else {
            // 1.2 被加数augend有效值溢出long类型
            return add(this.intCompact, this.scale, augend.intVal, augend.scale);
        }
    } else {
        // 2. 加数this的有效值溢出long类型
        if ((augend.intCompact != INFLATED)) {
            // 2.1 被加数augend有效值未溢出long类型
            return add(augend.intCompact, augend.scale, this.intVal, this.scale);
        } else {
            // 2.2 被加数augend有效值溢出long类型
            return add(this.intVal, this.scale, augend.intVal, augend.scale);
        }
    }
}


private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
        // 1. 比较加数与被加数的标度值,使用long类型记录差异值
        long sdiff = (long) scale1 - scale2;
        if (sdiff == 0) {
            // 2.1 加数与被加数标度值一样,可直接相加
            return add(xs, ys, scale1);
        } else if (sdiff < 0) {
            // 2.2 加数的标度值 < 被加数的标度值 (eg:0.1 + 0.01,那么此时的 sdiff 就是 1)
            int raise = checkScale(xs,-sdiff);  // 获取标量差异的绝对值
            // 数值对齐,将加数乘以10^n次方,因为其scale标量会设置为跟被加数的scale标量相同
            long scaledX = longMultiplyPowerTen(xs, raise);
            if (scaledX != INFLATED) {
                // 加数扩大之后的结果没有溢出(超过Long类型支持的最大值)
                return add(scaledX, ys, scale2); // 相同标量的两者进行相加操作,并返回操作结果(注意返回值是new了一个对象进行存储!!)
            } else {
                // 加数扩大之后的结果出现溢出(超过Long类型支持的最大值),使用BigInteger表示加数及求和结果
                BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
                return ((xs^ys)>=0) ?     // 正负符号是否相同
                    new BigDecimal(bigsum, INFLATED, scale2, 0)  // 相同正负号则必然会溢出
                    : valueOf(bigsum, scale2, 0);                // 不同正负号可能不会溢出(根据值判断用long或BigInteger表示)
            }
        } else {
            // 2.3 加数的标度值 > 被加数的标度值 (eg:0.01 + 0.1,那么此时的 sdiff 就是 1)
            int raise = checkScale(ys,sdiff); // 获取标量差异的绝对值
            // 数值对齐,将被加数乘以10^n次方,因为其scale标量会设置为跟加数的scale标量相同
            long scaledY = longMultiplyPowerTen(ys, raise);
            if (scaledY != INFLATED) {
                // 被加数扩大之后的结果没有溢出(超过Long类型支持的最大值)
                return add(xs, scaledY, scale1); // 相同标量的两者进行相加操作,并返回操作结果(注意返回值是new了一个对象进行存储!!)
            } else {
                // 加数扩大之后的结果出现溢出(超过Long类型支持的最大值),使用BigInteger表示被加数及求和结果
                BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
                return ((xs^ys)>=0) ?     // 正负符号是否相同
                    new BigDecimal(bigsum, INFLATED, scale1, 0)  // 相同正负号则必然会溢出
                    : valueOf(bigsum, scale1, 0);                // 不同正负号可能不会溢出(根据值判断用long或BigInteger表示)
            }
        }
    }

2.2.5 比较操作

2.2.5.1 equals

/**
 * 比较此BigDecimal与指定的Object的相等性
 * 仅当两个BigDecimal对象的数值和标度都相等时, 此方法才认为它们相等(因此通过此方法进行比较时,2.0不等于2.00)
 * @param x  待比较的对象
 * @return   当且仅当指定的Object为BigDecimal,并且其数值和标度都等于此BigDecimal的数值和标度时,返回 true
 */
@Override
public boolean equals(Object x) {
        if (!(x instanceof BigDecimal))
            // 1. 比较对象非BigDecimal,直接返回false
            return false;
        BigDecimal xDec = (BigDecimal) x;  // 类型转换
        if (x == this)
            // 2. 同一个对象,必然相等,返回true
            return true;
        if (scale != xDec.scale)
            // 3. 两个对象的标度scale不相等,返回false
            return false;

        // 4. 比较两个对象的未缩放值大小(由于scale相等,未缩放值的比较结果即真实数值的比较结果)
        //    数值相等则返回true,否则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());
    }

2.2.5.2 compareTo

/**
 * 与指定的BigDecimal比较
 * 该方法中,值相等但具有不同标度的两个BigDecimal对象(如2.0等于2.00)
 * @param val   将此 BigDecimal 与之比较的 BigDecimal。
 * @return 当此BigDecimal在数值上小于、等于或大于val时,分别返回-1、0或1
 */
public int compareTo(BigDecimal val) {
        // 1. 精度相等并且二者的未缩放值均未膨胀的情况,可直接通过以下逻辑比较大小
        if (scale == val.scale) {
            long xs = intCompact;
            long ys = val.intCompact;
            if (xs != INFLATED && ys != INFLATED)
                return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
        }
        // 2. 提取二者的正负符号,进行数值大小的快速判断
        int xsign = this.signum();
        int ysign = val.signum();
        if (xsign != ysign)
            // 正负符号不同,正数 > 负数
            return (xsign > ysign) ? 1 : -1;
        if (xsign == 0)
            // 二者正负符号相等且为0,表示数值为0
            return 0;
        // compareMagnitude(val)比较二者数值的绝对值大小
        int cmp = compareMagnitude(val);
        return (xsign > 0) ? cmp : -cmp;  // 正数:绝对值越大,数值越大;负数:绝对值越大,数值越小
    }

2.3 注意事项

2.3.1 构造方法的使用

我们在使用 BigDecimal 时,为了防止精度丢失,推荐使用它的BigDecimal(String val)构造方法或者 BigDecimal.valueOf(double val) 静态方法来创建对象。
《阿里巴巴 Java 开发手册》对这部分内容也有提到,如下图所示:


image.png

代码示例:

System.out.println(new BigDecimal(0.2));     // 0.200000000000000011102230246251565404236316680908203125
System.out.println(new BigDecimal(0.2D));    // 0.200000000000000011102230246251565404236316680908203125
System.out.println(new BigDecimal("0.2"));   // 0.2
System.out.println(BigDecimal.valueOf(0.2)); // 0.2

输出结果:

0.200000000000000011102230246251565404236316680908203125
0.200000000000000011102230246251565404236316680908203125
0.2
0.2

注意:BigDecimal的valueOf方法虽然内部执行了Double的toString()方法,但Double的toString方法按double类型实际能表达的精度对尾数进行了截断。示例代码如下:

public static void main(String[] args) {
    System.out.println(Double.toString(123456789.123456789123456789));
        
    BigDecimal bd = BigDecimal.valueOf(123456789.123456789123456789);
    System.out.println(bd);
}

// 输出结果
1.2345678912345679E8
123456789.12345679

因此,对于超出double类型实际能表达的精度范围的数值,BigDecimal.valueOf(double)得到的是个近似值。在实际编码中最好使用new BigDecimal(string)来获得具有精确数值的BigDecimal对象。

【总结】

  • 使用BigDecimal(double val)初始化BigDecimal对象存在精度损失,在实际编码中禁止使用该构造方法 !!!!
  • 最好使用new BigDecimal(string)来获取具有精确数值的BigDecimal对象。
    2.3.2 等值比较问题
  • equals():同时比较数值和精度,只有当数值和精度都相等时才会判断为相等(true:相等 false:不相等)
  • compareTo():只比较数值,会忽略精度,数值相等时即判断为相等(-1:小于 0:等于 1:大于)

《阿里巴巴 Java 开发手册》中也提到:


image.png

示例代码

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("1.0");
System.out.println(a.equals(b));     // false (判断为不等)
System.out.println(a.compareTo(b));  // 0     (判断为相等)

【总结】

  • 比较BigDecimal对象的值是否相等时,应该使用compareTo()方法,才能得到预期的比较结果

2.3.3 运算精度问题

image.png

方法名
说明
计算结果的首选精度(scale)
add
将两个 BigDecimal 对象相加
max ( 加数的scale, 被加数的scale )
subtract
将两个 BigDecimal 对象相减
max ( 被减数的scale, 减数的scale)
multiply
将两个 BigDecimal 对象相乘
乘数的scale + 被乘数的scale
divide
将两个 BigDecimal 对象相除
被除数scale - 除数的scale

我们在使用 divide 方法的时候尽量使用 3 个参数的版本,并且RoundingMode 不要选择 UNNECESSARY,否则很可能会遇到 ArithmeticException(无法除尽出现无限循环小数的时候),其中 scale 表示要保留几位小数,roundingMode 代表舍入模式。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.add(b));        // 1.9
System.out.println(a.subtract(b));   // 0.1
System.out.println(a.multiply(b));   // 0.90
System.out.println(a.divide(b));     // 无法除尽,抛出 ArithmeticException 异常(如下图所示)
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));   // 1.11
image.png

【总结】

  • BigDecimal类提供了加、减、乘、除四种算术运算,在使用BigDecimal进行除法运算时,要明确指定精度和舍入模式,避免因为除不尽导致程序异常。

2.3.4 字符串输出

在将BigDecimal对象转换成十进制字符串进行输出的时候,直接调用toString()方法输出字符串可能会获得预料之外的结果。
先来看看下面的代码:

public static void main(String[] args) {
    BigDecimal a = new BigDecimal("3.563453525545672E16");
    System.out.println(a.toString());
}

执行的结果是实际数值的十进制字符串吗?并不是:

3.563453525545672E+16

也就是说,本来想打印实际数值对应的十进制字符串("35634535255456720")的,结果打印出来的是它用科学计数法表示的字符串。

BigDecimal提供了字符串输出的三个方法:

  • toPlainString():不使用任何科学计数法;
  • toString():在必要的时候使用科学计数法;
  • toEngineeringString() :在必要的时候使用工程计数法。类似于科学计数法,只不过指数的幂都是3的倍数,这样方便工程上的应用,因为在很多单位转换的时候都是10^3;

三种方法的输出结果示例如下:

public static void main(String[] args) {
    BigDecimal a = new BigDecimal("3.563453525545672E16");
    System.out.println(a.toString());
    System.out.println(a.toPlainString());
    System.out.println(a.toEngineeringString());
}

// 输出结果
3.563453525545672E+16    // toString()
35634535255456720        // toPlainString()
35.63453525545672E+15    // toEngineeringString()

3 总结

  • float和double类型的主要设计目标是为了科学计算和工程计算。它们可以在广域数值范围上提供较为精确的快速近似计算,然而却无法提供完全精确的结果,所以不应该被用于要求精确结果的场合。
  • 商业计算往往要求结果精确(银行/金融/风控/支付结算等),float和double无法提供精确的数值表示及计算结果,因此Java提供了BigDecimal类来满足浮点数精确运算结果的业务场景,使用BigDecimal来操作浮点数不会造成精度丢失。

使用BigDecimal需要注意以下事项:

  • 构造函数问题
  • 等值比较问题
  • 运算精度问题(除法运算)
  • 字符串输出

在实际编码中,我们可以编写BigDecimal 工具类,来简化规范BigDecimal 的操作。
以下为BigDecimal 工具类的参考代码。

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * @ClassName BigDecimalUtil
 * @Description: 简化BigDecimal计算的小工具类
 * @Author py
 * @Date 2022/11/7 7:45 PM
 */
public class BigDecimalUtil {
    /**
     * 默认除法运算精度
     */
    private static final int DEF_DIV_SCALE = 2;

    /**
     * 提供精确的加法运算。
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    public static double add(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.add(b2).doubleValue();
    }

    /**
     * 提供精确的减法运算。
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static double subtract(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.subtract(b2).doubleValue();
    }

    /**
     * 提供精确的乘法运算。
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static double multiply(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.multiply(b2).doubleValue();
    }

    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
     * 小数点以后DEF_DIV_SCALE位,以后的数字四舍五入。
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商
     */
    public static double divide(double v1, double v2) {
        return divide(v1, v2, DEF_DIV_SCALE);
    }

    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
     * 定精度,以后的数字四舍五入。
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商
     */
    public static double divide(double v1, double v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();
    }

    /**
     * 提供精确的小数位四舍五入处理。
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果
     */
    public static double round(double v, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b = BigDecimal.valueOf(v);
        BigDecimal one = new BigDecimal("1");
        return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
    }

    /**
     * 提供精确的类型转换(Float)
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static float convertToFloat(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.floatValue();
    }

    /**
     * 提供精确的类型转换(Int)不进行四舍五入
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static int convertsToInt(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.intValue();
    }

    /**
     * 提供精确的类型转换(Long)
     *
     * @param v 需要被转换的数字
     * @return 返回转换结果
     */
    public static long convertsToLong(double v) {
        BigDecimal b = new BigDecimal(v);
        return b.longValue();
    }

    /**
     * 返回两个数中大的一个值
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 返回两个数中大的一个值
     */
    public static double returnMax(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.max(b2).doubleValue();
    }

    /**
     * 返回两个数中小的一个值
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 返回两个数中小的一个值
     */
    public static double returnMin(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.min(b2).doubleValue();
    }

    /**
     * 精确对比两个数字
     *
     * @param v1 需要被对比的第一个数
     * @param v2 需要被对比的第二个数
     * @return 如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1
     */
    public static int compareTo(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.compareTo(b2);
    }
}

参考文章

  1. 定点数和浮点数

  2. 一文读懂浮点数

  3. 计算机系统基础(四)浮点数

  4. BigDecimal 详解

  5. IEEE 754浮点数标准详解

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,548评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,497评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,990评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,618评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,618评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,246评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,819评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,725评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,268评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,356评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,488评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,181评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,862评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,331评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,445评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,897评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,500评论 2 359

推荐阅读更多精彩内容