java bigdecimal负数转正数_java的四舍五入(四舍六入?)舍入模式详解

在开发中,保留N位小数位的需求肯定不会少。我们项目也有很多地方有这要求,其中一个地方是必须用到 freemarker的保留2位小数位即 ${x?string("0.##")} 语法,而这就是bug的源点。

场景重现

某一天测试突然提了个bug给我,没有人会乐意别人给自己提bug。而且一看这bug还是四舍五入的问题,这就是使用的框架自带的方法,怎么会存在问题呢?一看详情,确实是框架的方法存在问题,一看源码,直接调用的 JDK 的方法,这难道还是 JDK 存在问题?继续查下去发现事情并没有这简单。

四舍五入

我们知道,保留2位小数一般使用如下几种方法(为避免直接使用 double 出现精度问题,使用 StringBigDecimal 来操作):

public static void main(String[] args) {
    String[] nums = new String[]{"1.114", "2.225", "3.336"};
    scaleByRound(nums);
    scaleByStringFormat(nums);
    scaleByDecimalFormat(nums);
    scaleByBigDecimal(nums);
}

// 第一种方法 先扩大100倍 然后四舍五入取值,然后再缩小100倍。
public static void scaleByRound(String[] nums) {
    System.out.println("--------第一种方法--------");
    Arrays.stream(nums).forEach(o ->
        System.out.println(o+"-> "+(double) Math.round(new BigDecimal(o).doubleValue() * 100) / 100)
    );
}

// 使用String#format
public static void scaleByStringFormat(String[] nums) {
    System.out.println("--------第二种方法--------");
    Arrays.stream(nums).forEach(o ->
        System.out.println(o+"-> "+String.format("%.2f", new BigDecimal(o)))
    );
}

// 使用DecimalFormat#format
public static void scaleByDecimalFormat(String[] nums) {
    System.out.println("--------第三种方法--------");
    DecimalFormat df = new DecimalFormat("#.00");
    Arrays.stream(nums).forEach(o ->
        System.out.println(o+"-> "+df.format(new BigDecimal(o)))
    );
}

// 使用BigDecimal#setScale
public static void scaleByBigDecimal(String[] nums) {
    System.out.println("--------第四种方法--------");
    Arrays.stream(nums).forEach(o -> {
        BigDecimal num = new BigDecimal(o).setScale(2,BigDecimal.ROUND_HALF_UP);
        System.out.println(o+"-> "+num.toString());
    });
}

运行结果在情理之中,又在意料之外:

--------第一种方法--------
1.114-> 1.11
2.225-> 2.23
3.336-> 3.34
--------第二种方法--------
1.114-> 1.11
2.225-> 2.23
3.336-> 3.34
--------第三种方法--------
1.114-> 1.11
2.225-> 2.22
3.336-> 3.34
--------第四种方法--------
1.114-> 1.11
2.225-> 2.23
3.336-> 3.34

结果显而易见,大部分都符合我们的预期。但是中间好像混进了什么,为什么第三种方法 2.225 保留2位小数是 2.22?这就让引出了 四舍六入五留双(银行家舍入法).

四舍六入五留双

四舍六入五留双起源于银行家舍入法,简单的四舍五入太过简单粗暴,对于大量使用的场景来说,会产生大量误差,比如银行。
比如保留两位小数:

  • 需要舍的场景 0.000、0.001、0.002、0.003、0.004 总舍了:0.01
  • 需要入的场景 0.005、0.006、0.007、0.008、0.009 总入了:0.15

可以看到入的比舍的多,长此以往并且在大体量数据下,则肯定造成客户或者银行损失。
为了使舍和入能达到相互平衡,关键就在于 0.005 不能直接入,而是也要一定的几率入或者舍。所以银行家规定:为5时,当5后面还有数字时(全0不算),则进一位。如果5后面没有数字了,则判断前一位数字,为奇数则进位,为偶数则舍去。

总结即为:四舍六入五考虑,五后非空就进一,五后为空看奇偶,五前为偶应舍去,五前为奇要进一
比如保留两位小数:

  • 1.114 =  1.11 ----> 四舍
  • 1.116 =  1.12   ----> 六入
  • 2.2251 = 2.23 ----> 五后非空就进一
  • 2.225 = 2.22   ----> 五前为偶应舍去
  • 2.235 = 2.24   -----> 五前为奇要进一

java支持的舍入方法

除了最常用的四舍五入还是上面提到的四舍六入五留双,java还支持多种舍人方法。所以类型在枚举类 RoundingMode,当然里面的值还是用的 BigDecimal 类静态常量。DecimalFormat 的默认舍入规则是 HALF_EVEN

  1. ROUND_UP:远离零方向舍入。正数更大,负数更小,远离0。
  2. ROUND_DOWN:趋向零方向舍入。正数更小,负数更大,靠近0。
  3. ROUND_CEILING:趋向正无穷方向舍入。正数、负数都是更大,正数时类似 ROUND_UP,负数时类似 ROUND_DOWNMath#round 方法使用的该舍入模式。
  4. ROUND_FLOOR:趋向负无穷方向舍入。正数、负数都是更小,正数时类似 ROUND_DOWN,负数时类似 ROUND_UP
  5. ROUND_HALF_UP:趋向最接近数字方向舍入的舍入模式(五入)。四舍五入。
  6. ROUND_HALF_DOWN:趋向最接近数字方向舍入的舍入模式(五舍)。五舍六入。
  7. ROUND_HALF_EVEN:趋向最接近数字方向舍入的舍入模式(五留双)。银行家舍入法
  8. ROUND_UNNECESSARY:断言使用的,如果需要舍入操作,则抛异常。
    直接列出 Java 源码中注释的例子:
9f99320d2d9d92c7a55218f563ba95e4.png
每个舍入模式实例

保留位

经常我们有很多需求,比如千分位显示金额,金额强制保留两位小数,不足补0。下面探讨一下常用的事项方法。第一种方法逗号分隔显示,保留2位小数(四舍五入),不足补0.

DecimalFormat df = new DecimalFormat("###,##0.00");
BigDecimal bigDecimal = new BigDecimal("12312300.235").setScale(2,BigDecimal.ROUND_HALF_UP);
System.out.println(df.format(bigDecimal));
BigDecimal bigDecimal1 = new BigDecimal("12312300.2").setScale(2,BigDecimal.ROUND_HALF_UP);
System.out.println(df.format(bigDecimal1));

DecimalFormat df1= new DecimalFormat("###,##0.##");
System.out.println(df1.format(bigDecimal1));

DecimalFormat df = new DecimalFormat("###,##0.00"); #代表有则显示没有则不显示,而0则说明强制显示,没有则用0填充。

运行结果为

12,312,300.24
12,312,300.20
12,312,300.2

第二种方法
保留2位小数(四舍五入),不足补0。

System.out.println(String.format("%.2f", new BigDecimal("1.223")));
System.out.println(String.format("%.02f", new BigDecimal("1.223")));
System.out.println(String.format("%.02f", new BigDecimal("1.2")));
System.out.println(String.format("%.03f", new BigDecimal("1")));

%.2f: %. 表示 小数点前任意位数 , 2 表示两位小数, 格式后的结果为f 表示浮点型。%.02f: %. 表示 小数点前任意位数,0说明不足时补0, 2 表示两位小数,  格式后的结果为f 表示浮点型。

运行结果为

1.22
1.22
1.20
1.000

总结

从小学就学了四舍五入,但是生活中舍入的场景不仅只有四舍五入,向上、向下、更平衡的银行家。。。这些舍入模式Java都支持,科学在发展,社会在进步,java也一直与时俱进,我们作为Java的直接使用者也需不断学习,紧跟时代步伐。话说Java都快出16了......

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值