bigdecimal.setscale配合roundingmode.half_up并非严格“四舍五入”,根本原因是用double构造会因二进制精度丢失导致舍入偏差,正确做法是始终用字符串构造;half_even仅在恰好处于正中间时按前一位奇偶舍入;scale为负数时表示对整数部分舍入;setscale返回新对象,保持高精度用于后续计算。

BigDecimal.setScale 用 RoundingMode.HALF_UP 不等于“四舍五入”?
很多人以为 setScale(2, RoundingMode.HALF_UP) 就是数学上严格的四舍五入,结果在处理 2.675 这类末位为 5 的数时发现结果是 2.67 而不是 2.68。这不是 Bug,而是 BigDecimal 构造时的精度陷阱。
根本原因:用 double 字面量构造 BigDecimal(比如 new BigDecimal(2.675))会先让 double 本身丢失精度——2.675 在二进制浮点中无法精确表示,实际传入的是略小于 2.675 的值(如 2.6749999999999998...),再用 HALF_UP 舍入自然向下取。
- ✅ 正确做法:始终用字符串构造,
new BigDecimal("2.675").setScale(2, RoundingMode.HALF_UP)→2.68 - ❌ 错误写法:
new BigDecimal(2.675).setScale(2, RoundingMode.HALF_UP)→2.67 - ⚠️ 注意:
Double.toString(2.675)也不可靠,它可能返回"2.675",但只是显示优化,底层仍是不精确的double
银行家舍入(RoundingMode.HALF_EVEN)到底怎么算?
RoundingMode.HALF_EVEN 是 setScale() 的默认模式,常被叫作“银行家舍入”,但它不是简单地“逢 5 看前一位奇偶”——它只在恰好处于两个可表示结果正中间时才触发奇偶判断。
例如保留 1 位小数:
立即学习“Java免费学习笔记(深入)”;
-
new BigDecimal("1.25").setScale(1, RoundingMode.HALF_EVEN)→1.2(因为 2 是偶数) -
new BigDecimal("1.35").setScale(1, RoundingMode.HALF_EVEN)→1.4(因为 3 是奇数) -
new BigDecimal("1.24").setScale(1, RoundingMode.HALF_EVEN)→1.2(没到中间,直接舍) -
new BigDecimal("1.26").setScale(1, RoundingMode.HALF_EVEN)→1.3(没到中间,直接入)
关键点:只有在待舍部分**严格等于 0.5 × 10⁻ⁿ**(即恰好卡在两个舍入目标正中间)时,才看前一位奇偶;否则该舍舍、该入入。
setScale 的 scale 参数为负数时会发生什么?
scale 可以是负数,表示对整数部分进行舍入——也就是“向左舍入”。比如 setScale(-1) 表示舍入到十位,setScale(-2) 到百位。
-
new BigDecimal("1234.56").setScale(-1, RoundingMode.HALF_UP)→1230(个位 4 -
new BigDecimal("1235.00").setScale(-1, RoundingMode.HALF_UP)→1240(个位是 5,入) -
new BigDecimal("999.99").setScale(-2, RoundingMode.HALF_UP)→1000(百位向上进位,结果位数可能增加)
注意:负 scale 下,HALF_UP 对 5 的处理仍遵循“>=5 入”的逻辑,和正 scale 一致;但容易忽略的是,结果类型仍是 BigDecimal,其内部 unscaledValue 和 scale 会同步调整,不影响精度表达。
为什么不用 Math.round 或 DecimalFormat 替代 setScale?
因为它们解决的不是同一类问题:Math.round 返回 long/int,丢精度;DecimalFormat 是格式化工具,输出是字符串,且受 Locale 和 roundingMode 设置影响大,不能用于后续计算。
-
Math.round(2.675 * 100) / 100.0→ 仍是double,无法避免浮点误差 -
new DecimalFormat("#.##").format(new BigDecimal("2.675"))→ 输出字符串"2.68",但你没法拿它加减乘除 -
setScale返回新BigDecimal,保持任意精度参与后续运算,这才是金融/计费场景真正需要的
真正容易被忽略的是:setScale 总是返回新对象,原 BigDecimal 不变;而且如果原值小数位数已少于目标 scale,它会补零(如 "1".setScale(3) → "1.000"),这点和 String.format 的行为完全不同。










