确保数值计算精度:BigDecimal 使用指南与最佳实践

86 阅读3分钟

适用于金融计算、高精度运算等对数值准确性要求高的场景。


📌 一、常见问题点

使用 BigDecimal(double) 构造函数 ❌

// 错误示范
BigDecimal bd = new BigDecimal(0.1); 
System.out.println(bd); // 输出: 0.1000000000000000055511151231257827021181583404541015625

推荐做法

BigDecimal bd1 = new BigDecimal("0.1");          // 安全
BigDecimal bd2 = BigDecimal.valueOf(0.1);        // 内部转字符串,也安全

除法未指定舍入模式导致异常 ❌

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
a.divide(b); // Non-terminating decimal expansion; no exact representable decimal result.

推荐做法

BigDecimal result = a.divide(b, 4, RoundingMode.HALF_UP);
// 或
BigDecimal result2 = a.divide(b, new MathContext(10, RoundingMode.HALF_UP));

📌 建议:所有除法操作都显式指定精度和舍入方式。


equals() 比较包含 scale(小数位数)❌

new BigDecimal("1.0").equals(new BigDecimal("1")); // false

正确比较数值是否相等

new BigDecimal("1.0").compareTo(new BigDecimal("1")) == 0; // true

📌 规则总结

  • 数值相等 → compareTo() == 0
  • 完全相同(含 scale)→ equals()

hashCode()equals() 不一致(因 scale 不同)❌

Set<BigDecimal> set = new HashSet<>();
set.add(new BigDecimal("1.0"));
set.add(new BigDecimal("1")); // 被视为两个不同元素!
System.out.println(set.size()); // 输出: 2

解决方案:统一格式后再放入集合

BigDecimal normalized = bd.stripTrailingZeros();
set.add(normalized);

stripTrailingZeros() 可能返回科学计数法 ❌

BigDecimal bd = new BigDecimal("100");
System.out.println(bd.stripTrailingZeros().toString()); // 输出: 1E+2

输出标准十进制格式

System.out.println(bd.stripTrailingZeros().toPlainString()); // 输出: 100

性能问题:频繁创建对象

  • BigDecimal 是不可变类,每次运算都生成新对象。
  • 高频循环中可能造成 GC 压力。

优化建议

  • 缓存常用常量:BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN
  • 避免不必要的中间变量

setScale() 不改变原对象 ❌

BigDecimal bd = new BigDecimal("1.234");
bd.setScale(2, RoundingMode.HALF_UP); // 无效!
System.out.println(bd); // 仍是 1.234

必须重新赋值

bd = bd.setScale(2, RoundingMode.HALF_UP);

默认舍入模式选择需谨慎

舍入模式说明
HALF_UP四舍五入(最常用)
HALF_EVEN银行家舍入(减少累积误差,适合金融)

根据业务需求选择,不要盲目使用默认。


📌 二、toString() vs toPlainString() 对比

方法行为适用场景
toString()可能使用科学计数法(如 1.23E-7日志、调试
toPlainString()始终返回普通十进制格式显示、存储、序列化

示例对比:

BigDecimal small = new BigDecimal("0.000000123");
System.out.println(small.toString());        // 1.23E-7
System.out.println(small.toPlainString());   // 0.000000123

BigDecimal trailing = new BigDecimal("100.00");
System.out.println(trailing.toString());        // 100.00
System.out.println(trailing.toPlainString());   // 100.00

BigDecimal large = new BigDecimal("1E+10");
System.out.println(large.toString());        // 1E+10
System.out.println(large.toPlainString());   // 10000000000

建议:对外输出(如 JSON、UI、数据库)一律使用 toPlainString()


📌 三、完整测试代码(验证所有问题点)

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.HashSet;
import java.util.Set;

public class BigDecimalPitfallsTest {

    public static void main(String[] args) {
        System.out.println("=== 1. 构造函数陷阱 ===");
        testConstructor();

        System.out.println("\n=== 2. 除法异常 ===");
        testDivision();

        System.out.println("\n=== 3. equals vs compareTo ===");
        testEqualsVsCompareTo();

        System.out.println("\n=== 4. Set 中重复问题 ===");
        testSetBehavior();

        System.out.println("\n=== 5. stripTrailingZeros 与 toPlainString ===");
        testStripAndToString();

        System.out.println("\n=== 6. setScale 必须重新赋值 ===");
        testSetScale();

        System.out.println("\n=== 7. toString vs toPlainString ===");
        testToStringFormats();
    }

    private static void testConstructor() {
        BigDecimal bad = new BigDecimal(0.1);
        BigDecimal good1 = new BigDecimal("0.1");
        BigDecimal good2 = BigDecimal.valueOf(0.1);
        System.out.println("new BigDecimal(0.1): " + bad);
        System.out.println("new BigDecimal(\"0.1\"): " + good1);
        System.out.println("BigDecimal.valueOf(0.1): " + good2);
    }

    private static void testDivision() {
        BigDecimal a = new BigDecimal("1");
        BigDecimal b = new BigDecimal("3");
        try {
            a.divide(b);
        } catch (ArithmeticException e) {
            System.out.println("除法异常: " + e.getMessage());
        }
        BigDecimal safe = a.divide(b, 6, RoundingMode.HALF_UP);
        System.out.println("安全除法结果: " + safe);
    }

    private static void testEqualsVsCompareTo() {
        BigDecimal x = new BigDecimal("1.0");
        BigDecimal y = new BigDecimal("1");
        System.out.println("x.equals(y): " + x.equals(y)); // false
        System.out.println("x.compareTo(y) == 0: " + (x.compareTo(y) == 0)); // true
    }

    private static void testSetBehavior() {
        Set<BigDecimal> set = new HashSet<>();
        set.add(new BigDecimal("1.0"));
        set.add(new BigDecimal("1"));
        System.out.println("Set 大小(未标准化): " + set.size()); // 2

        Set<BigDecimal> normalizedSet = new HashSet<>();
        normalizedSet.add(new BigDecimal("1.0").stripTrailingZeros());
        normalizedSet.add(new BigDecimal("1").stripTrailingZeros());
        System.out.println("Set 大小(标准化后): " + normalizedSet.size()); // 1
    }

    private static void testStripAndToString() {
        BigDecimal bd = new BigDecimal("100");
        System.out.println("stripTrailingZeros().toString(): " + bd.stripTrailingZeros().toString());
        System.out.println("stripTrailingZeros().toPlainString(): " + bd.stripTrailingZeros().toPlainString());
    }

    private static void testSetScale() {
        BigDecimal bd = new BigDecimal("1.234");
        bd.setScale(2, RoundingMode.HALF_UP); // 无效果
        System.out.println("未重新赋值: " + bd); // 1.234

        bd = bd.setScale(2, RoundingMode.HALF_UP);
        System.out.println("重新赋值后: " + bd); // 1.23
    }

    private static void testToStringFormats() {
        BigDecimal[] cases = {
            new BigDecimal("0.000000123"),
            new BigDecimal("100.00"),
            new BigDecimal("12345678901234567890")
        };
        for (BigDecimal bd : cases) {
            System.out.println("原始: " + bd);
            System.out.println("  toString():        " + bd.toString());
            System.out.println("  toPlainString():  " + bd.toPlainString());
            System.out.println();
        }
    }
}

📌 四、最佳实践总结表

场景推荐做法
构造优先使用 new BigDecimal("xxx")BigDecimal.valueOf(xxx)
比较数值使用 compareTo() == 0
判断完全相等使用 equals()(含 scale)
除法运算总是指定精度和 RoundingMode
集合存储先调用 .stripTrailingZeros() 统一格式
字符串输出使用 .toPlainString() 避免科学计数法
修改值记住 BigDecimal 不可变,必须重新赋值
舍入策略根据业务选 HALF_UP(通用)或 HALF_EVEN(金融)
性能优化缓存常量,避免高频创建