一、先搞懂:为什么浮点型会丢精度?
我们常用的float和double,属于二进制浮点类型,但现实中我们计算的“金额(如1.2元)”“重量(如3.5kg)”是十进制数据——这两种进制的“不兼容”,就是精度丢失的根源。
举个最直观的例子,运行这段代码:
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的精度误差“带进去”,比如:
// 错误用法:用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),且计算时要指定“舍入模式”(避免除不尽时抛异常)。
正确计算示例(以金额计算为例):
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:定义“保留小数位数”和“舍入模式”常量
避免硬编码,后续修改更方便:
// 金额计算:固定保留2位小数,四舍五入 private static final int scale = 2; private static final int round_mode = bigdecimal.round_half_up;
套路2:封装“加减乘除”工具方法
重复代码抽成工具类,减少重复错误:
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接收
四、知识扩展
在 java 中,float 和 double 用于科学计算和工程计算,但由于它们采用二进制浮点数表示,无法精确表示某些十进制小数(如 0.1),从而导致精度丢失。
示例:
double a = 0.1; double b = 0.2; system.out.println(a + b); // 0.30000000000000004
为了进行精确的十进制运算(如金额计算),java 提供了 bigdecimal 类。它通过用整数(biginteger)和标度(小数点后位数)的组合来精确表示任意精度的十进制数。
正确创建 bigdecimal 对象
错误方式:使用 double 构造方法
bigdecimal bd = new bigdecimal(0.1); // 0.1000000000000000055511151231257827021181583404541015625
因为 0.1 本身已经是近似值,所以传入 double 就失去了精度。
正确方式:使用字符串构造器
bigdecimal bd = new bigdecimal("0.1");推荐方式:使用 bigdecimal.valueof(double)如果参数是 double 字面量或已知精确值,可使用静态方法:
bigdecimal bd = bigdecimal.valueof(0.1);
该方法内部先将 double 转换为字符串再构造,能避免部分精度问题。
基本运算(加减乘除)
bigdecimal a = new bigdecimal("10.25");
bigdecimal b = new bigdecimal("3.05");
// 加法
bigdecimal sum = a.add(b); // 13.30
// 减法
bigdecimal diff = a.subtract(b); // 7.20
// 乘法
bigdecimal prod = a.multiply(b); // 31.2625
// 除法(必须指定舍入模式,否则可能抛出 arithmeticexception)
bigdecimal quot = a.divide(b, 2, roundingmode.half_up); // 3.36
// 参数:除数,保留小数位数,舍入模式关键:除法必须指定精度和舍入模式,避免无限循环小数。
精度控制与舍入模式
| 舍入模式 | 说明 |
|---|---|
roundingmode.half_up | 四舍五入(最常用) |
roundingmode.half_down | 五舍六入 |
roundingmode.up | 远离零方向舍入 |
roundingmode.down | 向零方向舍入 |
roundingmode.ceiling | 向正无穷方向舍入 |
roundingmode.floor | 向负无穷方向舍入 |
roundingmode.half_even | 向偶数舍入(银行家舍入法) |
bigdecimal d = new bigdecimal("2.335");
system.out.println(d.setscale(2, roundingmode.half_up)); // 2.34
system.out.println(d.setscale(2, roundingmode.half_down)); // 2.33比较 bigdecimal 对象
错误方式:使用 equalsequals 会比较精度(scale),例如 new bigdecimal("2.0") 与 new bigdecimal("2.00") 不相等。
正确方式:使用 compareto
bigdecimal x = new bigdecimal("2.0");
bigdecimal y = new bigdecimal("2.00");
system.out.println(x.compareto(y) == 0); // truecompareto 返回 -1(小于)、0(等于)、1(大于)。
五、最后总结:3句话避开浮点精度坑
- 场景判断:只要是“需要精确到分/厘”的计算(金额、财务),绝对不用float/double;
- 构造正确:bigdecimal用string构造或valueof(double),别用double构造;
- 计算规范:用成员方法(add/subtract),指定舍入模式和保留位数,优先封装工具类。
到此这篇关于java使用bigdecimal解决精度丢失问题的文章就介绍到这了,更多相关java bigdecimal解决精度丢失内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论