Java随谈(八)货币计算中整型替代 BigDecimal的实战运用
前言
在计算机的世界中,由于操作系统和存储介质的限制,我们对于数字的运用总是充满着警惕。比如float和double类型,浮点运算一直是货币计算的灾难区。
参考阅读
正文
比如在著名的《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,单位元
这很完美,但是有两个缺点
- BigDecimal类相对浮点运算来说不方便使用。
- 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》
拓展
- 你可以拓展上述代码为任意币种最小单位的工具类吗?比如日元(0位小数)、科威特第纳尔(3位小数)等。

浙公网安备 33010602011771号