拒绝赔钱!Java 中使用 BigDecimal 一些需要注意的点~

127 阅读6分钟

哈哈,有点标题党了,公司不会让你赔钱的 ~ 不过据我在这两段实习期间的观察,出现 BigDecimal 精度丢失问题是很常见的现象,因为它的坑实在是太多太多了,稍不留神就背上大锅,基于之前整理的笔记,对 BigDecimal 的底层实现原理以及使用它的注意事项进行刨析, OK!下面进入正题 ~

Snipaste_2024-11-06_09-45-08.png

首先,为什么小数有精度丢失呢?

十进制整数转化为二进制: 整数将被除数每次都除以2,只要除到商为0就可以停止这个过程。永远都不会无限循环

十进制小数转化为二进制: 每次将小数部分乘2,取出整数部分,如果小数部分为0,就可以停止这个过程。但是有概率出现无限循环。

比如0.1,0.1×2=0.2,0.2×2=0.4,0.4×2=0.8,0.8×2=1.6,取出整数部分剩 0.6,0.6×2=1.2,再取出整数部分剩 0.2,这样就又回到之前的步骤,也就是陷入了无限循环 ~

其次,BIgDecimal 为什么可以解决这个问题?

BigDecimal类位于 java.math 包下,用于对超过16位有效位的数进行精确的运算。一般来说,double类型的变量可以处理16位有效数,但实际应用中,如果超过16位,就需要BigDecimal类来操作。

BigDecimal 实际上由两部分组成:

  1. BigInteger:用来表示数字的整数部分和小数部分的整数形式。BigInteger 是一个不可变的类,它可以表示任意大小的整数,包括非常大的数。

  2. scale:表示小数点后的位数。scale 是一个整数,用于指定数字中小数点的位置。如果 scale 为正数,则表示小数点后有几位;如果 scale 为负数,则表示小数点前有几位。

由此可见,BigDecimal在计算时,实际会把数值扩大10的n次倍,变成一个long型整数进行计算,整数计算时自然可以实现精度不丢失。同时结合精度scale,实现最终结果的计算。

最后,那我们再用 Bigdecimal 有哪些注意事项?

1)System.out.println() 中的数字默认是 double 类型的,double 类型小数计算不精准;

2)使用 BigDecimal 类构造方法传入double类型时,计算的结果也是不精确的!可以传 String 或者调用 BIgDecimal.valueof(double)

3)BigDecimal 都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值,或者直接把需要的运算一直运行到底!

4)使用除法函数在 divide 的时候要设置各种参数,要精确的小数位数和舍入模式,不然会出现报错 ~

5)【强制】如上所示 BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法。 说明:equals()方法会比较值和精度(1.0和1.00返回结果为false),而 compareTo() 则会忽略精度。

嗯?那 BIgDecimal 是非使用不可嘛?

其实也不是的,在需要精确的小数计算时再使用 BigDecimal,由于它的底层原理,BigDecimal 的性能比 double 和 float 差很多,尤其是在处理庞大的复杂运算时尤为明显,故一般精度的计算没必要使用 BigDecimal。

但话又说回来,像笔者这两次实习的公司里,涉及钱包、余额扣减计算、借贷等场景,这些不允许出错的场景还是很有必要的!

附:BigDecimal 工具类分享

网上粘的 ~

import java.math.BigDecimal;
import java.math.RoundingMode;
​
/**
 * 简化BigDecimal计算的小工具类
 */
public class BigDecimalUtil {
​
    /**
     * 默认除法运算精度
     */
    private static final int DEF_DIV_SCALE = 10;
​
    private BigDecimalUtil() {
    }
​
    /**
     * 提供精确的加法运算。
     *
     * @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();
    }
​
    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
     * 小数点以后10位,以后的数字四舍五入。
     *
     * @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_EVEN).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);
    }
}

题外:在 mysql 中除了用 decimal 还能用什么数据类型?

NUMERIC数据类型

NUMERIC 数据类型是一种精确数字数据类型,其精度在算术运算后保留到最小有效位,numeric 是标准 sql 的数据类型,格式是 numeric(m,n)。

numeric(a,b) 函数有两个参数,前面一个为总的位数,后面一个参数是小数点后的位数,例如 numeric(5,2) 是总位数为 5,小数点后为 2 位的数,也就是说这个字段的整数位最大是 3 位。