这篇带你深入浅出BigDecimal

张开发
2026/4/21 4:20:22 15 分钟阅读

分享文章

这篇带你深入浅出BigDecimal
背景一直从事金融相关项目所以对BigDecimal再熟悉不过了也曾看到很多同学因为不知道、不了解或使用不当导致资损事件发生。所以如果你从事金融关项目 或者你的项目中涉及到金额的计算那么你一定要花时间看看这篇文章全面学习一下BigDecimal。BigDecimal概述Java在java.math包中提供的API类BigDecimal用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数但在实际应用中可能需要对更大或者更小的数进行运算和处理。一般情况下对于不需要准确计算精度的数字可以直接使用Float和Double处理但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以如果需要精确计算的结果则必须使用BigDecimal类来操作。BigDecimal对象提供了传统的、-、*、/等算术运算符对应的方法通过这些方法进行相应的操作。BigDecimal都是不可变的immutable的 在进行每一次四则运算时都会产生一个新的对象 所以在做加减乘除运算时要记得要保存操作后的值。BigDecimal的4个坑在使用BigDecimal时有4种使用场景下的坑你一定要了解一下如果使用不当必定很惨。掌握这些案例当别人写出有坑的代码你也能够一眼识别出来大牛就是这么练成的。第一浮点类型的坑在学习了解BigDecimal的坑之前先来说一个老生常谈的问题如果使用Float、Double等浮点类型进行计算时有可能得到的是一个近似值而不是精确的值。比如下面的代码Test public void test0(){ float a 1; float b 0.9f; System.out.println(a - b); }结果是多少0.1吗不是执行上面代码执行的结果是0.100000024。之所以产生这样的结果是因为0.1的二进制表示是无限循环的。由于计算机的资源是有限的所以是没办法用二进制精确的表示 0.1只能用「近似值」来表示就是在有限的精度情况下最大化接近 0.1 的二进制数于是就会造成精度缺失的情况。关于上述的现象大家都知道不再详细展开。同时还会得出结论在科学计数法时可考虑使用浮点类型但如果是涉及到金额计算要使用BigDecimal来计算。那么BigDecimal就一定能避免上述的浮点问题吗来看下面的示例Test public void test1(){ BigDecimal a new BigDecimal(0.01); BigDecimal b BigDecimal.valueOf(0.01); System.out.println(a a); System.out.println(b b); }上述单元测试中的代码a和b结果分别是什么a 0.01000000000000000020816681711721685132943093776702880859375 b 0.01上面的实例说明即便是使用BigDecimal结果依旧会出现精度问题。这就涉及到创建BigDecimal对象时如果有初始值是采用new BigDecimal的形式还是通过BigDecimal#valueOf方法了。之所以会出现上述现象是因为new BigDecimal时传入的0.1已经是浮点类型了鉴于上面说的这个值只是近似值在使用new BigDecimal时就把这个近似值完整的保留下来了。而BigDecimal#valueOf则不同它的源码实现如下public static BigDecimal valueOf(double val) { // Reminder: a zero double returns 0.0, so we cannot fastpath // to use the constant ZERO. This might be important enough to // justify a factory approach, a cache, or a few private // constants, later. return new BigDecimal(Double.toString(val)); }在valueOf内部使用Double#toString方法将浮点类型的值转换成了字符串因此就不存在精度丢失问题了。此时就得出一个基本的结论第一在使用BigDecimal构造函数时尽量传递字符串而非浮点类型第二如果无法满足第一条则可采用BigDecimal#valueOf方法来构造初始化值。这里延伸一下BigDecimal常见的构造方法有如下几种BigDecimal(int) 创建一个具有参数所指定整数值的对象。 BigDecimal(double) 创建一个具有参数所指定双精度值的对象。 BigDecimal(long) 创建一个具有参数所指定长整数值的对象。 BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象。其中涉及到参数类型为double的构造方法会出现上述的问题使用时需特别留意。第二浮点精度的坑如果比较两个BigDecimal的值是否相等你会如何比较使用equals方法还是compareTo方法呢先来看一个示例Test public void test2(){ BigDecimal a new BigDecimal(0.01); BigDecimal b new BigDecimal(0.010); System.out.println(a.equals(b)); System.out.println(a.compareTo(b)); }乍一看感觉可能相等但实际上它们的本质并不相同。equals方法是基于BigDecimal实现的equals方法来进行比较的直观印象就是比较两个对象是否相同那么代码是如何实现的呢Override public boolean equals(Object x) { if (!(x instanceof BigDecimal)) return false; BigDecimal xDec (BigDecimal) x; if (x this) return true; if (scale ! xDec.scale) return 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()); }仔细阅读代码可以看出equals方法不仅比较了值是否相等还比较了精度是否相同。上述示例中由于两者的精度不同所以equals方法的结果当然是false了。而compareTo方法实现了Comparable接口真正比较的是值的大小返回的值为-1小于0等于1大于。基本结论通常情况如果比较两个BigDecimal值的大小采用其实现的compareTo方法如果严格限制精度的比较那么则可考虑使用equals方法。另外这种场景在比较0值的时候比较常见比如比较BigDecimal(0)、BigDecimal(0.0)、BigDecimal(0.00)此时一定要使用compareTo方法进行比较。第三设置精度的坑在项目中看到好多同学通过BigDecimal进行计算时不设置计算结果的精度和舍入模式真是着急人虽然大多数情况下不会出现什么问题。但下面的场景就不一定了Test public void test3(){ BigDecimal a new BigDecimal(1.0); BigDecimal b new BigDecimal(3.0); a.divide(b); }执行上述代码的结果是什么ArithmeticException异常java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. at java.math.BigDecimal.divide(BigDecimal.java:1690) ...这个异常的发生在官方文档中也有说明If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.总结一下就是如果在除法divide运算过程中如果商是一个无限小数0.333…而操作的结果预期是一个精确的数字那么将会抛出ArithmeticException异常。此时只需在使用divide方法时指定结果的精度即可Test public void test3(){ BigDecimal a new BigDecimal(1.0); BigDecimal b new BigDecimal(3.0); BigDecimal c a.divide(b, 2,RoundingMode.HALF_UP); System.out.println(c); }执行上述代码输入结果为0.33。基本结论在使用BigDecimal进行所有运算时一定要明确指定精度和舍入模式。拓展一下舍入模式定义在RoundingMode枚举类中共有8种RoundingMode.UP舍入远离零的舍入模式。在丢弃非零部分之前始终增加数字(始终对非零舍弃部分前面的数字加1)。注意此舍入模式始终不会减少计算值的大小。RoundingMode.DOWN接近零的舍入模式。在丢弃某部分之前始终不增加数字(从不对舍弃部分前面的数字加1即截短)。注意此舍入模式始终不会增加计算值的大小。RoundingMode.CEILING接近正无穷大的舍入模式。如果 BigDecimal 为正则舍入行为与 ROUNDUP 相同;如果为负则舍入行为与 ROUNDDOWN 相同。注意此舍入模式始终不会减少计算值。RoundingMode.FLOOR接近负无穷大的舍入模式。如果 BigDecimal 为正则舍入行为与 ROUNDDOWN 相同;如果为负则舍入行为与 ROUNDUP 相同。注意此舍入模式始终不会增加计算值。RoundingMode.HALF_UP向“最接近的”数字舍入如果与两个相邻数字的距离相等则为向上舍入的舍入模式。如果舍弃部分 0.5则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。注意这是我们在小学时学过的舍入模式(四舍五入)。RoundingMode.HALF_DOWN向“最接近的”数字舍入如果与两个相邻数字的距离相等则为上舍入的舍入模式。如果舍弃部分 0.5则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同(五舍六入)。RoundingMode.HALF_EVEN向“最接近的”数字舍入如果与两个相邻数字的距离相等则向相邻的偶数舍入。如果舍弃部分左边的数字为奇数则舍入行为与 ROUNDHALFUP 相同;如果为偶数则舍入行为与 ROUNDHALF_DOWN 相同。注意在重复进行一系列计算时此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”主要在美国使用。四舍六入五分两种情况。如果前一位为奇数则入位否则舍去。以下例子为保留小数点1位那么这种舍入方式下的结果。1.15 1.2 ,1.25 1.2RoundingMode.UNNECESSARY断言请求的操作具有精确的结果因此不需要舍入。如果对获得精确结果的操作指定此舍入模式则抛出ArithmeticException。通常我们使用的四舍五入即RoundingMode.HALF_UP。第四三种字符串输出的坑当使用BigDecimal之后需要转换成String类型你是如何操作的直接toString先来看看下面的代码Test public void test4(){ BigDecimal a BigDecimal.valueOf(35634535255456719.22345634534124578902); System.out.println(a.toString()); }执行的结果是上述对应的值吗并不是3.563453525545672E16也就是说本来想打印字符串的结果打印出来的是科学计数法的值。这里我们需要了解BigDecimal转换字符串的三个方法toPlainString()不使用任何科学计数法toString()在必要的时候使用科学计数法toEngineeringString() 在必要的时候使用工程计数法。类似于科学计数法只不过指数的幂都是3的倍数这样方便工程上的应用因为在很多单位转换的时候都是10^3三种方法展示结果示例如下计算法基本结论**根据数据结果展示格式不同采用不同的字符串输出方法通常使用比较多的方法为toPlainString()**。另外NumberFormat类的format()方法可以使用BigDecimal对象作为其参数可以利用BigDecimal对超出16位有效数字的货币值百分值以及一般数值进行格式化控制。使用示例如下NumberFormat currency NumberFormat.getCurrencyInstance(); //建立货币格式化引用 NumberFormat percent NumberFormat.getPercentInstance(); //建立百分比格式化引用 percent.setMaximumFractionDigits(3); //百分比小数点最多3位 BigDecimal loanAmount new BigDecimal(15000.48); //金额 BigDecimal interestRate new BigDecimal(0.008); //利率 BigDecimal interest loanAmount.multiply(interestRate); //相乘 System.out.println(金额:\t currency.format(loanAmount)); System.out.println(利率:\t percent.format(interestRate)); System.out.println(利息:\t currency.format(interest));输出结果如下金额: 15,000.48 利率: 0.8% 利息: 120.00小结本篇文章介绍了BigDecimal使用中场景的坑以及基于这些坑我们得出的“最佳实践”。虽然某些场景下推荐使用BigDecimal它能够达到更好的精度但性能相较于double和float还是有一定的损失的特别在处理庞大复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal。而必须使用时一定要规避上述的坑。

更多文章