java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java计算金额

一文浅析Java金额计算用long还是BigDecimal

作者:刘大华

在Java后端开发中处理与钱有关的业务时,确保金额计算的准确性和避免错误非常重要,这篇文章主要给大家介绍了关于Java中处理金额计算之使用Long还是BigDecimal的相关资料,希望对大家有所帮助

前言

最近接触一个新项目,发现系统中所有金额相关字段都使用long类型来表示。

作为一个习惯使用BigDecimal处理金额的开发者,这让我产生了疑惑:这会不会有精度问题?为什么要这样设计?

“用double不行吗?它也能表示小数啊!”

经过一番研究和思考,把最终得出的结论来和大家详细分享一下。

一、double、float类型比较

doublefloat都是Java中的浮点数类型,它们采用IEEE 754标准来表示小数。

这种表示方法类似于科学计数法,但存在一个致命问题:不是所有小数都能精确表示

举一个简单的例子:

double a = 0.1;
double b = 0.2;
double result = a + b;
System.out.println(result); 
// 输出:0.30000000000000004

什么?简单的0.1 + 0.2居然不等于0.3?

是的,这是因为在二进制的计算中,有些十进制小数没有办法精确表示,就像1除以3一样,在十进制中也不能精确表示(0.33333...)。

二、BigDecimal 解决方案

BigDecimal的正确用法

BigDecimal可以解决精度问题,但必须正确使用。

错误的构造方式

BigDecimal wrong1 = new BigDecimal(0.1); 
BigDecimal wrong2 = new BigDecimal(0.2);

这种传入浮点型的构造方式还是会有精度问题。

正确的构造方式

BigDecimal correct1 = new BigDecimal("0.1");
BigDecimal correct2 = new BigDecimal("0.2");
System.out.println("正确方式: " + correct1.add(correct2)); // 0.3

通过传入字符串进行构造。

或者用valueOf(内部也是转字符串)

BigDecimal safe1 = BigDecimal.valueOf(0.1);
BigDecimal safe2 = BigDecimal.valueOf(0.2);
System.out.println("安全方式: " + safe1.add(safe2)); // 0.3

BigDecimal的运算规则

public class BigDecimalOperations {
    public static void main(String[] args) {
        BigDecimal price = new BigDecimal("19.99");
        BigDecimal quantity = new BigDecimal("3");
        BigDecimal taxRate = new BigDecimal("0.13");
        
        // 乘法
        BigDecimal subtotal = price.multiply(quantity);
        
        // 除法必须指定精度和舍入模式
        BigDecimal tax = subtotal.multiply(taxRate)
                               .setScale(2, RoundingMode.HALF_UP);
        
        // 加法
        BigDecimal total = subtotal.add(tax);
        
        System.out.println("小计: " + subtotal); // 59.97
        System.out.println("税金: " + tax);      // 7.80
        System.out.println("总计: " + total);    // 67.77
    }
}

BigDecimal的比较操作

public class BigDecimalComparison {
    public static void main(String[] args) {
        BigDecimal num1 = new BigDecimal("10.00");
        BigDecimal num2 = new BigDecimal("10.000");
        
        // 错误:比较引用
        System.out.println("== 比较: " + (num1 == num2)); // false
        
        // 错误:equals会比较精度
        System.out.println("equals比较: " + num1.equals(num2)); // false
        
        // 正确:compareTo只比较数值
        System.out.println("compareTo比较: " + (num1.compareTo(num2) == 0)); // true
    }
}

BigDecimal的缺点

性能开销:对象创建和运算成本高

内存占用:每个对象需要20-30字节

使用复杂:容易用错构造方法和运算规则

代码冗长:简单的运算也需要多行代码

三、long方案

核心思想:以分为单位存储

使用long表示金额的核心思路是:不以元为单位,而以最小货币单位(分)存储。

public class LongAmountDemo {
    // 工具方法:元转分
    public static long yuanToFen(double yuan) {
        return Math.round(yuan * 100);
    }
    
    // 工具方法:分转元(用于显示)
    public static String fenToYuanDisplay(long fen) {
        return String.format("¥%.2f", fen / 100.0);
    }
    
    // 工具方法:分转元(用于计算)
    public static double fenToYuan(long fen) {
        return fen / 100.0;
    }
    
    public static void main(String[] args) {
        // 以分为单位存储所有金额
        long price = yuanToFen(19.99);  // 1999分 = 19.99元
        long quantity = 3;
        long taxRate = 13;  // 13% = 0.13,这里用整数表示百分比
        
        // 计算过程全部使用整数运算
        long subtotal = price * quantity;                    // 5997分
        long tax = (subtotal * taxRate) / 100;               // 780分
        long total = subtotal + tax;                         // 6777分
        
        System.out.println("小计: " + fenToYuanDisplay(subtotal)); // ¥59.97
        System.out.println("税金: " + fenToYuanDisplay(tax));      // ¥7.80
        System.out.println("总计: " + fenToYuanDisplay(total));    // ¥67.77
    }
}

long方案的优势

1.绝对精确 整数运算在计算机中是精确的,不会出现浮点数的精度问题。

2.性能卓越

// long运算 - 机器指令级别,极快
long a = 1000L, b = 2000L;
long result = a + b;

// BigDecimal运算 - 方法调用,对象创建,较慢
BigDecimal c = new BigDecimal("10.00");
BigDecimal d = new BigDecimal("20.00");
BigDecimal result2 = c.add(d);

3.存储高效

4.序列化简单 在数据库、JSON、网络传输中处理更简单。

实际业务应用

public class OrderService {
    // 订单金额(分)
    private long orderAmount;
    // 优惠金额(分)
    private long discountAmount;
    // 实付金额(分)
    private long actualAmount;
    
    public void calculateOrder(long unitPrice, int quantity, long discountRate) {
        // 计算订单金额
        orderAmount = unitPrice * quantity;
        
        // 计算优惠金额
        discountAmount = (orderAmount * discountRate) / 100;
        
        // 计算实付金额
        actualAmount = orderAmount - discountAmount;
    }
    
    // 显示方法
    public String getDisplayAmount() {
        return String.format("订单金额: %s, 优惠: %s, 实付: %s", 
            formatFen(orderAmount),
            formatFen(discountAmount),
            formatFen(actualAmount));
    }
    
    private String formatFen(long fen) {
        return String.format("¥%.2f", fen / 100.0);
    }
}

四、各种方案的适用场景

double/float:绝对不要用于金额

BigDecimal:复杂金融计算

long:大多数业务系统

五、BigDecimal的最佳实践

如果确实需要使用BigDecimal,请遵循以下规则:

1. 构造方法

// 推荐
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = BigDecimal.valueOf(0.1);  // 内部转字符串

// 避免
BigDecimal c = new BigDecimal(0.1);      // 精度问题

2. 运算控制

BigDecimal num1 = new BigDecimal("10.00");
BigDecimal num2 = new BigDecimal("3.00");

// 除法必须指定精度
BigDecimal result = num1.divide(num2, 4, RoundingMode.HALF_UP);

// 乘法建议控制精度
BigDecimal product = num1.multiply(num2).setScale(2, RoundingMode.HALF_UP);

3. 数值比较

BigDecimal amount1 = new BigDecimal("100.00");
BigDecimal amount2 = new BigDecimal("100.000");

// 正确
if (amount1.compareTo(amount2) == 0) {
    // 数值相等
}

// 错误
if (amount1.equals(amount2)) {
    // 不会执行,因为精度不同
}

六、long方案的实施建议

1. 建立工具类

public class MoneyUtils {
    private MoneyUtils() {} // 工具类,防止实例化
    
    // 元转分
    public static long yuanToFen(double yuan) {
        return Math.round(yuan * 100);
    }
    
    // 分转元(显示用)
    public static String fenToDisplayYuan(long fen) {
        return String.format("¥%.2f", fen / 100.0);
    }
    
    // 分转元(计算用)
    public static double fenToYuan(long fen) {
        return fen / 100.0;
    }
    
    // 金额加法(防止溢出)
    public static long add(long amount1, long amount2) {
        return Math.addExact(amount1, amount2);
    }
    
    // 金额乘法
    public static long multiply(long amount, int multiplier) {
        return Math.multiplyExact(amount, multiplier);
    }
}

2. 数据库设计

-- 使用bigint存储金额(分)
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    order_amount BIGINT COMMENT '订单金额(分)',
    discount_amount BIGINT COMMENT '优惠金额(分)',
    actual_amount BIGINT COMMENT '实付金额(分)'
);

3. API设计

// 请求和响应中使用Long类型表示金额(分)
public class OrderRequest {
    private Long productId;
    private Integer quantity;
    private Long unitPrice;  // 单价(分)
}

public class OrderResponse {
    private String orderNo;
    private Long totalAmount;  // 总金额(分)
    private String displayAmount; // 显示金额 "¥99.99"
}

总结与选择

为什么那个项目选择long?

现在我可以理解那个项目的设计思路了:

如何选择

场景推荐方案理由
简单业务系统long性能好,简单可靠
银行金融系统BigDecimal精度要求极高
高并发交易long性能至关重要
复杂税 务计算BigDecimal需要复杂小数运算
新项目启动long技术债务少

技术选型从来都不是单一的,理解业务的需求,能选择出最适合的技术方案就是最好的方案。

以上就是一文浅析Java金额计算用long还是BigDecimal的详细内容,更多关于Java计算金额的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文