Java中的BigDecimal和8种RoundingMode(舍入模式)分析

前言

相信大家对Java中的基本数据类型都已经很熟悉了,每种类型处理不同的数据,但是当有一个特别大的数字需要处理的时候,并且要求准确的精度时,你可能需要用到BigDecimal类。

有什么用?

先来执行一段代码,看看结果

System.out.println(0.2 + 0.1);
System.out.println(0.3 - 0.1);
System.out.println(0.2 * 0.1);
System.out.println(0.3 / 0.1);

执行结果如下:

0.30000000000000004
0.19999999999999998
0.020000000000000004
2.9999999999999996

对你没有看错,再执行一遍还是这个结果,奇怪了难道不应该是 0.3/0.2/0.2/0.3吗,这是为什么?

原因:

我们的计算机只能识别二级制,在进行数学计算时,参与计算的数据都是二进制,所以我们输入的十进制计算数据都会先转换为二进制,计算机用二进制计算完成后,再由二进制转换为十进制返回。

在这过程中,十进制转二进制时,有些数字是无法完全转换的,小数在转换为二进制时并不一定能用一个精确的二进制表示,大多数时候都是取的一个近似值,这就造成了精度的丢失。如果再用这个二进制进行计算,明显计算结果的精度会进一步丢失。终极原因还是因为精度丢失,

关于小数的二进制表示方法可以参考这里:《小数二进制的表示与转换》

针对这一问题,Java语言提供了java.math.BigDecimal类专门用来进行精确计算。

什么时候用?

在Java中float的精度为 7~ 8位有效数字,double精度为:16~17位有效数字,在不考虑精度丢失的情况下,如果要处理20位的数据或者30、40位的数据,此时就需要用到BigDecimal。

float和double只能用来做科学计算或者是工程计算,而且存在精度丢失问题,而涉及到较大数据,对精度有严格要求的计算中BigDecimal是首选,比如金融、银行业务、涉及到货币的业务上的计算都是使用BigDecimal进行数据处理的。

怎么用?

BigDecimal类被用在大数值的具体精度计算中,下面具体从如果创建一个BigDecimal对象,如何在多个BigDecimal对象间进行加减乘除计算等。

构造方法:

BigDecimal的构造方法有很多种,此处列举常用的几种

public BigDecimal(int val)
public BigDecimal(long val)
public BigDecimal(double val)
public BigDecimal(String val) 

通过这四个最基本的构造方法,即可创建Bigdecimal对象,但是这其中仍然有猫腻,且看下文,执行下面的代码:

public class BigDecimalDemo {
    public static void main(String[] args) {
        BigDecimal b1 = new BigDecimal(1);
        BigDecimal b2 = new BigDecimal(1.1);
        BigDecimal b3 = new BigDecimal(111L);
        BigDecimal b4 = new BigDecimal("1.1");
        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b3);
        System.out.println(b4);
    }
}

执行结果如下:

1
1.100000000000000088817841970012523233890533447265625
111
1.1

很显然,b2的值不是我们所期望的,BigDecimal中也存在精度丢失问题,我们可以看看JDK源码中public BigDecimal(double val)的说明

  • 参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中写入 newBigDecimal(0.1)所创建的BigDecimal正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。
  • String 构造方法是完全可预知的:写入 newBigDecimal("0.1") 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言,通常建议优先使用String构造方法。
  • 如果传入的参数必须是double类型时,可以通过 Double.toString(double b) 方法将double参数转换成字符串传入,或者直接使用 BigDecimal.valueOf(double b) 来进行接收处理

从源码中的方法注释可以看出,BigDecimal在处理double型数据时确实会出现精度问题,Java推荐我们使用String类型来作为构造方法的的入参。

加减乘除:

BigDecimal之间的运算不是基本数据类型的普通运算,而是自行封装好的四则运算,方法如下:

//加法
public BigDecimal add(BigDecimal value);
//减法
public BigDecimal subtract(BigDecimal value);                    
//乘法
public BigDecimal multiply(BigDecimal value);                   
//除法
public BigDecimal divide(BigDecimal value);                      

来个小栗子:

public class BigDecimalDemo {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("6.6");
        BigDecimal b = new BigDecimal("1.1");

        System.out.println("a + b =" + a.add(b));
        System.out.println("a - b =" + a.subtract(b));
        System.out.println("a * b =" + a.multiply(b));
        System.out.println("a / b =" + a.divide(b));
    }
}

输出结果如下:

a + b =7.7
a - b =5.5
a * b =7.26
a / b =6
其他方法:
//保留小数位(newScale:保留几位小数,roundingMode:舍入模式)
BigDecimal setScale(int newScale, int roundingMode)
//取余运算
BigDecimal remainder(BigDecimal divisor);
//求相反数
BigDecimal negate();
//比较大小(左边比右边数大返回1,相等返回0,比右边小返回-1)
int compareTo(BigDecimal val);
//将BigDecimal对象中的值以整数返回
int intValue() 
//将BigDecimal对象中的值以double类型返回
double doubleValue()
//将BigDecimal对象中的值以float类型返回
float floatValue()
//将BigDecimal对象中的值以long类型返回
long longValue()
//将BigDecimal对象中的值以String类型返回
String toString()

例子如下:

public class BigDecimalDemo {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("6.664");
        BigDecimal b = new BigDecimal("3");


        System.out.println("a四舍入五保留两位小数:"+a.setScale(2,RoundingMode.HALF_UP));
        System.out.println("a取余运算:"+a.remainder(b));
        System.out.println("a求相反数:"+a.negate());
        System.out.println("a,b比大小:"+a.compareTo(b));
        System.out.println("a以整数形式输出:"+a.intValue());
        System.out.println("a以double形式输出:"+a.doubleValue());
        System.out.println("a以float形式输出:"+a.floatValue());
        System.out.println("a以long形式输出:"+a.longValue());
        System.out.println("a以String形式输出:"+a.toString());
    }
}

输出如下:

a四舍入五保留两位小数:6.66
a取余运算:0.664
a求相反数:-6.664
a,b比大小:1
a以整数形式输出:6
a以double形式输出:6.664
a以float形式输出:6.664
a以long形式输出:6
a以String形式输出:6.664
舍入模式说明:

在BigDecimal中可以对高精度的浮点数根据不同的舍入模式进行数位保留操作。

舍入模式(Rounding Mode):是BigDecimal类中的静态变量,其中包括了八种常见舍入规则,比如四舍五入法、银行家舍入法等。

下面具体说明下舍入模式的分类

  • ROUND_UP

    向远离零的方向舍入。舍弃非零部分,并将非零舍弃部分相邻的一位数字加一

  • ROUND_DOWN

    向接近零的方向舍入。舍弃非零部分,同时不会非零舍弃部分相邻的一位数字加一,采取截取行为。

  • ROUND_CEILING

    向正无穷的方向舍入。如果为正数,舍入结果同ROUND_UP一致;如果为负数,舍入结果同ROUND_DOWN一致。注意:此模式不会减少数值大小。

  • ROUND_FLOOR

    向负无穷的方向舍入。如果为正数,舍入结果同ROUND_DOWN一致;如果为负数,舍入结果同ROUND_UP一致。注意:此模式不会增加数值大小。

  • ROUND_HALF_UP

    向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。
    如果舍弃部分>= 0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN相同。
    这种模式也就是我们常说的我们的“四舍五入”。

  • ROUND_HALF_DOWN

    向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则为向下舍入的舍入模式。
    如果舍弃部分> 0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN相同。
    这种模式也就是我们常说的我们的“五舍六入”。

  • ROUND_HALF_EVEN

    向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则相邻的偶数舍入。
    如果舍弃部分左边的数字奇数,则舍入行为与 ROUND_HALF_UP 相同;
    如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同。
    注意:在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。
    此舍入模式也称为“银行家舍入法”,主要在美国使用。
    四舍六入,被舍位为5时两种情况,如果前一位为奇数,则入位,否则舍去。

  • ROUND_UNNECESSARY

    断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。

空说理论看着头大,来一段示例代码

public class BigDecimalDemo {
    public static void main(String[] args) {
        BigDecimal aa = new BigDecimal("3.300");
        BigDecimal bb = new BigDecimal("3.344");
        BigDecimal cc = new BigDecimal("3.355");
        BigDecimal dd = new BigDecimal("3.356");
        BigDecimal ee = new BigDecimal("3.366");
        demo(aa,2);
        demo(bb,2);
        demo(cc,2);
        demo(dd,2);
        demo(ee,2);
    }

    public static void demo(BigDecimal bigDecimal,int scale){
        System.out.println();
        System.out.print(bigDecimal.toString()+"\t");
        //循环使用8种舍入模式
        for (int i =0 ;i<8;i++){
            try{
                System.out.print(bigDecimal.setScale(scale,i)+"\t");
            }catch (Exception e){
                System.out.print("ArithmeticException"+"\t");
            }

        }
    }
}

输出结果,从左到右每一列依次是测试数据、ROUND_UP、ROUND_DOWN、ROUND_CEILING…

3.300	3.30	3.30	3.30	3.30	3.30	3.30	3.30	3.30	
3.344	3.35	3.34	3.35	3.34	3.34	3.34	3.34	ArithmeticException	
3.355	3.36	3.35	3.36	3.35	3.36	3.35	3.36	ArithmeticException	
3.356	3.36	3.35	3.36	3.35	3.36	3.36	3.36	ArithmeticException	
3.366	3.37	3.36	3.37	3.36	3.37	3.37	3.37	ArithmeticException	

总结

BigDecimal类有效的解决了高精度、较大数据的运算,在涉及到货币、金融业务中广泛应用。

Java中的舍入模式在BigDecimal中运用十分广泛,具体的舍入规则可以参考源码中的方法注释,JDK中的注释十分十分详细。

使用BigDceimal时推荐使用new BigDecimal(String num)的形式来创建对象。

感谢和参考

Java 的四舍五入:http://wiki.jikexueyuan.com/project/java-enhancement/java-four.html

Java BigDecimal详解:https://www.cnblogs.com/LeoBoy/p/6056394.html

  • 9
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值