文章目录
- 前言
-
- [金融场景下BigDecimal 运算规范 + 常用场景使用 + 数据库字段设计详解](#金融场景下BigDecimal 运算规范 + 常用场景使用 + 数据库字段设计详解)
-
- [1. 数据库方面](#1. 数据库方面)
- [2. VO 接受](#2. VO 接受)
- [3. 原生BigDecimal的核心痛点](#3. 原生BigDecimal的核心痛点)
- [4. BigDecimal 与 BigInteger 的区别说明](#4. BigDecimal 与 BigInteger 的区别说明)
- [5. 常用的使用方法](#5. 常用的使用方法)
-
- [5.1. 金额转换](#5.1. 金额转换)
- [5.2. 金额运算](#5.2. 金额运算)
- [5.3. 金额批量聚合](#5.3. 金额批量聚合)
- [5.4. 金额比较](#5.4. 金额比较)
- [5.5. 金额判断](#5.5. 金额判断)
- [5.6. 金额数值工具](#5.6. 金额数值工具)
- [5.7. 金额格式化](#5.7. 金额格式化)
- [5.8. 计算百分比](#5.8. 计算百分比)
- [5.9. 判断金额是否有效](#5.9. 判断金额是否有效)
前言
如果您觉得有用的话,记得给博主点个赞,评论,收藏一键三连啊,写作不易啊^ _ ^。
而且听说点赞的人每天的运气都不会太差,实在白嫖的话,那欢迎常来啊!!!
金融场景下BigDecimal 运算规范 + 常用场景使用 + 数据库字段设计详解

1. 数据库方面
只要是跟钱、精度有关 ,一律用 DECIMAL:
DECIMAL = 高精度、无误差的小数类型。
格式:
bash
DECIMAL(M,D)
* M = 总共多少位数字
* D = 小数点后保留几位
专门存:钱、金额、财务数据。
- 通用金额(最常见):DECIMAL (10,2) 整数 8 位、小数 2 位:最大 99,999,999.99(近 1 亿)
- 一般企业 / 电商:DECIMAL (12,2) 整数 10 位:最大 9,999,999,999.99(近 100 亿)
- 超大金额 / 财务汇总:DECIMAL (17,2) 或 DECIMAL (18,2) 整数 15/16 位:999...999.99(万亿级别)
2. VO 接受
使用BigDecimal,BigDecimal 是金额唯一标准类型。
bash
public class OrderVO {
// 数据库 DECIMAL(17,2) → VO 用 BigDecimal
private BigDecimal amount;
}
json样式示例:
bash
{
"amount": 123456789012345.67
}
3. 原生BigDecimal的核心痛点
- equals比较失效问题
new BigDecimal("1.0").equals(new BigDecimal("1.00")) → false
原因:equals 会比较数值+小数位数,业务上数值相等即可,必须用 compareTo。 - 舍入模式不统一,资损风险极大
金融场景默认需要四舍五入(HALF_UP),但代币场景必须向下取整(DOWN),防止多给资产。
项目中如果到处乱写舍入模式,会出现:对账不平、用户多提币、手续费计算错误等严重问题。 - DecimalFormat线程不安全
全局复用 DecimalFormat 会导致高并发下格式化错乱、数字异常,绝大多数开源工具类都没解决这个问题。
注意的是直接使用 double 存金额,会出现浮点精度丢失。
4. BigDecimal 与 BigInteger 的区别说明
BigDecimal:适用于需要高精度的小数计算的场景,常用于法币金额的计算;
BigInteger:适用于需要处理非常大的整数的场景,常用于原始金额(如 Wei、Satoshi 等)的计算;
5. 常用的使用方法
5.1. 金额转换
1、String 转换 BigDecimal;
java
public static BigDecimal of(String value) {
// 检查 value 不能为 null,然后去掉前后空格
String clean = Objects.requireNonNull(value, "金额不能为空").trim();
if (clean.isEmpty()) {
throw new IllegalArgumentException("金额不能为空字符串");
}
// 移除逗号,准备转换为 BigDecimal
clean = clean.replace(",", "");
// 禁止科学计数法输入,避免精度问题和意外结果
// 检查金额里有没有出现科学计数法(比如 1E6、1.23e8)
if (clean.contains("E") || clean.contains("e")) {
throw new IllegalArgumentException("金额不支持科学计数法: " + value);
}
try {
return new BigDecimal(clean);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("非法的金额格式: " + value, e);
}
}
2、long 转换 BigDecimal;
java
public static BigDecimal of(long value) {
return BigDecimal.valueOf(value);
}
3、double 转换 BigDecimal;
java
public static BigDecimal of(double value) {
return new BigDecimal(Double.toString(value));
}
5.2. 金额运算
java
private static void checkNotNull(BigDecimal a, BigDecimal b) {
// 检查参数 a 不能为 null
Objects.requireNonNull(a, "参数 a 不能为空");
// 检查参数 b 不能为 null
Objects.requireNonNull(b, "参数 b 不能为空");
}
1、加法;
java
public static BigDecimal add(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.add(b);
}
2、减法;
java
public static BigDecimal subtract(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.subtract(b);
}
3、乘法;
java
public static BigDecimal multiply(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.multiply(b);
}
4、除法;
java
public static BigDecimal divide(
BigDecimal a,
BigDecimal b,
int scale,
RoundingMode roundingMode) {
checkNotNull(a, b);
Objects.requireNonNull(roundingMode, "舍入模式不能为空");
if (scale < 0) {
throw new IllegalArgumentException("scale 不能小于 0");
}
if (eqZero(b)) {
throw new IllegalArgumentException("除数不能为 0");
}
return a.divide(b, scale, roundingMode);
}
5、乘法(舍入);
java
public static BigDecimal multiply(
BigDecimal a,
BigDecimal b,
int scale,
RoundingMode roundingMode) {
checkNotNull(a, b);
Objects.requireNonNull(roundingMode, "舍入模式不能为空");
if (scale < 0) {
throw new IllegalArgumentException("scale 不能小于 0");
}
// 先乘法后舍入,避免中间结果过大导致的精度问题
return a.multiply(b).setScale(scale, roundingMode);
}
6、除法(舍入);
java
public static BigDecimal divideOrZero(
BigDecimal a,
BigDecimal b,
int scale,
RoundingMode roundingMode) {
checkNotNull(a, b);
if (eqZero(b)) {
return BigDecimal.ZERO;
}
return divide(a, b, scale, roundingMode);
}
5.3. 金额批量聚合
java
public static BigDecimal sum(Collection<BigDecimal> values) {
if (values == null || values.isEmpty()) {
return BigDecimal.ZERO;
}
// 使用流式 API 进行聚合,增加 null 过滤,避免潜在的 NullPointerException
return values.stream()
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
5.4. 金额比较
1、相等比较方法;
java
public static boolean eq(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.compareTo(b) == 0;
}
2、大于比较方法;
java
public static boolean gt(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.compareTo(b) > 0;
}
3、大于等于比较方法;
java
public static boolean ge(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.compareTo(b) >= 0;
}
4、小于比较方法;
java
public static boolean lt(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.compareTo(b) < 0;
}
5、小于等于比较方法;
java
public static boolean le(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.compareTo(b) <= 0;
}
5.5. 金额判断
1、判断金额是否大于 0;
java
public static boolean gtZero(BigDecimal amount) {
Objects.requireNonNull(amount, "金额不能为空");
// 使用 compareTo 而不是 equals 来比较 BigDecimal 是否大于 0,避免 scale 不同导致的比较失败
return amount.compareTo(BigDecimal.ZERO) > 0;
}
2、判断金额是否大于等于 0;
java
public static boolean geZero(BigDecimal amount) {
Objects.requireNonNull(amount, "金额不能为空");
// 使用 compareTo 而不是 equals 来比较 BigDecimal 是否大于等于 0,避免 scale 不同导致的比较失败
return amount.compareTo(BigDecimal.ZERO) >= 0;
}
3、判断金额是否等于 0;
java
public static boolean eqZero(BigDecimal amount) {
Objects.requireNonNull(amount, "金额不能为空");
// 使用 compareTo 而不是 equals 来比较 BigDecimal 是否等于 0,避免 scale 不同导致的比较失败
return amount.compareTo(BigDecimal.ZERO) == 0;
}
4、判断金额是否小于 0;
java
public static boolean ltZero(BigDecimal amount) {
Objects.requireNonNull(amount, "金额不能为空");
// 使用 compareTo 而不是 equals 来比较 BigDecimal 是否小于 0,避免 scale 不同导致的比较失败
return amount.compareTo(BigDecimal.ZERO) < 0;
}
5.6. 金额数值工具
1、获取两个金额中的较大值
场景说明:
获取两个金额中的较大值,注意 BigDecimal 的 max 方法会返回两个数中较大的一个。
注意它不会修改原有的 BigDecimal 对象,而是返回一个新的对象。
java
public static BigDecimal max(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.max(b);
}
2、获取两个金额中的较小值
场景说明:
获取两个金额中的较小值,注意 BigDecimal 的 min 方法会返回两个数中较小的一个。
注意它不会修改原有的 BigDecimal 对象,而是返回一个新的对象。
java
public static BigDecimal min(BigDecimal a, BigDecimal b) {
checkNotNull(a, b);
return a.min(b);
}
3、获取金额的绝对值
场景说明:
获取金额的绝对值,注意 BigDecimal 的 abs 方法会返回金额的绝对值。
注意它不会修改原有的 BigDecimal 对象,而是返回一个新的对象。
java
public static BigDecimal abs(BigDecimal amount) {
Objects.requireNonNull(amount, "金额不能为空");
return amount.abs();
}
4、获取金额的相反数
场景说明:
获取金额的相反数,注意 BigDecimal 的 negate 方法会返回金额的相反数。
注意它不会修改原有的 BigDecimal 对象,而是返回一个新的对象。
java
public static BigDecimal negate(BigDecimal amount) {
Objects.requireNonNull(amount, "金额不能为空");
return amount.negate();
}
5、将金额限制在一个范围内
场景说明:
将金额限制在一个范围内,如果金额小于最小值,则返回最小值。
如果金额大于最大值,则返回最大值;否则返回金额本身。
java
public static BigDecimal clamp(BigDecimal value, BigDecimal min, BigDecimal max) {
Objects.requireNonNull(value, "值不能为空");
Objects.requireNonNull(min, "最小值不能为空");
Objects.requireNonNull(max, "最大值不能为空");
// 先检查 min 和 max 的关系,避免逻辑错误导致的异常情况
// 如果 min 大于 max,说明参数传递有误,抛出 IllegalArgumentException 异常,提示调用方检查参数
if (gt(min, max)) {
throw new IllegalArgumentException("min 不能大于 max");
}
// 进行范围限制,如果 value 小于 min,则返回 min;如果 value 大于 max,则返回 max;否则返回 value 本身
if (lt(value, min)) {
return min;
}
// 进行范围限制,如果 value 大于 max,则返回 max;否则返回 value 本身
if (gt(value, max)) {
return max;
}
// 如果 value 在 min 和 max 之间,则直接返回 value 本身
return value;
}
6,对金额进行舍入(自定义)
场景说明:
对金额进行舍入,注意 BigDecimal 的 setScale 方法会返回一个新对象(不会修改原有的 BigDecimal 对象)。
例如:对金额进行舍入,或者对金额进行小数位调整等场景,通常需要使用 setScale 方法来实现。
java
public static BigDecimal roundToken(
BigDecimal amount,
int decimals,
RoundingMode roundingMode) {
Objects.requireNonNull(amount, "金额不能为空");
Objects.requireNonNull(roundingMode, "舍入模式不能为空");
// 对金额进行舍入,保留 decimals 位小数,并使用 roundingMode 来处理四舍五入
if (decimals < 0) {
throw new IllegalArgumentException("decimals 不能小于 0");
}
// 对金额进行舍入,保留 decimals 位小数,并使用 roundingMode 来处理四舍五入
return amount.setScale(decimals, roundingMode);
}
5.7. 金额格式化
1、格式化金额为字符串(含货币符号)
场景说明:
格式化金额为字符串,自动添加货币符号,使用对应货币的默认小数位和舍入模式,简化调用。
java
/**
* DecimalFormat 线程本地缓存
* 作用:避免重复创建 DecimalFormat 实例,提升性能;确保线程安全
*/
private static final ThreadLocal<DecimalFormatHelper> DECIMAL_FORMAT_CACHE =
ThreadLocal.withInitial(DecimalFormatHelper::new);
/**
* 法币类型枚举
* 说明:
* - 小数位数:不同货币可能有不同的小数位要求
* - 货币符号:可用于格式化输出
* - 舍入模式:不同货币可能有不同的舍入需求,默认使用 HALF_UP
*/
public enum Currency {
// 人民币, 2位小数 , 默认舍入模式 HALF_UP
CNY(2, "¥", RoundingMode.HALF_UP),
// 美元, 2位小数, 默认舍入模式 HALF_UP
USD(2, "$", RoundingMode.HALF_UP),
// 欧元, 2位小数, 默认舍入模式 HALF_UP
EUR(2, "€", RoundingMode.HALF_UP),
// 日元, 0位小数(不使用小数部分), 默认舍入模式 HALF_UP
JPY(0, "¥", RoundingMode.HALF_UP);
private final int scale;
private final String symbol;
private final RoundingMode rounding;
Currency(int scale, String symbol, RoundingMode rounding) {
this.scale = scale;
this.symbol = symbol;
this.rounding = rounding;
}
public int getScale() {
return scale;
}
public String getSymbol() {
return symbol;
}
public RoundingMode getRounding() {
return rounding;
}
}
public static String formatMoney(BigDecimal amount, Currency currency) {
Objects.requireNonNull(amount, "金额不能为空");
Objects.requireNonNull(currency, "货币类型不能为空");
// 对金额进行舍入,保留 currency.getScale() 位小数
// 并使用 currency.getRounding() 来处理四舍五入
BigDecimal rounded = roundMoney(amount, currency);
// 从 ThreadLocal 获取 DecimalFormatHelper 实例,避免重复创建 DecimalFormat 对象,提升性能
DecimalFormatHelper helper = DECIMAL_FORMAT_CACHE.get();
try {
// 配置 DecimalFormat,根据货币的要求设置小数位数、舍入模式和分组使用,确保格式化输出符合预期
DecimalFormat df = helper.getFormat();
// 强制设置小数位数,确保与货币规则一致(最小/最大小数位相同)
df.setMinimumFractionDigits(currency.getScale());
df.setMaximumFractionDigits(currency.getScale());
// 设置舍入模式,与货币定义保持统一
df.setRoundingMode(currency.getRounding());
// 开启千分位分组,符合财务金额展示习惯
df.setGroupingUsed(true);
// 返回格式化后的字符串,包含货币符号和金额,确保输出符合预期的格式
return currency.getSymbol() + df.format(rounded);
} finally {
DECIMAL_FORMAT_CACHE.remove(); // ✅ 关键:移除 ThreadLocal,避免内存泄漏
}
}
2、格式化金额为字符串(不含货币符号)
场景说明:
格式化金额为字符串,不添加货币符号,使用对应货币的默认小数位和舍入模式,简化调用。
例如:formatMoneyPlain(123.456, Currency.USD) 会返回 "123.46"
formatMoneyPlain(123.456, Currency.JPY) 会返回 "123"
formatMoneyPlain(123.456, Currency.CNY) 会返回 "123.46"
java
/**
* DecimalFormat 线程本地缓存
* 作用:避免重复创建 DecimalFormat 实例,提升性能;确保线程安全
*/
private static final ThreadLocal<DecimalFormatHelper> DECIMAL_FORMAT_CACHE =
ThreadLocal.withInitial(DecimalFormatHelper::new);
/**
* 法币类型枚举
* 说明:
* - 小数位数:不同货币可能有不同的小数位要求
* - 货币符号:可用于格式化输出
* - 舍入模式:不同货币可能有不同的舍入需求,默认使用 HALF_UP
*/
public enum Currency {
// 人民币, 2位小数 , 默认舍入模式 HALF_UP
CNY(2, "¥", RoundingMode.HALF_UP),
// 美元, 2位小数, 默认舍入模式 HALF_UP
USD(2, "$", RoundingMode.HALF_UP),
// 欧元, 2位小数, 默认舍入模式 HALF_UP
EUR(2, "€", RoundingMode.HALF_UP),
// 日元, 0位小数(不使用小数部分), 默认舍入模式 HALF_UP
JPY(0, "¥", RoundingMode.HALF_UP);
private final int scale;
private final String symbol;
private final RoundingMode rounding;
Currency(int scale, String symbol, RoundingMode rounding) {
this.scale = scale;
this.symbol = symbol;
this.rounding = rounding;
}
public int getScale() {
return scale;
}
public String getSymbol() {
return symbol;
}
public RoundingMode getRounding() {
return rounding;
}
}
/**
*
* @param amount 金额
* @param currency 货币类型
* @return 格式化后的字符串
*/
public static String formatMoneyPlain(BigDecimal amount, Currency currency) {
Objects.requireNonNull(amount, "金额不能为空");
Objects.requireNonNull(currency, "货币类型不能为空");
BigDecimal rounded = roundMoney(amount, currency);
DecimalFormatHelper helper = DECIMAL_FORMAT_CACHE.get();
try {
DecimalFormat df = helper.getFormat();
df.setMinimumFractionDigits(currency.getScale());
df.setMaximumFractionDigits(currency.getScale());
df.setRoundingMode(currency.getRounding());
df.setGroupingUsed(true);
return df.format(rounded);
} finally {
DECIMAL_FORMAT_CACHE.remove(); // ✅ 关键:移除 ThreadLocal
}
}
5.8. 计算百分比
场景说明:
计算百分比,自动处理分母为零的情况,避免调用方需要额外的 null 检查和异常处理。
例如:计算分子占分母的百分比,或者计算一个金额占另一个金额的百分比等场景。
java
public static boolean eqZero(BigDecimal amount) {
Objects.requireNonNull(amount, "金额不能为空");
// 使用 compareTo 而不是 equals 来比较 BigDecimal 是否等于 0,避免 scale 不同导致的比较失败
return amount.compareTo(BigDecimal.ZERO) == 0;
}
/**
* 计算百分比
* 实现说明:
* return ratio.multiply(BigDecimal.valueOf(100)).setScale(scale, DEFAULT_ROUND)
* 表示计算百分比,先计算分子与分母的比值,保留 scale + 2 位小数以避免过早舍入导致的精度问题
* 然后乘以 100 得到百分比值,最后再舍入到指定的 scale 位小数,使用默认的舍入模式
* return BigDecimal.ZERO 表示如果分母为零,直接返回 0%,避免抛出异常,简化调用方的错误处理逻辑
*
* 例子:
* percent(123.456, 2, 2) 会返回 "62.50%"
* percent(123.456, 0, 2) 会返回 "0.00%",避免分母为零导致的异常
*
* @param numerator 分子
* @param denominator 分母
* @param scale 结果小数位
* @return 百分比值
*/
public static BigDecimal percent(
BigDecimal numerator,
BigDecimal denominator,
int scale) {
Objects.requireNonNull(numerator, "分子不能为空");
Objects.requireNonNull(denominator, "分母不能为空");
if (eqZero(denominator)) {
return BigDecimal.ZERO;
}
// 计算百分比,先计算分子与分母的比值,保留 scale + 2 位小数以避免过早舍入导致的精度问题
// 然后乘以 100 得到百分比值,最后再舍入到指定的 scale 位小数,使用默认的舍入模式
BigDecimal ratio = divide(numerator, denominator, scale + 2, DEFAULT_ROUND);
return ratio.multiply(BigDecimal.valueOf(100)).setScale(scale, DEFAULT_ROUND);
}
5.9. 判断金额是否有效
验证规则:
- 金额不能为 null
- 金额必须为正数或零
- 金额的小数位数不能超过货币规定的小数位数
java
/**
* 法币类型枚举
* 说明:
* - 小数位数:不同货币可能有不同的小数位要求
* - 货币符号:可用于格式化输出
* - 舍入模式:不同货币可能有不同的舍入需求,默认使用 HALF_UP
*/
public enum Currency {
// 人民币, 2位小数 , 默认舍入模式 HALF_UP
CNY(2, "¥", RoundingMode.HALF_UP),
// 美元, 2位小数, 默认舍入模式 HALF_UP
USD(2, "$", RoundingMode.HALF_UP),
// 欧元, 2位小数, 默认舍入模式 HALF_UP
EUR(2, "€", RoundingMode.HALF_UP),
// 日元, 0位小数(不使用小数部分), 默认舍入模式 HALF_UP
JPY(0, "¥", RoundingMode.HALF_UP);
private final int scale;
private final String symbol;
private final RoundingMode rounding;
Currency(int scale, String symbol, RoundingMode rounding) {
this.scale = scale;
this.symbol = symbol;
this.rounding = rounding;
}
public int getScale() {
return scale;
}
public String getSymbol() {
return symbol;
}
public RoundingMode getRounding() {
return rounding;
}
}
/**
* 判断金额是否为负数(小于 0)
* 场景说明:
* 判断金额是否为负数,用于验证金额输入是否合法
* 例如:检查是否为退款、负调整等负数金额场景
*
* @param amount 金额
* @return 如果 amount < 0 则返回 true,否则返回 false
*/
public static boolean isNegative(BigDecimal amount) {
Objects.requireNonNull(amount, "金额不能为空");
return ltZero(amount);
}
/**
* 判断金额是否有效(符合法币规则)
* 场景说明:
* 判断金额是否有效,包括检查小数位数是否符合货币规则
* 例如:检查 USD 金额是否只有 2 位小数、JPY 是否有小数部分等
*
*
* @param amount 金额
* @param currency 货币类型
* @return 如果金额有效则返回 true,否则返回 false
*/
public static boolean isValidCurrency(BigDecimal amount, Currency currency) {
if (amount == null) {
return false;
}
if (currency == null) {
return false;
}
// 检查金额是否为非负数
if (isNegative(amount)) {
return false;
}
// 检查金额的小数位数是否符合货币规则
// 获取金额的 scale(小数位数)
int amountScale = amount.scale();
int currencyScale = currency.getScale();
// 小数位数不能超过货币规定的小数位数
// 例如:USD 规定 2 位小数,金额不能有 3 位或以上小数
if (amountScale > currencyScale) {
return false;
}
return true;
}