Java随谈(八)货币计算中整型替代 BigDecimal的实战运用

前言

在计算机的世界中,由于操作系统和存储介质的限制,我们对于数字的运用总是充满着警惕。比如float和double类型,浮点运算一直是货币计算的灾难区。

参考阅读

Java随谈(六)我们真的理解 Java 里的整型吗?

正文

比如在著名的《EffectiveJava》书中提到几个经典的例子,判断一下下面的java能输出什么?

// 1.03元减去0.42元
System.out.println(1.03 - 0.42);

答案是0.6100000000000001。
例子还有很多,不需要一一列举,可以注意的是,要进行货币计算, 在 Java 中推荐采用BigDecimal进行。

System.out.println(BigDecimal.valueOf(1.03).subtract(BigDecimal.valueOf(0.42)));

输出为0.61,单位元
这很完美,但是有两个缺点

  1. BigDecimal类相对浮点运算来说不方便使用。
  2. BigDecimal类相对浮点运算来说速度比较慢。

另一种做法是将浮点数改为原始类型的整型计算(更改单位,从元改为分)

System.out.println(103 - 42);

输出为61,单位分
这个改造解决了上述的两个问题,计算简单且高效,但是同时带来了一个代价,整型具有长度限制,int不能超过9位,long不能超过18位。

实战代码

在项目实战中,18位基本满足了运用,下面写一段金额工具类,以便参考。


public class MathUtils {
    /**
     * 保留两位小数的正则表达式
     */
    public static final Pattern NUMBER_PATTERN = Pattern.compile("^\\d+(\\.\\d{2})?$");

    public static final BigDecimal ONE_HUNDRED = new BigDecimal("100");

    /**
     * 校验参数符合保留两位小数的定义
     */
    public static BigDecimal check(String numberStr) {
        Objects.requireNonNull(numberStr, "数字不能空");
        if (!NUMBER_PATTERN.matcher(numberStr).matches()) {
            throw new RuntimeException("该数字有误,请重新输入:" + numberStr);
        }

        return new BigDecimal(numberStr);
    }

    /**
     * 将单位为(元)的数字字符串转化为单位为(分),并进行四舍五入的长整型数字
     * 注:用于接收外界的数字入库,使用String主要基于它的不变性,同时也可以很好的运用正则工具
     */
    public static long parseYuanToFen(String numberStr) {
        BigDecimal number = check(numberStr);

        // 校验金额范围
        BigDecimal result = number.multiply(ONE_HUNDRED).setScale(0, RoundingMode.HALF_UP);
        if (result.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) > 0 ||
                result.compareTo(BigDecimal.valueOf(Long.MIN_VALUE)) < 0) {
            throw new ArithmeticException("金额超出 long 类型表示范围");
        }

        return result.longValue();
    }

    /**
     * 将单位为(分)的数字转化为单位为(元)的数字字符串,保留两位小数
     * 注:用于库中的数字在接口(可能是前端或者其他服务调用方)传递
     */
    public static String parseFenToYuan(Long number) {
        Objects.requireNonNull(number, "数字不能空");
        return BigDecimal.valueOf(number).divide(ONE_HUNDRED, 2, RoundingMode.HALF_UP).toPlainString();
    }

    public static void main(String[] args) {
        // 模拟从外界获取金额(元)
        String number = "1000.05";
        // 通过代码校验后转化为分
        long numberLong = parseYuanToFen(number);

        // 在代码中运用基本类型 long 进行高效计算
        // 配合此方案,数据库中金额字段定义为 BIGINT NOT NULL COMMENT '单位:分',同时也减轻了数据库的压力。
        System.out.format("1000.05(元) = %s(分)", numberLong);
        System.out.println();

        // 模拟从数据库或其他存储介质取出金额字段,直接进行计算操作,最后通过转化函数在接口传递
        Long out = numberLong + 1000L;
        String outString = parseFenToYuan(out);
        System.out.format("%s(分) = %s(元)", out, outString);
    }
}

输出为
1000.05(元) = 100005(分)
101005(分) = 1010.05(元)

参考

《EffectiveJava》

拓展

  1. 你可以拓展上述代码为任意币种最小单位的工具类吗?比如日元(0位小数)、科威特第纳尔(3位小数)等。
posted @ 2026-08-18 20:48  Kwanwooo  阅读(0)  评论(0)    收藏  举报