一、先搞懂:为什么浮点型会丢精度?
我们常用的float和double,属于二进制浮点类型,但现实中我们计算的“金额(如1.2元)”“重量(如3.5kg)”是十进制数据——这两种进制的“不兼容”,就是精度丢失的根源。
举个最直观的例子,运行这段代码:
java
复制
public class FloatTest {
public static void main(String[] args) {
System.out.println(0.1 + 0.2); // 输出结果不是0.3,而是0.30000000000000004
System.out.println(1.0 - 0.9); // 输出0.09999999999999998
System.out.println(2.01 * 100); // 输出200.99999999999997
}
}
为什么会这样?
因为0.1在二进制中是“无限循环小数” (类似十进制的1/3=0.333...),而float和double的存储位数有限(float占4字节,double占8字节),只能“四舍五入”保留部分二进制位——这种“截断”就导致了精度丢失,后续计算会把误差放大,最终出现“0.1+0.2≠0.3”的诡异结果。
重点:只要涉及“需要精确计算”的场景(金额、财务、计量),绝对不能用float/double! 这不是“代码写错了”,而是数据类型的底层特性决定的。
二、救星BigDecimal:但90%的人用错了
很多人知道“用BigDecimal代替浮点型”,但我见过太多“用了BigDecimal还丢精度”的情况——问题出在构造方法和计算方式上,这两个坑一定要避开。
坑1:用double构造BigDecimal(最常见错误)
直接用new BigDecimal(double)构造,会把double的精度误差“带进去”,比如:
java
复制
// 错误用法:用double构造,精度误差被保留
BigDecimal wrong1 = new BigDecimal(0.1);
System.out.println(wrong1); // 输出0.1000000000000000055511151231257827021181583404541015625
// 正确用法:用String构造,完全保留十进制精度
BigDecimal right1 = new BigDecimal("0.1");
System.out.println(right1); // 输出0.1
// 也可以用BigDecimal.valueOf(double)(底层会转成String,推荐)
BigDecimal right2 = BigDecimal.valueOf(0.1);
System.out.println(right2); // 输出0.1
结论:构造BigDecimal时,优先用new BigDecimal(String)或BigDecimal.valueOf(double),绝对别用new BigDecimal(double)!
坑2:用“+、-、*、/”直接计算(编译都不通过)
BigDecimal是“对象”,不能像基本类型那样用算术运算符计算,必须用它的成员方法(add/subtract/multiply/divide),且计算时要指定“舍入模式”(避免除不尽时抛异常)。
正确计算示例(以金额计算为例):
java
复制
public class BigDecimalCalc {
public static void main(String[] args) {
// 1. 初始化金额(单位:元,用String构造)
BigDecimal price = new BigDecimal("99.9"); // 商品单价
BigDecimal quantity = new BigDecimal("3"); // 购买数量
BigDecimal discount = new BigDecimal("0.9"); // 9折优惠
// 2. 计算:总价 = 单价 * 数量 * 折扣(用成员方法)
BigDecimal total = price.multiply(quantity).multiply(discount);
System.out.println("折后总价:" + total); // 输出269.73
// 3. 除法示例(如拆分金额,必须指定舍入模式)
BigDecimal split = total.divide(new BigDecimal("2"), 2, BigDecimal.ROUND_HALF_UP);
System.out.println("每人分摊:" + split); // 输出134.87(四舍五入保留2位小数)
}
}
关键:舍入模式怎么选?(金额计算常用3种)
| 舍入模式常量 | 含义(以保留2位小数为例) | 适用场景 |
|---|---|---|
ROUND_HALF_UP | 四舍五入(1.234→1.23,1.235→1.24) | 金额计算、日常统计 |
ROUND_DOWN | 直接截断(1.239→1.23) | 计算最小支付金额(不进位) |
ROUND_UP | 直接进位(1.231→1.24) | 计算税费(不遗漏分厘) |
注意:
divide方法必须指定“舍入模式”和“保留小数位数”,否则当除法结果是无限小数时(如1÷3),会抛出ArithmeticException!
三、实战避坑:金额计算的3个“固定套路”
结合8年项目经验,总结出“金额计算(元为单位)”的标准化写法,直接套用能避免99%的问题:
套路1:定义“保留小数位数”和“舍入模式”常量
避免硬编码,后续修改更方便:
java
复制
// 金额计算:固定保留2位小数,四舍五入
private static final int SCALE = 2;
private static final int ROUND_MODE = BigDecimal.ROUND_HALF_UP;
套路2:封装“加减乘除”工具方法
重复代码抽成工具类,减少重复错误:
java
复制
public class MoneyUtil {
private static final int SCALE = 2;
private static final int ROUND_MODE = BigDecimal.ROUND_HALF_UP;
// 加法
public static BigDecimal add(BigDecimal a, BigDecimal b) {
return a.add(b).setScale(SCALE, ROUND_MODE);
}
// 减法
public static BigDecimal subtract(BigDecimal a, BigDecimal b) {
return a.subtract(b).setScale(SCALE, ROUND_MODE);
}
// 乘法
public static BigDecimal multiply(BigDecimal a, BigDecimal b) {
return a.multiply(b).setScale(SCALE, ROUND_MODE);
}
// 除法
public static BigDecimal divide(BigDecimal a, BigDecimal b) {
if (BigDecimal.ZERO.compareTo(b) == 0) {
throw new IllegalArgumentException("除数不能为0");
}
return a.divide(b, SCALE, ROUND_MODE);
}
}
套路3:和数据库交互时的“类型对应”
如果数据库存储金额用DECIMAL类型(推荐),Java中用BigDecimal接收,避免类型转换丢失精度:
-
数据库字段定义:
amount DECIMAL(10,2)(10位整数+2位小数,足够存储千万级金额) -
MyBatis映射:直接用
java.math.BigDecimal对应,不要用double接收
四、最后总结:3句话避开浮点精度坑
-
场景判断:只要是“需要精确到分/厘”的计算(金额、财务),绝对不用float/double;
-
构造正确:BigDecimal用String构造或valueOf(double),别用double构造;
-
计算规范:用成员方法(add/subtract),指定舍入模式和保留位数,优先封装工具类。
如果你的项目中还有“浮点精度”相关的奇葩问题,或者想了解“BigDecimal的性能优化”(比如频繁创建对象的问题),可以随时跟我聊~