1. 先把0.10.2≠0.3这件事说透BigDecimal的初始化、赋值和比大小是 Java 后端绕不开的三件小事。之所以说小事是因为 API 就那么几个IDE 一补全全出来了之所以说绕不开是因为这三个动作里埋的雷几乎每一个都曾让线上账目对不上、让对账脚本半夜报警。我见过太多人第一次接触BigDecimal是因为一段看起来完全正确的代码算出了0.30000000000000004然后被要求把所有涉及金额的 double 都换成 BigDecimal。这个要求本身没错但很多人换完之后只是把double改成BigDecimal写法照抄结果是精度问题从算错变成了偶尔算错排查难度反而更高。先说清楚这个类到底是什么。BigDecimal是 Java 提供的任意精度十进制有符号数由两部分构成一个不限长度的整数unscaledValue无标度值和一个 32 位的整数scale标度可以理解为小数点后保留几位。实际数值 unscaledValue × 10^(-scale)。比如new BigDecimal(123.45)内部存的是 unscaledValue12345、scale2。这个结构决定了它能精确表示任意十进制小数也决定了后面所有看起来奇怪的行为——equals判不等、toString输出科学计数法、setScale必须指定舍入模式——全都源于这个双字段结构。1.1 二进制浮点数的先天缺口double用 IEEE 754 双精度格式存储尾数只有 52 位本质是符号 × 2^指数 × 尾数的形式。十进制里的 0.1 转成二进制是无限循环小数0.0001100110011...52 位根本装不下只能截断。所以内存里存的 0.1 其实是一个无限接近但绝不等于 0.1 的数。两个这样的近似值相加误差被放大就得到了那个著名的0.30000000000000004。这不是 Java 的锅C、Python、JavaScript 全都一样。判断标准很简单只要一个数字在业务上要求分毫不差就不能用 float/double。金额、税率、库存数量、积分、汇率、利率、K 线价格这些都属于此类。反过来说物理仿真、图形渲染、机器学习里那种本身就有测量误差的场景用 double 完全合适硬套BigDecimal只会让性能掉一个量级还不讨好。值得强调的是double的问题不只是算出来多几位小数这么温和。0.1 0.2 0.3返回false这意味着任何用判等的代码都会静默失效1.0 - 0.9得到0.09999999999999998这意味着累加一万次之后偏差可能达到几毛钱。金额场景下这几毛钱就是资损。1.2 从 double 到 BigDecimal 的迁移判断标准不是所有double都要换。我用的判断标准有三条第一这个值是否会参与必须精确相等的比较第二这个值是否会做加减乘除后落库或展示给用户第三这个值是否存在多方对账需求。三条命中任意一条就该换。反过来如果只是用来算个超时时间、算个百分比用于日志打点换成BigDecimal是过度设计。这里有个容易被忽略的点换了类型不等于换了语义。曾经有个案例把金额字段从double改成BigDecimal之后代码里还留着if (amount BigDecimal.ZERO)这样的判断编译期不报错运行期永远为false因为比较的是对象引用。这类半截子迁移比不迁移更危险因为看代码的人会误以为这里已经处理好了精度问题。1.3 什么场景其实不需要 BigDecimal同样要说清楚反面。第一BigDecimal是不可变对象每一次加减乘除都会 new 一个新对象在十万级以上的循环里做累加性能开销是double的十几倍到几十倍第二BigDecimal不能直接用 - * /运算符代码可读性会下降第三序列化体积更大传输成本更高。所以我在实际项目里的做法是在领域模型的入口就把double挡在门外用BigDecimal承载业务语义在纯计算密集且不需要精度的中间环节允许临时降级为double但绝不跨越落库和展示这两道边界。这条边界一旦被突破精度问题的暴露往往延迟到几个月后对账的时候。2. 初始化的四种写法只有两种能上生产初始化是BigDecimal所有坑的源头。同一个数值用不同方式构造出来的对象equals结果可能不同内部 scale 可能不同打印出来可能一个是0.1一个是0.1000000000000000055511151231257827021181583404541015625。这一节把四种常见构造方式摊开讲。2.1 new BigDecimal(double)编译器不报错但结果已经错了BigDecimal bad new BigDecimal(0.1); System.out.println(bad); // 0.1000000000000000055511151231257827021181583404541015625这是最典型的翻车现场。new BigDecimal(double)会把传入的 double 值的精确二进制展开原封不动转成十进制得到的就是上面那一长串。很多人第一次看到这个输出会以为是打印问题实际上是值本身就是这个。IDEA 对new BigDecimal(0.1)这种字面量会给出警告但对变量入参不会。所以在方法签名里设计setAmount(double amount)是极其危险的等于把坑转移给了所有调用方。我的做法是项目里禁用new BigDecimal(double)用静态检查规则比如 Checkstyle 的自定义规则或 Sonar 的自定义规则直接拦住。这条规则实施成本极低收益极高。注意new BigDecimal(0.1f)更糟。float 精度比 double 低展开后的十进制位数虽然少但偏离更大。2.2 字符串与 valueOf 的等价性与非等价性BigDecimal a new BigDecimal(0.1); // 精确scale1 BigDecimal b BigDecimal.valueOf(0.1); // 也是 0.1scale1BigDecimal.valueOf(double)的内部实现是new BigDecimal(Double.toString(d))也就是先把 double 转成字符串再用字符串构造。Double.toString会输出能唯一标识该 double 的最短字符串所以0.1就输出0.1最终得到精确的 0.1。这条路径是安全的但要注意它绕了一圈字符串性能略低于直接传字符串。那到底选哪个我的建议是分场景从String来的数据前端入参、配置、报文一律用new BigDecimal(String)从double变量来的数据遗留代码、第三方库返回值一律用BigDecimal.valueOf(double)。两种都不用new BigDecimal(double)。还有一个细节new BigDecimal(1e3)是合法的得到1E3scale-3。负数标度在toString时会显示成科学计数法如果这个值要拼到 SQL 或者展示给用户必须用toPlainString()。这一点在 3.4 节展开。2.3 整数、长整型与常量复用new BigDecimal(100); // 安全整数转换无精度损失 BigDecimal.valueOf(100L); // 安全且 JDK 9 对 0~10 有缓存 BigDecimal.TEN; // 常量直接用int、long转BigDecimal是精确的因为没有小数部分。BigDecimal类里预定义了ZERO、ONE、TEN三个常量JDK 9 之后valueOf(long)对 0 到 10 返回的是缓存实例。在循环里频繁用 0 做初始值累加时用BigDecimal.ZERO而不是BigDecimal.valueOf(0)能少创建很多对象。不过别过度迷信缓存。真正的性能瓶颈从来不在常量创建上而在那些本可以避免的大数运算。我见过有人为了性能把金额用long存成分运算完再转BigDecimal逻辑复杂度翻了三倍收益却完全感知不到。除非是每秒几十万笔的高频结算场景否则不值得。2.4 从数据库和 JSON 拿到的值怎么落地数据库的DECIMAL(18,2)字段通过 JDBC 读出来直接就是BigDecimal这是最舒服的路径——别中途转成 double 再转回来。ORM 框架里如果字段类型声明成了Double那就是把精度在映射层丢掉了后面再怎么处理都救不回来。JSON 反序列化要小心。不同框架对数字的处理策略不同有的默认把 JSON 数字解析成double再给BigDecimal赋值这时候1.10可能变成1.1scale 丢了有的支持直接以字符串/字面量方式解析成BigDecimal尺度保持原样。稳妥做法是前后端约定金额字段用字符串传输后端接收时new BigDecimal(str)前端展示时拿到字符串原样显示整条链路不出现浮点数。3. 赋值这件事在 BigDecimal 里根本不存在标题里写了赋值但严格说BigDecimal里没有赋值只有引用重定向。这个类被final修饰内部的两个字段也全是final的所有看起来像修改的方法实际都返回了新对象。这个认知决定了你能不能写出正确的累加代码。3.1 不可变对象每一次运算都是新建BigDecimal total BigDecimal.ZERO; total.add(new BigDecimal(10.00)); // 这行代码什么也没做 System.out.println(total); // 0上面这段是新手最常写的错误。add返回了新对象但没有接收原来的total依然是 0。正确写法是total total.add(...)。这个问题在单行代码里很显眼但换成list.forEach(item - total.add(item.getAmount()))就很隐蔽了——lambda 里的total还必须是final或等效 final 的编译器会直接拦住反而算是因祸得福。不可变带来的好处是天然线程安全。BigDecimal可以安全地在多线程间共享不需要任何同步措施。这也是它在并发结算场景里比double更受欢迎的原因之一——double的读写在某些架构上甚至不是原子的更别说复合运算了。另一个好处是可以放心当 Map 的 key、放进 Set 去重。但这里有个大坑见 4.1 节BigDecimal的hashCode包含了scale1.0和1.00哈希不同去重会失效。3.2 scale 决定了你看到的字符串scale是BigDecimal的双字段结构里那个容易被忽略的字段。它不影响数值大小但影响打印结果和equals 结果。BigDecimal x new BigDecimal(1.0); // scale 1 BigDecimal y new BigDecimal(1.00); // scale 2 BigDecimal z new BigDecimal(1); // scale 0三者数值完全相等compareTo全都返回 0但打印出来分别是1.0、1.00、1equals两两不相等。所以赋值这个动作在BigDecimal里其实包含了两层含义赋数值和赋标度。前者靠构造后者靠setScale。业务上这两层都有意义。金额字段通常要求 scale2即使值是整数也要显示成10.00数量字段可能要求 scale3汇率可能要求 scale6。如果只关心数值不关心展示scale 随便一旦涉及展示、对账、导出 Excelscale 必须统一。3.3 setScale 的第二个参数才是业务规则的载体BigDecimal v new BigDecimal(10.005); System.out.println(v.setScale(2, RoundingMode.HALF_UP)); // 10.01 System.out.println(v.setScale(2, RoundingMode.DOWN)); // 10.00 System.out.println(v.setScale(2, RoundingMode.HALF_EVEN)); // 10.00setScale(int)单参数版本在需要舍入时会直接抛ArithmeticException因为它不知道该往哪边舍。这个设计很好逼着你显式声明业务规则。可惜很多人被异常逼急了直接加上RoundingMode.HALF_UP了事从来没想过这个选择对不对。几个舍入模式的业务含义对照模式行为典型场景HALF_UP四舍五入0.5 远离零国内大多数金额计算、发票HALF_EVEN银行家舍入0.5 向偶数靠国际金融、统计汇总、多次累加HALF_DOWN五舍六入少数特定行业规则UP一律远离零罚息、违约金对己方有利DOWN一律向零截断折扣、优惠券对用户不利需谨慎CEILING/FLOOR向正无穷 / 负无穷分页、配额计算UNNECESSARY有舍入就抛异常断言这里不该有精度损失HALF_EVEN值得多说一句。它在大量数据累加时能避免系统性偏差HALF_UP会让所有 0.5 都向上累加一万次就凭空多出五千个最小单位HALF_EVEN让 0.5 一半向上、一半向下长期期望偏差趋近于零。如果你的业务是做报表汇总、指标统计用HALF_EVEN更合理如果是单笔交易金额用HALF_UP更符合用户直觉。这个选择必须写进技术方案不能靠程序员拍脑袋。3.4 stripTrailingZeros 与 toPlainString 的配合stripTrailingZeros()用来去掉尾部的零比如把1.500变成1.5。但它有两个坑。第一个坑是零的处理。在某些 JDK 版本里new BigDecimal(0.00).stripTrailingZeros()返回的是0E-2打印出来是科学计数法而不是0。这个行为在不同版本上表现不一致不能依赖。安全的写法是先判零BigDecimal r v.compareTo(BigDecimal.ZERO) 0 ? BigDecimal.ZERO : v.stripTrailingZeros();第二个坑是toString()可能输出科学计数法。当 scale 为负数或者调整后的 scale 小于 -6 时toString会输出1E3这样的形式。如果你的值要拼进 SQL、写进 CSV、返回给前端必须用toPlainString()它永远输出普通十进制字符串。我一般在所有对外输出的位置统一用toPlainString()避免某天数据量级变了突然冒出个1E3让下游解析失败。提示toEngineeringString()用的是工程计数法指数是 3 的倍数除非有特殊展示需求一般不用。4. 比大小compareTo 与 equals 的分水岭比大小是BigDecimal里最容易写出看起来对、实际错的代码的地方。核心就一句话equals比的是值 标度compareTo比的是数值大小。这一句话能解释 90% 的相关 bug。4.1 equals 为什么判 1.0 和 1.00 不等BigDecimal a new BigDecimal(1.0); BigDecimal b new BigDecimal(1.00); System.out.println(a.equals(b)); // false System.out.println(a.compareTo(b) 0); // trueequals的源码逻辑是先比较scale是否相等不等直接返回 false相等再比较unscaledValue。1.0的 scale 是 1、unscaledValue 是 101.00的 scale 是 2、unscaledValue 是 100。数值一样但两个字段都不一样所以判不等。这个行为在集合里会放大成灾难SetBigDecimal set new HashSet(); set.add(new BigDecimal(1.0)); set.add(new BigDecimal(1.00)); System.out.println(set.size()); // 2因为hashCode的实现是31 * unscaledValue.hashCode() scale两个对象的哈希值不同落到了不同的桶。用BigDecimal作为HashMap的 key 去查缓存、做幂等判断就会出现明明存在却查不到的诡异现象。修复方式很直接入集合前统一setScale或者在业务层约定所有参与比较的BigDecimal都经过同一个规范化方法处理。我在项目里通常会写一个normalize(BigDecimal v)方法内部做v.setScale(2, RoundingMode.HALF_UP)所有进入集合、进入缓存 key 的值都先过一遍。这样从源头上消灭了标度不一致。4.2 compareTo 返回值的使用姿势compareTo返回-1、0、1分别代表小于、等于、大于。JDK 7 之后规范明确保证只返回这三个值早期版本可能返回任意负数或正数。int c a.compareTo(b); if (c 0) { /* a 大于 b */ } else if (c 0) { /* a 小于 b */ } else { /* 相等 */ }注意不要写if (a.compareTo(b) 1)。虽然 JDK 7 确实返回 1但用 0语义更清晰也不依赖实现细节。更不要写if (a.compareTo(b) ! -1)来表达大于等于这种写法可读性极差正确性还依赖返回值恰好是 -1。判零有个专门的写法a.compareTo(BigDecimal.ZERO) 0。不要用a.equals(BigDecimal.ZERO)因为它对0.00会返回 false。也不要写a.intValue() 0因为0.5的intValue()是 0判零会误判。同理判断正负用signum()更直观返回 -1、0、1。4.3 空值、区间与批量比较的封装compareTo碰到null会直接 NPE。业务代码里从数据库、从报文里取出来的金额完全可能是null所以每个比较点都要做判空。散落各处的判空很快就会失控我一般封装成一个工具方法public static int nullSafeCompare(BigDecimal a, BigDecimal b) { if (a null b null) return 0; if (a null) return -1; if (b null) return 1; return a.compareTo(b); }这样null被当作最小值处理排序、取最大值都能直接用。区间判断也值得封装。判断金额是否在某个范围内时闭区间和开区间的边界很容易写错public static boolean inRange(BigDecimal v, BigDecimal min, BigDecimal max) { return v.compareTo(min) 0 v.compareTo(max) 0; }批量场景下取最大值/最小值用Collections.max(list)是可以的BigDecimal实现了Comparable但要注意 list 里不能有 null且标度不一致不影响比较结果——这一点比equals友善得多。5. 除法才是真正的高危操作加减乘都不会抛异常乘法的 scale 会累加但不会失败只有除法会。这是BigDecimal里唯一一个运行期可能炸的基础运算值得单独一节。5.1 Non-terminating decimal expansion 的触发条件new BigDecimal(1).divide(new BigDecimal(3)); // java.lang.ArithmeticException: Non-terminating decimal expansion; // no exact representable decimal result.1 除以 3 是无限循环小数BigDecimal无法精确表示于是抛异常。这个设计初衷是好的——宁可报错也不给你一个悄悄截断的结果。但在实际项目里这个异常往往在某个特定数据上才触发测试环境跑了三个月都没事上线第一天就炸。触发条件很明确除数除以被除数约分后的质因数中含有 2 和 5 以外的因子。3、7、11、13 这类都会触发。所以除以 2、4、5、8、10、20 这类数是安全的除以 3、6、7、9 是危险的。这个规律记不住也没关系记住除法要么给精度要么给MathContext就够了。5.2 三种 divide 重载的适用边界divide有三个常用重载用途完全不同// 1. 精确除法除不尽抛异常 a.divide(b); // 2. 指定标度和舍入模式最常用 a.divide(b, 2, RoundingMode.HALF_UP); // 3. 指定 MathContext按有效数字位数控制 a.divide(b, new MathContext(10, RoundingMode.HALF_UP));日常业务里 99% 的情况用第二种。它把保留几位和怎么舍两个决策都摆到明面上代码即文档。第一种只在你能保证整除时使用比如除以 100、除以 4 这种。第三种MathContext控制的是有效数字位数而不是小数位数适合科学计算场景业务代码里很少见。有个变体很实用divideAndRemainder(b)一次返回商和余数数组做整除分配、进制转换时用得上。5.3 舍入模式的业务含义对照除以 3 之后保留两位用不同舍入模式的结果差异如下被除数/除数HALF_UPHALF_EVENDOWNUP1 / 30.330.330.330.342 / 30.670.670.660.6710 / 42.502.502.502.5011 / 42.752.752.752.755 / 22.502.502.502.501 / 80.130.120.120.13最后一行 1/8 0.125正好卡在边界上HALF_UP给 0.13HALF_EVEN因为 2 是偶数给 0.12。这就是银行家舍入的直观体现。选择哪个必须在技术方案里写清楚最好在工具类里统一定义一个常量SCALE_MONEY 2和ROUNDING RoundingMode.HALF_UP全项目共用避免不同模块用不同规则。6. 分摊、累加、求平均三个真实计算模型的落地光讲 API 没意思讲三个我实际遇到过、也确实踩过坑的计算模型。这三个场景覆盖了BigDecimal在业务里的绝大多数用法。6.1 分摊到分的尾差兜底一百块钱平均分给三个人每人 33.33加起来 99.99少了一分。这一分钱去哪了这不是 bug是舍入的必然结果。处理方式有两种把尾差挂到第一个人头上或者挂到最后一个人头上。public static ListBigDecimal average(BigDecimal total, int parts) { BigDecimal each total.divide(BigDecimal.valueOf(parts), 2, RoundingMode.DOWN); BigDecimal assigned each.multiply(BigDecimal.valueOf(parts)); BigDecimal diff total.subtract(assigned); // 尾差 ListBigDecimal result new ArrayList(); for (int i 0; i parts; i) { result.add(i parts - 1 ? each.add(diff) : each); } return result; }关键点在于用DOWN而不是HALF_UP做基础值。用DOWN保证每份都不会超出平均值尾差一定是非负的挂到最后一份上不会出现负数。如果用HALF_UPeach可能偏大diff变成负数最后一份就成了负数金额业务上很难解释。还有一个更隐蔽的坑分摊的份数可能很大。比如把一笔费用分摊到几百个商品上如果每份都做四舍五入累计误差可能达到几块钱。这时候必须用先分配、再补差的方式并且补差放到最后一份。绝对不能每份独立舍入后直接返回那样总和对不上必然被财务打回来。6.2 累加过程中的精度保持循环里做累加有两件事要注意接收返回值和统一标度。BigDecimal total BigDecimal.ZERO; for (Order o : orders) { BigDecimal amt o.getAmount() null ? BigDecimal.ZERO : o.getAmount(); total total.add(amt); // 必须接收返回值 } total total.setScale(2, RoundingMode.HALF_UP); // 最后统一舍入标度的策略是过程中不限制最后统一舍入。如果每加一次就setScale(2)误差会被逐次放大如果在过程中就截断累加十万条之后偏差可能达到几百元。正确做法是让BigDecimal自己维护精确值它的标度会随着运算自动调整只在最终输出时做一次舍入。这里还有个性能考量如果累加的数据量极大比如百万级每次add都创建新对象内存压力不小。可以考虑用double做预聚合、再分批转BigDecimal汇总但前提是单批数据量不大、且预聚合的误差可接受。这种优化必须有压测数据支撑不能凭空猜。6.3 平均值与百分比的先乘后除算百分比时最常见的错误是先除后乘。// 错误0.33 * 100 33丢了两位 BigDecimal pct part.divide(total, 2, RoundingMode.HALF_UP) .multiply(BigDecimal.valueOf(100)); // 正确先乘后除中间过程保持精度 BigDecimal pct part.multiply(BigDecimal.valueOf(100)) .divide(total, 2, RoundingMode.HALF_UP);divide的第二次参数是小数位数先除的话中间结果只有 2 位有效小数再乘 100 精度已经丢了。先乘后除能把中间值保持在整数域除法只做一次精度损失最小。求平均值的思路类似。sum.divide(count, 2, HALF_UP)一次搞定别先对每个元素舍入再平均——那样每个元素都引入了舍入误差平均之后误差不会抵消只会累积。这是统计学的基本常识但在代码里特别容易忘。7. 工程化收口工具类、序列化与库表精度前面讲的是怎么用这一节讲怎么管。BigDecimal的问题不在于单个方法难懂而在于用的人多、写法分散、规则不统一。所以必须做工程化收口。7.1 一个可以直接抄的工具类public final class MoneyUtils { public static final int SCALE 2; public static final RoundingMode ROUNDING RoundingMode.HALF_UP; private MoneyUtils() {} /** 安全构造null 与空串返回 ZERO */ public static BigDecimal of(String v) { if (v null || v.trim().isEmpty()) return BigDecimal.ZERO; return new BigDecimal(v.trim()); } /** 安全构造double 走 valueOf避免二进制展开 */ public static BigDecimal of(double v) { return BigDecimal.valueOf(v); } /** 规范化统一标度用于比较和入集合 */ public static BigDecimal norm(BigDecimal v) { return v null ? BigDecimal.ZERO : v.setScale(SCALE, ROUNDING); } /** 空安全比较 */ public static int cmp(BigDecimal a, BigDecimal b) { return norm(a).compareTo(norm(b)); } public static boolean gt(BigDecimal a, BigDecimal b) { return cmp(a, b) 0; } public static boolean lt(BigDecimal a, BigDecimal b) { return cmp(a, b) 0; } public static boolean eq(BigDecimal a, BigDecimal b) { return cmp(a, b) 0; } /** 空安全加法 */ public static BigDecimal add(BigDecimal a, BigDecimal b) { return norm(a).add(norm(b)); } /** 空安全乘法结果保留两位 */ public static BigDecimal mul(BigDecimal a, BigDecimal b) { return norm(norm(a).multiply(norm(b))); } /** 安全除法除零返回 ZERO */ public static BigDecimal div(BigDecimal a, BigDecimal b) { if (b null || b.compareTo(BigDecimal.ZERO) 0) return BigDecimal.ZERO; return a.divide(b, SCALE, ROUNDING); } /** 去掉尾部零零值特殊处理 */ public static BigDecimal strip(BigDecimal v) { if (v null || v.compareTo(BigDecimal.ZERO) 0) return BigDecimal.ZERO; return v.stripTrailingZeros(); } /** 对外输出永不用科学计数法 */ public static String plain(BigDecimal v) { return norm(v).toPlainString(); } }这个类有几个设计取舍值得说。第一所有方法都做 null 安全因为在业务代码里 null 的传播速度极快一处不防就是一片 NPE。第二norm统一成 scale2意味着这个工具类只适用于金额不适合数量、汇率。如果项目里多种标度并存就按领域拆成MoneyUtils、RateUtils、QtyUtils别做一个万能工具类。第三div遇到除零返回 ZERO 而不是抛异常这个选择有争议——有人认为除零应该报错。我的理由是报表、统计场景下除数确实可能为 0返回 0 比让整个页面 500 更友好但要保证日志里有记录。7.2 序列化到前端时的精度断层后端算得好好的BigDecimal一变成 JSON 就可能出事。原因很直接JSON 规范里数字没有精度约定前端 JavaScript 的Number就是双精度浮点有效数字大约 15 到 17 位。一个 18 位的金额主键或者一个 scale6 的汇率超过这个范围就会被静默截断而且不会有任何报错。解决方案是所有BigDecimal字段序列化成字符串。public class OrderVO { JsonSerialize(using ToStringSerializer.class) private BigDecimal amount; }或者在全局配置里让序列化器把BigDecimal统一写成字符串。代价是前端要按字符串处理做运算时得用专门的库。但相比精度丢失导致的资损这点改造量完全值得。反序列化方向同理前端传金额用字符串后端用JsonDeserialize或者直接把 DTO 字段声明成String再new BigDecimal(str)。千万别让前端传数字也别在后端声明double。7.3 数据库字段与 Java 属性的精度对齐最后说库表。DECIMAL(p, s)是 MySQL、PostgreSQL、Oracle 通用的精确数值类型p 是总位数、s 是小数位。金额一般用DECIMAL(18, 2)18 位总长度足够支撑万亿级金额2 位小数对应分。三处必须对齐数据库字段的 scale、Java 属性的 scale、业务层舍入的 scale。任何一处不一致都会导致存进去是 10.00读出来是 10或者算出 10.005入库被截断成 10.00对账差 5 厘。我的做法是在技术方案里明确写死金额统一DECIMAL(18,2)Java 层统一scale2、HALF_UP所有涉及金额的 DTO 字段加注释说明。注意Oracle 的NUMBER类型如果不指定精度行为依驱动版本而异可能读出BigDecimal也可能读出Double务必显式声明精度。还有个细节统计类 SQL 的返回值。SUM(amount)在不同数据库里返回的类型不一样有的给BigDecimal、有的给Double、有的给String。如果用ResultSet.getObject()拿到的是Double那在 JDBC 层就已经丢精度了后面再怎么用BigDecimal都白搭。稳妥做法是把SUM结果在 SQL 里就CAST成DECIMAL或者用getBigDecimal()显式取值。我在几个项目里推行过这套规矩推行初期阻力不小——有人觉得不就是个金额嘛至于搞个工具类。等到第一次因为0.10.2对不上账、第一次因为equals判等失败导致重复扣款、第一次因为除法抛异常导致订单卡单规矩就自然而然立起来了。这些坑不用全踩一遍提前知道它们长什么样就能少熬几个夜。
企业数字化 ERP 产品动态
相关推荐
Vue动态样式绑定与Tab切换高亮实现详解 1. Vue动态样式绑定基础解析动态样式绑定是Vue.js框架中最实用也最容易被新手忽视的特性之一。在实际项目中,我们经常需要根据数据状态来改变元素的样式表现。传统做法是通过JavaScript直接操作DOM元素的className属性,这种方式不仅代码冗长,… · 2026/11/3 8:06:04
RabbitMQ在大数据实时处理中的架构设计与优化实践 1. 消息队列与大数据处理的天然契合三年前我第一次在电商平台大促期间遇到数据积压问题时,凌晨三点盯着不断飙升的服务器监控曲线,突然意识到传统批处理架构的致命缺陷——当每秒十万级的订单数据涌来时,再强大的Hadoop集群也会在数据洪流前显… · 2026/11/3 8:06:04
Jev模型:TypeSafe AI与结构化决策在自动化审核和Agent工作流中的应用 1. 从“不说话”的模型说起:Jev 到底在解决什么问题第一次看到“Jev”这个名字,加上“前 OpenAI 研究员做的‘不说话’模型”这个描述,我脑子里冒出来的第一个念头是:又一个噱头?毕竟这两年模型层出不穷,几… · 2026/11/3 8:06:04
【大数据框架】Calcite 基础概念:从 SQL 到执行计划的思维路径 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 9:51:56
Qwen3-Omni:多模态AI实时交互全攻略 Qwen3-Omni:多模态AI实时交互全攻略
导语
Qwen3-Omni-30B-A3B-Instruct正式发布,作为一款原生支持文本、图像、音视频输入并实时生成语音的多语言全模态模型,它标志着AI交互从单一模态向"感知-理解-响应"全链路智能化的跨越。
行… · 2026/11/3 9:51:56
三环境测试全覆盖:OpenRouter ai-sdk-provider Node、Edge与E2E测试体系解析 【免费下载链接】ai-sdk-provider The OpenRouter provider for the Vercel AI SDK contains support for hundreds of models through the OpenRouter chat and completion APIs. 项目地址: https://gitcode.com/gh_mirrors/ai/ai-sdk-provider 点击查看 免费下载 … · 2026/11/3 9:51:56
RLPR-Qwen2.5:无验证器也能玩转推理? RLPR-Qwen2.5:无验证器也能玩转推理?
导语:OpenBMB团队推出基于Qwen2.5-7B-Base优化的RLPR-Qwen2.5-7B-Base模型,采用创新的RLPR框架,无需外部验证器即可提升大模型推理能力,为通用领域推理任务提供新范式。… · 2026/11/3 9:51:56
Folia PWA 手机端安装指南:3 步装好歌词动画播放器 Folia PWA 手机端安装指南:3 步装好歌词动画播放器 【免费下载链接】folia-major 专注于绚丽的歌词动画效果的本地音乐/navidrome/第三方多平台在线音乐播放器 项目地址: https://gitcode.com/GitHub_Trending/fo/folia-major
Folia 歌词动画播放器可以以 PW… · 2026/11/3 9:51:56
【44】苹果数据集(有v5/v8模型)/YOLO苹果检测 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 9:50:51
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46