java金额计算精度丢失源于float/double二进制表示缺陷,bigdecimal通过字符串构造和整数运算规避;须避免double初始化、数据库直连decimal、乘除指定舍入、json序列化启用writebigdecimalasplain。

Java 中处理金额或高精度计算时,精度丢失不是 bug,而是 float/double 的固有缺陷。BigDecimal 不是“修复”浮点数,而是绕过它——用字符串建模十进制数,再用整数运算模拟加减乘除。关键不在“用了 BigDecimal”,而在“怎么用才不丢精度”。
源头控制:别让 double 污染第一步
最常见也最危险的错误,就是直接用 new BigDecimal(19.99)。此时 19.99 已经是 double 类型的近似值(实际存的是 19.990000000000002),构造出的 BigDecimal 只是把这个错误“精确记录”下来。
- ✅ 推荐:前端传“19.99”字符串 →
new BigDecimal("19.99") - ✅ 备选:已有 double d → 先转规范字符串:
new BigDecimal(Double.toString(d))(不能用String.valueOf(d),它可能输出科学计数法如 "1.23E5") - ❌ 禁止:
new BigDecimal(d)—— 无论 d 看起来多干净,底层二进制误差已存在
数据库对接:跳过 double 中间层
数据库字段应定义为 DECIMAL(18,2) 或类似定点类型,而非 FLOAT/DOUBLE。JDBC 读取时,务必使用:
-
resultSet.getBigDecimal("amount")—— 直接映射为 BigDecimal - 避免
resultSet.getDouble("amount")再转 BigDecimal,这等于重蹈覆辙
同理,写入时用 preparedStatement.setBigDecimal(1, amount),确保全程不触碰 double。
运算与舍入:乘除必须指定精度和模式
加减法天然保持精度(只要标度一致),但乘除会改变标度,必须显式控制:
-
add()/subtract():可直接调用,结果标度取操作数中较大者 -
multiply():建议用双参数版本,例如a.multiply(b, new MathContext(10, RoundingMode.HALF_UP)) -
divide():必须指定标度和舍入模式,如a.divide(b, 2, RoundingMode.HALF_EVEN)(金融常用银行家舍入)
不指定时,divide() 可能抛 ArithmeticException(除不尽),multiply() 可能产生远超业务需要的小数位。
序列化到 JSON:防止小数点后零被吞掉
FastJSON 默认调用 BigDecimal.toString(),会把 "1.0123" 输出为 1.123(吃掉小数点后首个 0)。前端解析后数值就错了。
- ✅ 正确做法:启用
SerializerFeature.WriteBigDecimalAsPlain - 代码示例:
JSON.toJSONString(obj, SerializerFeature.WriteBigDecimalAsPlain) - 原理:内部调用
toPlainString(),保证输出格式与原始字符串一致,且不用科学计数法











