杰瑞科技汇

Java中BigDecimal如何避免精度丢失?

BigDecimal 是 Java 中用于精确表示和计算十进制数的类,它位于 java.math 包下。为什么需要它? 因为 Java 中基本类型 floatdouble,以及它们的包装类 FloatDouble,都是基于二进制浮点数实现的,这在处理十进制数时会产生精度丢失问题。

Java中BigDecimal如何避免精度丢失?-图1
(图片来源网络,侵删)

为什么 floatdouble 不够用?

计算机底层使用二进制(0和1)来表示所有数据,十进制小数(如 1)在转换为二进制时,是一个无限循环小数。

示例:

public class DoubleProblem {
    public static void main(String[] args) {
        // 期望结果是 0.3
        double a = 0.1;
        double b = 0.2;
        System.out.println(a + b); // 输出: 0.30000000000000004
        // 期望结果是 1.0
        double c = 1.00;
        double d = 0.99;
        System.out.println(c - d); // 输出: 0.010000000000000009
    }
}

从上面的例子可以看出,简单的加减法都可能导致精度错误,这在金融、会计等对精度要求极高的领域是绝对不能接受的。

BigDecimal 通过将数字表示为“未缩放整数”和“缩放因子”(即小数点位置)的虚拟组合,完美地解决了这个问题。1 会被存储为 110(即 1 * 10^-1),2 会被存储为 210,这样就能精确表示。

Java中BigDecimal如何避免精度丢失?-图2
(图片来源网络,侵删)

BigDecimal 的核心构造方法

创建 BigDecimal 实例时,强烈建议使用 String 参数的构造方法,以避免精度问题。

✅ 推荐的构造方法

// 1. 使用 String 构造,这是最精确、最安全的方式
BigDecimal bd1 = new BigDecimal("0.1");
BigDecimal bd2 = new BigDecimal("123456789.987654321");
// 2. 使用 int 或 long 构造
BigDecimal bd3 = new BigDecimal(123); // 整数
BigDecimal bd4 = new BigDecimal(123L); // 长整数

❌ 避免使用的构造方法

// 错误示范!不要使用 double 构造!
// 0.1 这个 double 本身就不精确,所以传给 BigDecimal 的也是不精确的值
BigDecimal wrongBd1 = new BigDecimal(0.1); // 内部存储的不是 0.1,而是 0.1000000000000000055511151231257827021181583404541015625
BigDecimal wrongBd2 = new BigDecimal(0.2); // 同理
System.out.println(wrongBd1.add(wrongBd2)); // 输出不是精确的 0.3

其他创建方式:valueOf()

BigDecimal 类提供了一个静态工厂方法 valueOf(),它是创建 BigDecimal 实例的推荐方式之一,因为它内部做了优化。

// 使用 valueOf() 方法
// 对于 double 类型,它会先将 double 转换为 String,再构造 BigDecimal
// 这比 new BigDecimal(double) 要精确得多
BigDecimal safeBd1 = BigDecimal.valueOf(0.1);
BigDecimal safeBd2 = BigDecimal.valueOf(0.2);
System.out.println(safeBd1.add(safeBd2)); // 输出: 0.3
  • 首选BigDecimal.valueOf(double)new BigDecimal(String)
  • 次选new BigDecimal(int/long)
  • 避免new BigDecimal(double)

常用方法

BigDecimal 提供了丰富的算术运算、比较和格式化方法。

算术运算

所有算术运算方法都会返回一个新的 BigDecimal 对象,它们不会修改原始对象BigDecimal 是不可变的)。

方法 描述 示例
add(BigDecimal augend) 加法 bd1.add(bd2)
subtract(BigDecimal subtrahend) 减法 bd1.subtract(bd2)
multiply(BigDecimal multiplicand) 乘法 bd1.multiply(bd2)
divide(BigDecimal divisor) 除法 bd1.divide(bd2)
divide(BigDecimal divisor, int scale, RoundingMode roundingMode) 除法(指定精度和舍入模式) bd1.divide(bd2, 2, RoundingMode.HALF_UP)

除法特别注意: 普通的 divide 方法可能会抛出 ArithmeticException,因为除不尽(如 1/3),通常需要指定保留小数位数舍入模式

BigDecimal num1 = new BigDecimal("1");
BigDecimal num2 = new BigDecimal("3");
// 错误示范,会抛出异常 ArithmeticException
// num1.divide(num2);
// 正确示范:保留2位小数,四舍五入
BigDecimal result = num1.divide(num2, 2, RoundingMode.HALF_UP);
System.out.println(result); // 输出: 0.33

比较方法

方法 描述
compareTo(BigDecimal val) 比较两个值的大小,返回 -1 (小于), 0 (等于), 1 (大于)。
equals(Object x) 比较两个 BigDecimal 的值和精度是否都相等。new BigDecimal("1.00").equals(new BigDecimal("1")) 返回 false
intValue(), longValue(), doubleValue() 转换为基本类型,注意可能丢失精度
BigDecimal bdA = new BigDecimal("10.00");
BigDecimal bdB = new BigDecimal("10.0");
BigDecimal bdC = new BigDecimal("9");
System.out.println(bdA.compareTo(bdB)); // 输出: 0 (值相等)
System.out.println(bdA.equals(bdB));     // 输出: false (精度不同)
System.out.println(bdA.compareTo(bdC)); // 输出: 1
System.out.println(bdC.compareTo(bdA)); // 输出: -1

精度和舍入方法

方法 描述
setScale(int scale, RoundingMode roundingMode) 设置小数位数,并指定舍入模式,这是最常用的舍入方法。
setScale(int scale) 设置小数位数,使用默认的舍入模式(RoundingMode.UNNECESSARY,表示不允许舍入,否则报错)。
round(MathContext mc) 根据指定的精度和舍入模式进行舍入。
BigDecimal price = new BigDecimal("99.899");
// 保留2位小数,四舍五入
BigDecimal roundedPrice = price.setScale(2, RoundingMode.HALF_UP);
System.out.println(roundedPrice); // 输出: 99.90
// 保留1位小数,向上取整(进一法)
BigDecimal roundedPriceUp = price.setScale(1, RoundingMode.CEILING);
System.out.println(roundedPriceUp); // 输出: 99.9

舍入模式

BigDecimal 的舍入行为由 java.math.RoundingMode 枚举定义,最常用的是 HALF_UP(四舍五入)。

舍入模式 描述
UP 远离零方向舍入。绝对值变大。 (5.5 -> 6, -5.5 -> -6)
DOWN 向零方向舍入。绝对值变小。 (5.5 -> 5, -5.5 -> -5)
CEILING 向正无穷方向舍入。 (5.5 -> 6, -5.5 -> -5)
FLOOR 向负无穷方向舍入。 (5.5 -> 5, -5.5 -> -6)
HALF_UP 四舍五入 (最常用)。 (5.5 -> 6, 5.4 -> 5)
HALF_DOWN 五舍六入。 (5.5 -> 5, 5.6 -> 6)
HALF_EVEN 银行家舍入法,如果舍弃部分的前一位是奇数,则向上舍入;如果是偶数,则向下舍入,这是最精确、最常用的舍入策略,可以最大程度地减少累积误差。 (5.5 -> 6, 6.5 -> 6, 2.5 -> 2)

最佳实践

  1. 永远不要使用 doublefloat 来构造 BigDecimal

    // 错误
    BigDecimal price = new BigDecimal(99.99);
    // 正确
    BigDecimal price = new BigDecimal("99.99");
    // 或者
    BigDecimal price = BigDecimal.valueOf(99.99);
  2. 总是为除法操作指定舍入模式,否则程序可能会在运行时崩溃。

    // 错误
    // result = a.divide(b);
    // 正确
    result = a.divide(b, 2, RoundingMode.HALF_UP);
  3. 使用 compareTo() 进行比较,而不是 equals()

    • equals() 会比较值和精度(小数位数)。
    • compareTo() 只比较值的大小,这在业务逻辑中(如比较价格)通常才是我们想要的。
  4. 在循环中进行累加时,优先使用 add 方法,而不是 。

    // 错误示范:将 BigDecimal 转为 double 进行计算,会丢失精度
    double sum = 0.0;
    for (int i = 0; i < 10; i++) {
        sum += 0.1;
    }
    System.out.println(sum); // 输出不是 1.0
    // 正确示范:使用 BigDecimal 的 add 方法
    BigDecimal bdSum = BigDecimal.ZERO;
    for (int i = 0; i < 10; i++) {
        bdSum = bdSum.add(BigDecimal.valueOf(0.1));
    }
    System.out.println(bdSum); // 输出 1.0

完整示例

下面是一个模拟购物车计算总价的完整示例,涵盖了 BigDecimal 的主要用法。

import java.math.BigDecimal;
import java.math.RoundingMode;
public class BigDecimalExample {
    public static void main(String[] args) {
        // 商品列表:名称、单价、数量
        String[][] products = {
                {"苹果", "5.99", "2"},
                {"香蕉", "3.49", "1.5"}, // 1.5公斤
                {"牛奶", "12.50", "1"}
        };
        // 1. 初始化总金额,使用 String 构造确保初始值精确
        BigDecimal totalAmount = new BigDecimal("0.00");
        // 2. 遍历商品列表,计算小计并累加到总金额
        for (String[] product : products) {
            String name = product[0];
            BigDecimal price = new BigDecimal(product[1]);
            BigDecimal quantity = new BigDecimal(product[2]);
            // 计算单项小计
            BigDecimal subtotal = price.multiply(quantity);
            System.out.printf("商品: %s, 小计: %s%n", name, subtotal);
            // 累加到总金额
            totalAmount = totalAmount.add(subtotal);
        }
        // 3. 计算税金(假设税率为 8%)
        BigDecimal taxRate = new BigDecimal("0.08");
        BigDecimal tax = totalAmount.multiply(taxRate).setScale(2, RoundingMode.HALF_UP);
        System.out.printf("税金: %s%n", tax);
        // 4. 计算最终应付金额(总金额 + 税金)
        BigDecimal finalAmount = totalAmount.add(tax);
        // 5. 格式化输出,保留两位小数
        System.out.println("------------------------");
        System.out.printf("商品总额: %s%n", totalAmount.setScale(2, RoundingMode.HALF_UP));
        System.out.printf("应付税金: %s%n", tax);
        System.out.printf("应付总额: %s%n", finalAmount.setScale(2, RoundingMode.HALF_UP));
    }
}

输出结果:

商品: 苹果, 小计: 11.98
商品: 香蕉, 小计: 5.235
商品: 牛奶, 小计: 12.5
税金: 2.38
------------------------
商品总额: 29.72
应付税金: 2.38
应付总额: 32.10

BigDecimal 是 Java 进行精确十进制计算的基石,虽然它比基本类型使用起来更繁琐(需要创建新对象、处理舍入等),但在金融、电商、科学计算等任何不能容忍精度错误的场景下,它是唯一正确的选择,掌握它的构造、运算、比较和舍入规则,是每个 Java 开发者的必备技能。

分享:
扫描分享到社交APP
上一篇
下一篇