java 中避免精度丢失的核心做法是放弃使用 double 或 float 处理货币等需要精确值的场景,改用 java.math.bigdecimal。具体实现时,必须通过字符串构造函数创建 bigdecimal 对象,严禁直接使用浮点数作为构造参数,否则浮点数的二进制近似值会直接带入转换过程导致精度不可逆的丢失。

为什么 double 转 BigDecimal 依然会丢失精度
即使目标类型是 BigDecimal,如果初始化时传入的是 double 类型的数值,精度丢失依然会发生。这是因为 double 在内存中存储的是 IEEE 754 标准的二进制近似值,例如 0.1 在二进制中是一个无限循环小数,转换为十进制后可能变成 0.10000000000000000555...。当这个有误差的值传给 new BigDecimal(double) 时,BigDecimal 会忠实地记录这个误差,而不是将其四舍五入为预期的 0.1。

import java.math.BigDecimal;
public class PrecisionDemo {
public static void main(String[] args) {
// 错误示范:使用 double 构造 BigDecimal
double d = 0.1;
BigDecimal badBd = new BigDecimal(d);
System.out.println("Double 构造: " + badBd);
// 正确示范:使用 String 构造 BigDecimal
String s = "0.1";
BigDecimal goodBd = new BigDecimal(s);
System.out.println("String 构造: " + goodBd);
}
}
运行上述代码,控制台输出结果如下:
Double 构造: 0.1000000000000000055511151231257827021181583404541015625 String 构造: 0.1
可以看到,badBd 保留了 double 的原始二进制误差,而 goodBd 则完全符合预期。因此,任何从外部获取的浮点数据,在转为 BigDecimal 前必须先转为字符串,或者使用 BigDecimal.valueOf(double),该方法内部先调用 Double.toString() 将浮点数转为人类可读的字符串,再解析为 BigDecimal,从而规避了底层二进制表示带来的精度问题。
BigDecimal 运算时的精度与舍入策略
在使用 BigDecimal 进行加减乘除运算时,除法操作最容易引发 ArithmeticException 异常,因为某些十进制分数(如 1/3)在十进制下也是无限循环小数,无法用有限位数的 BigDecimal 精确表示。为了避免程序崩溃并保证业务逻辑的正确性,必须在执行除法时指定.scale()(小数位数)和.roundingMode()(舍入模式)。

import java.math.BigDecimal;
import java.math.RoundingMode;
public class BigDecimalOperation {
public static void main(String[] args) {
BigDecimal bd1 = new BigDecimal("10");
BigDecimal bd2 = new BigDecimal("3");
// 错误示范:不指定舍入模式的除法
try {
BigDecimal result = bd1.divide(bd2);
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("发生异常: " + e.getMessage());
}
// 正确示范:指定保留两位小数及舍入模式
BigDecimal safeResult = bd1.divide(bd2, 2, RoundingMode.HALF_UP);
System.out.println("安全计算结果: " + safeResult);
}
}
在上述代码中,bd1.divide(bd2) 会抛出异常,提示非终止十进制扩展。而 bd1.divide(bd2, 2, RoundingMode.HALF_UP) 则明确指定了结果保留两位小数,并采用“四舍五入”模式。在实际开发中,尤其是涉及金额计算时,通常推荐使用 RoundingMode.HALF_UP 以符合商业习惯,或者根据具体业务需求选择 RoundingMode.DOWN 或 RoundingMode.CEILING。此外,乘法运算虽然不会抛出异常,但如果两个 BigDecimal 的小数位数之和超过了系统限制或业务预期,也可能导致结果过于庞大,建议在乘法后也适当调整 scale。
如何正确处理输入源中的浮点数据
在真实项目中,BigDecimal 的数据来源通常是 JSON 响应、数据库查询结果或用户输入。如果前端或 API 返回的是数字类型(Number),Jackson 或 Gson 等序列化框架可能会默认将其映射为 Double,从而导致前述的精度丢失问题。解决这一问题的关键在于配置反序列化器,强制将特定字段或全局数字类型解析为 BigDecimal。
// Jackson 配置示例 ObjectMapper mapper = new ObjectMapper(); mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true); // 或者针对特定字段 @JsonFormat(shape = JsonFormat.Shape.STRING) private BigDecimal price;
通过启用 USE_BIG_DECIMAL_FOR_FLOATS,Jackson 会将 JSON 中的所有浮点数解析为 BigDecimal 对象,从而绕过 Double 的中间转换环节。对于数据库查询,Hibernate 或 MyBatis 等 ORM 框架通常能自动将 SQL 的 DECIMAL 类型映射为 Java 的 BigDecimal,但需确保实体类字段类型声明正确。若直接从 ResultSet 获取数据,应使用 getBigDecimal() 方法而非 getDouble(),以确保从 JDBC 驱动层就获得精确的十进制值。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











