java中存储金额字段
商场或者电商项目中会涉及到金额字段的存储,本文来讨论下选择最多的BigDecimal保证精度的原理以及其他字段类型替换方案
BigDemical
BigDecimal
是处理高精度数值计算的核心类,底层实现通过 整数未缩放值 和标度的组合来保证精确的十进制数值表示。
底层数据结构
BigDecimal
的数值由两个关键部分组成:
- 未缩放值(
unscaledValue
)- 类型为
BigInteger
,表示所有有效数字(不含小数点)。 - 例如:
123.45
→unscaledValue = 12345
。
- 类型为
- 标度(
scale
)- 类型为
int
,表示小数点后的位数(即数值 =unscaledValue × 10^-scale
)。 - 例如:
123.45
→scale = 2
。
- 类型为
若标度为负数,表示小数点前补零的数量:
- 例如:
12345
(scale = -3
)→12345 × 10^3 = 12,345,000
。
保证精度的原理
- 基于十进制的精准存储
BigDecimal
直接存储十进制数字的每一位,而非二进制浮点数的近似值(如double
的 IEEE 754 格式)。- 例如:
0.1
在BigDecimal
中被精确存储为unscaledValue=1
、scale=1
,而double
会存储为0.10000000000000000555...
。
- 运算时保留所有中间精度
- 所有算术运算(如加、减、乘、除)均基于
unscaledValue
和scale
的精确操作,不会丢失有效数字。 - 例如:加法会先对齐标度(扩展数值到相同
scale
),再进行整数运算。
-
显式控制舍入行为
- 对于除法用户必须指定舍入模式(
RoundingMode
) 和 精度(小数位数),避免隐式截断。 - 例如:
10 ÷ 3 = 3.333...
,但通过divide(scale, RoundingMode)
可得到3.33
(scale=2
)。
- 对于除法用户必须指定舍入模式(
基于特殊结构的运算
1. 加法
-
对齐标度:将两个操作数的
scale
调整为相同值(取较大值),扩展unscaledValue
。 -
整数相加:对扩展后的
unscaledValue
执行加法。BigDecimal a = new BigDecimal("1.23"); // unscaled=123, scale=2 BigDecimal b = new BigDecimal("4.5"); // unscaled=45, scale=1 // 对齐标度到 2 → b 扩展为 450 (scale=2) // 123 + 450 = 573 → 5.73
2. 乘法
-
直接相乘:两个
unscaledValue
相乘,标度相加。BigDecimal a = new BigDecimal("1.2"); // unscaled=12, scale=1 BigDecimal b = new BigDecimal("0.3"); // unscaled=3, scale=1 // unscaled=12 * 3=36, scale=1+1=2 → 0.36
3. 除法
-
扩展分子:通过增加
unscaledValue
的标度,避免精度丢失。 -
整数除法:使用
BigInteger
的除法算法,结合舍入模式截断余数。BigDecimal a = new BigDecimal("10"); // unscaled=10, scale=0 BigDecimal b = new BigDecimal("3"); // unscaled=3, scale=0 // 10 ÷ 3 → 标度扩展为 2 → 1000 ÷ 3 = 333 (余1) // 应用 RoundingMode.HALF_UP → 3.33
使用时注意的点
1. 构造方法
-
需要传入字符串而不是浮点数
new BigDecimal("0.1") BigDecimal.valueOf(0.1)//底层调用 Double.toString()
2. 未指定舍入模式的除法
-
错误写法:
a.divide(b)
- 若结果为无限小数(如
1/3
),会抛出ArithmeticException
。
- 若结果为无限小数(如
-
正确写法:
a.divide(b, scale, RoundingMode.HALF_UP)
- 显式指定精度和舍入模式。
3. 比较操作的陷阱
-
错误写法:
a.equals(b)
- 同时检查
unscaledValue
和scale
,2.0
和2.00
不相等。
- 同时检查
-
正确写法:
a.compareTo(b) == 0
- 仅比较数值大小,忽略标度差异。
Long(BigInteger)
将金额转换为最小货币单位(比如 分),然后用整数进行存储,这样就可以避免浮点数运算导致的精度缺失,比如
-
1.23元–>123分
-
100.50元–>10050分
long amountInCents = 123; // 1.23 元 // 转换为字符串显示 String display = String.format("%d.%02d", amountInCents / 100, amountInCents % 100);
优点
- 不存在浮点数的进度丢失问题
- 基本数据类型运算速度更快,内存占用也更小
缺点
- 需要处理单位转换,每次展示数据都要/100
- 范围限制,long的最大值为9,223,372,036,854,775,807,约为900万亿亿,如果还有更大的范围们可以转为BigInteger拓展
由上可知金额字段类型的选择原则:
- 避免
double
/float
:二进制浮点类型会导致精度丢失(如0.1
无法精确表示)。 - 优先选择不可变类型:确保金额在计算中不会被意外修改(如
BigDecimal
、long
)。