为什么需要精确计算?

浮点数(float/double)在计算机中采用二进制表示,无法精确表达所有十进制小数(如 0.1),导致计算误差。在金融、科学计算等领域需要精确结果时,必须使用特殊技术。


核心解决方案

1. BigDecimal 类(推荐方案)

import java.math.BigDecimal;
import java.math.RoundingMode;

public class PreciseCalculation {
    public static void main(String[] args) {
        // 创建BigDecimal对象(必须使用字符串构造器)
        BigDecimal a = new BigDecimal("0.1");
        BigDecimal b = new BigDecimal("0.2");
        
        // 基本运算
        BigDecimal sum = a.add(b);           // 0.3
        BigDecimal diff = a.subtract(b);      // -0.1
        BigDecimal product = a.multiply(b);   // 0.02
        
        // 除法(必须指定精度和舍入模式)
        BigDecimal quotient = new BigDecimal("10")
            .divide(new BigDecimal("3"), 10, RoundingMode.HALF_UP); // 3.3333333333
        
        // 精度控制
        BigDecimal result = new BigDecimal("2.34567")
            .setScale(2, RoundingMode.HALF_UP); // 2.35
        
        // 比较操作(不能用equals!)
        boolean equal = (a.compareTo(b) == 0);
    }
}

关键特性:

  • 不可变对象:每次操作返回新对象
  • 任意精度:支持高精度小数运算
  • 完全控制:可指定舍入模式(RoundingMode)
  • 提供丰富API:加减乘除、取余、乘方等

2. BigInteger 类(大整数计算)

import java.math.BigInteger;

public class BigIntExample {
    public static void main(String[] args) {
        BigInteger big1 = new BigInteger("123456789012345678901234567890");
        BigInteger big2 = new BigInteger("987654321098765432109876543210");
        
        // 大数运算
        BigInteger gcd = big1.gcd(big2);                // 最大公约数
        BigInteger modPow = big1.modPow(big2, new BigInteger("1000000007")); // 模幂
        BigInteger factorial = calculateFactorial(100); // 100的阶乘
    }
    
    private static BigInteger calculateFactorial(int n) {
        BigInteger result = BigInteger.ONE;
        for (int i = 2; i <= n; i++) {
            result = result.multiply(BigInteger.valueOf(i));
        }
        return result;
    }
}

3. 整数定点表示法(性能优化)

// 以分为单位存储金额(避免小数)
class Money {
    private final long cents; // 1元 = 100分
    
    public Money(String yuan) {
        this.cents = Long.parseLong(yuan.replace(".", ""));
    }
    
    public Money add(Money other) {
        return new Money(this.cents + other.cents);
    }
    
    public String toString() {
        return cents / 100 + "." + String.format("%02d", cents % 100);
    }
}

关键技术与最佳实践

1. 正确初始化 BigDecimal

// ✅ 正确方式(字符串构造)
BigDecimal correct = new BigDecimal("0.1");

// ❌ 错误方式(浮点构造)
BigDecimal wrong = new BigDecimal(0.1); 
// 实际值:0.1000000000000000055511151231257827021181583404541015625

2. 精确比较策略

BigDecimal a = new BigDecimal("1.00");
BigDecimal b = new BigDecimal("1.0");

// 错误:考虑精度和值 (1.00 ≠ 1.0)
boolean eq1 = a.equals(b); // false

// 正确:仅比较数值 (1.00 = 1.0)
boolean eq2 = (a.compareTo(b) == 0; // true

3. 除法安全处理

// ❌ 危险操作(无限小数导致异常)
BigDecimal danger = new BigDecimal("1").divide(new BigDecimal("3"));

// ✅ 安全做法(指定精度)
BigDecimal safe = new BigDecimal("1").divide(
    new BigDecimal("3"), 
    10,                    // 精度:10位小数
    RoundingMode.HALF_UP   // 舍入模式:四舍五入
);

4. 性能优化技巧

// 缓存常用值
private static final BigDecimal HUNDRED = new BigDecimal("100");

// 批量计算重用对象
BigDecimal total = BigDecimal.ZERO;
for (Transaction t : transactions) {
    total = total.add(t.getAmount()); // 避免在循环中创建新对象
}

// 使用不可变对象特性(线程安全)
public class Calculator {
    private final BigDecimal base;
    public Calculator(BigDecimal base) {
        this.base = base; // 安全共享
    }
}

5. 舍入模式选择

模式 描述 示例 (2.5) 示例 (-2.5)
RoundingMode.UP 远离零方向舍入 3 -3
RoundingMode.DOWN 向零方向舍入 2 -2
RoundingMode.CEILING 向正无穷方向舍入 3 -2
RoundingMode.FLOOR 向负无穷方向舍入 2 -3
RoundingMode.HALF_UP 四舍五入 3 -3
RoundingMode.HALF_EVEN 银行家舍入法 2 -2

常见错误及解决方案

  1. 浮点数构造陷阱

    // 错误
    double d = 0.1;
    BigDecimal bd = new BigDecimal(d);
    
    // 正确
    BigDecimal bd = BigDecimal.valueOf(d); // 内部调用Double.toString()
    BigDecimal bd = new BigDecimal(Double.toString(d));
    
  2. 忽略不可变性

    // 错误:认为原对象被修改
    BigDecimal a = new BigDecimal("1.23");
    a.add(new BigDecimal("0.77")); // a仍然是1.23
    
    // 正确:接收返回值
    a = a.add(new BigDecimal("0.77")); // 2.00
    
  3. 精度丢失问题

    // 错误:未指定精度
    BigDecimal result = a.divide(b);
    
    // 正确:明确精度要求
    BigDecimal result = a.divide(b, 6, RoundingMode.HALF_UP);
    
  4. 性能敏感场景滥用

    // 高频交易场景优化
    // 原始方案:BigDecimal计算
    BigDecimal total = BigDecimal.ZERO;
    for (...) {
        total = total.add(item.getValue());
    }
    
    // 优化方案:分计算(整数运算)
    long totalCents = 0;
    for (...) {
        totalCents += item.getCents();
    }
    

精确计算策略选择指南

场景 推荐方案 优势 注意事项
金融计算(金额) 整数定点法 高性能,避免小数问题 需处理单位转换
科学计算(高精度) BigDecimal 任意精度,精确控制 内存消耗大,速度慢
超大整数运算 BigInteger 支持任意大整数 不适合小数运算
简单小数运算(<10位) 放大为整数计算 性能接近原生类型 需处理缩放因子
统计计算(可容错) double + 误差控制 性能最优 需允许微小误差

性能对比(纳秒/操作)

操作类型 double BigDecimal 整数定点法
加法 1 50 2
乘法 2 100 3
除法 10 200 5
复杂函数 20 500+ 不支持

总结:精确计算最佳实践

  1. 构造原则
    ✅ 使用字符串创建 BigDecimal
    ❌ 禁用浮点数构造函数

  2. 运算规范

    • 除法必须指定精度和舍入模式
    • 使用 compareTo() 进行数值比较
    • 复用不可变对象减少开销
  3. 性能优化

    • 高频场景使用整数定点法
    • 批量计算预存中间值
    • 精度要求不高时使用放大整数法
  4. 取舍策略

    graph TD
        A[需要精确计算] --> B{精度要求}
        B -->|任意精度| C[BigDecimal]
        B -->|固定小数位| D[整数定点法]
        B -->|超大整数| E[BigInteger]
        A --> F{性能要求}
        F -->|极高| G[放大整数法]
        F -->|可接受| C
    
  5. 行业应用

    • 金融系统:金额使用整数分/厘存储
    • 科学计算:BigDecimal + 指定精度
    • 区块链:BigInteger 处理加密大数
    • 交易系统:性能敏感部分用整数运算