杰瑞科技汇

BigDecimal如何精准处理小数运算?

BigDecimal 是 Java 中用于精确表示和计算十进制数的类,它专门为需要高精度计算的财务、金融等场景设计。floatdouble 是二进制浮点数,不适合用于精确的十进制计算,因为它们在表示某些小数时会产生舍入误差。

BigDecimal如何精准处理小数运算?-图1
(图片来源网络,侵删)

为什么需要 BigDecimal

我们来看一个经典的例子,说明 floatdouble 的问题:

public class DoubleProblem {
    public static void main(String[] args) {
        double a = 0.1;
        double b = 0.2;
        System.out.println(a + b); // 预期输出 0.3,但实际输出 0.30000000000000004
    }
}

这个结果就是二进制浮点数精度问题的体现,对于金额、利率等场景,这种微小的误差是绝对不能接受的。

BigDecimal 通过将数字表示为一个“未缩放的整数”和一个“小数点偏移量”(scale)来解决这个精度问题。

  • = unscaledValue × 10^(-scale)
  • 456 可以表示为 unscaledValue=123456scale=3

BigDecimal 的核心操作

创建 BigDecimal 对象

错误且常见的方式:

BigDecimal如何精准处理小数运算?-图2
(图片来源网络,侵删)
// 错误!0.1这个double值本身就不精确,然后传给BigDecimal
BigDecimal bd1 = new BigDecimal(0.1); 
System.out.println(bd1); // 输出可能是 0.1000000000000000055511151231257827021181583404541015625

正确的方式:使用 String 构造方法

这是最推荐、最精确的方式,因为它直接解析十进制字符串。

BigDecimal bdFromString = new BigDecimal("0.1");
System.out.println(bdFromString); // 输出 0.1

其他推荐方式:使用 valueOf

BigDecimal.valueOf() 是一个静态工厂方法,它内部会先将 double 转换为 String,然后再创建 BigDecimal 对象,这比直接使用 double 构造要精确得多。

BigDecimal如何精准处理小数运算?-图3
(图片来源网络,侵删)
BigDecimal bdFromValueOf = BigDecimal.valueOf(0.1);
System.out.println(bdFromValueOf); // 输出 0.1
  • 首选 BigDecimal.valueOf(double)new BigDecimal(String)
  • 绝对避免 new BigDecimal(double)

四则运算

BigDecimal 的所有运算都不会改变原始对象,而是返回一个新的 BigDecimal 对象,你需要用这个新对象来接收结果。

BigDecimal a = new BigDecimal("10.5");
BigDecimal b = new BigDecimal("4.2");
// 加法
BigDecimal sum = a.add(b); // 14.7
// 减法
BigDecimal difference = a.subtract(b); // 6.3
// 乘法
BigDecimal product = a.multiply(b); // 44.1
// 除法 (最复杂,见下一节)

除法 - 精度控制

BigDecimal 的除法是精度管理的核心,由于两个数相除可能得到无限循环小数(如 10/3),你必须指定如何处理这种情况。

方法签名: BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode)

  • divisor: 除数
  • scale: 小数点后保留的位数
  • roundingMode: 舍入模式

常见的舍入模式:

  • RoundingMode.HALF_UP: “四舍五入”(最常用)。
  • RoundingMode.UP: 远离零方向舍入(正数向上,负数向下)。
  • RoundingMode.DOWN: 向零方向舍入(截断)。
  • RoundingMode.CEILING: 向正无穷方向舍入。
  • RoundingMode.FLOOR: 向负无穷方向舍入。

示例:

BigDecimal dividend = new BigDecimal("10");
BigDecimal divisor = new BigDecimal("3");
// 保留2位小数,四舍五入
BigDecimal result1 = dividend.divide(divisor, 2, RoundingMode.HALF_UP);
System.out.println(result1); // 输出 3.33
// 保留4位小数,四舍五入
BigDecimal result2 = dividend.divide(divisor, 4, RoundingMode.HALF_UP);
System.out.println(result2); // 输出 3.3333
// 保留0位小数,向上舍入
BigDecimal result3 = dividend.divide(divisor, 0, RoundingMode.UP);
System.out.println(result3); // 输出 4

异常情况: 如果除不尽,且你没有指定舍入模式,会抛出 ArithmeticException

// 这行代码会抛出异常!
// BigDecimal error = dividend.divide(divisor); 

比较大小

不要使用 equals() 方法!

BigDecimal.equals() 不仅比较数值,还会比较 scale(小数位数),这在业务逻辑中通常不是我们想要的。

BigDecimal a = new BigDecimal("1.00");
BigDecimal b = new BigDecimal("1.0");
System.out.println(a.equals(b)); // 输出 false,因为 a.scale()=2, b.scale()=1

正确的方法:使用 compareTo()

compareTo() 只比较数值大小,返回 -1, 0, 或 1。

  • a.compareTo(b) < 0 => a < b
  • a.compareTo(b) == 0 => a == b
  • a.compareTo(b) > 0 => a > b
BigDecimal a = new BigDecimal("1.00");
BigDecimal b = new BigDecimal("1.0");
System.out.println(a.compareTo(b)); // 输出 0,表示数值相等

设置和获取精度

  • setScale(int newScale, RoundingMode roundingMode): 设置小数位数,并指定舍入模式。这会返回一个新的 BigDecimal 对象
  • setScale(int newScale): 设置小数位数,使用默认的舍入模式(RoundingMode.UNNECESSARY,表示不允许舍入,如果精度不够会抛出异常)。
  • int scale(): 获取当前的小数位数。
  • int precision(): 获取总的数字位数(不包括符号和小数点)。
BigDecimal price = new BigDecimal("123.4567");
// 设置为2位小数,四舍五入
BigDecimal roundedPrice = price.setScale(2, RoundingMode.HALF_UP);
System.out.println(roundedPrice); // 123.46
System.out.println(roundedPrice.scale()); // 2
System.out.println(roundedPrice.precision()); // 5 (1,2,3,4,6)

完整示例:购物车结算

这是一个综合运用 BigDecimal 的典型场景。

import java.math.BigDecimal;
import java.math.RoundingMode;
public class ShoppingCart {
    public static void main(String[] args) {
        // 商品单价
        BigDecimal itemPrice1 = new BigDecimal("19.99");
        BigDecimal itemPrice2 = new BigDecimal("5.99");
        BigDecimal itemPrice3 = new BigDecimal("12.49");
        // 商品数量
        int quantity1 = 2;
        int quantity2 = 1;
        int quantity3 = 3;
        // 计算小计 (价格 * 数量)
        // 注意:不能直接用 int * BigDecimal,需要先将 int 转为 BigDecimal
        BigDecimal subtotal1 = itemPrice1.multiply(BigDecimal.valueOf(quantity1));
        BigDecimal subtotal2 = itemPrice2.multiply(BigDecimal.valueOf(quantity2));
        BigDecimal subtotal3 = itemPrice3.multiply(BigDecimal.valueOf(quantity3));
        // 计算总价
        BigDecimal total = subtotal1.add(subtotal2).add(subtotal3);
        // 假设折扣是 10%
        BigDecimal discountRate = new BigDecimal("0.10");
        BigDecimal discountAmount = total.multiply(discountRate);
        // 计算折扣后价格,保留2位小数
        BigDecimal discountedTotal = total.subtract(discountAmount).setScale(2, RoundingMode.HALF_UP);
        // 假设税率是 8%
        BigDecimal taxRate = new BigDecimal("0.08");
        BigDecimal taxAmount = discountedTotal.multiply(taxRate).setScale(2, RoundingMode.HALF_UP);
        // 计算最终应付金额
        BigDecimal finalTotal = discountedTotal.add(taxAmount);
        // 输出结果
        System.out.println("商品1小计: $" + subtotal1);
        System.out.println("商品2小计: $" + subtotal2);
        System.out.println("商品3小计: $" + subtotal3);
        System.out.println("商品总价: $" + total);
        System.out.println("折扣金额: -$" + discountAmount.setScale(2, RoundingMode.HALF_UP));
        System.out.println
分享:
扫描分享到社交APP
上一篇
下一篇