BigDecimal避坑原则

1、使用BIgDecimal表示和计算浮点数,务必使用字符串的构造方法来初始化BigDecimal

// 字符串方式
System.out.println(new BigDecimal("100")); // 输出 100 

不能调用BigDecimal传入Double的构造方法,但入参只有一个Double,如何转换为精确表达的BigDecimal呢?

来看看以下三种方式:

System.out.println(new BigDecimal(Double.toString(100))); // 100.0
System.out.println(new BigDecimal(String.valueOf(100d))); // 100.0
System.out.println(BigDecimal.valueOf(100d)); // 100.0

为什么多了1个0呢?原因是BigDecimal有scale和precision的概念,scale表示小数点右边的为数,而precision表示精度,也就是有效数字的长度。

调试以下可以发现,上述方式得到的BigDecimal的scale=1、precision=4;而new BigDecimal("100")得到的BigDecimal的scale=0、precision=3。

对于BigDecimal乘法操作,返回值的scale是两个数的scale相加:

System.out.println(new BigDecimal("1.123").multiply(new BigDecimal("100"))); // 112.300
System.out.println(new BigDecimal("1.123").multiply(new BigDecimal(Double.toString(100)))); // 112.3000

2、浮点数的字符串格式化通过BigDecimal进行

除了使用Double保存浮点数可能带来精度问题外,更匪夷所思的是这种精度问题加上String.format的格式化舍入方式,可能得到让人摸不着头脑的结果。

比如:

double n1 = 3.35;
float n2 = 3.35f;
System.out.println(String.format("%.1f", n1));
System.out.println(String.format("%.1f", n2));

得到的结果居然是3.4和3.3。

这就是由精度问题和舍入方式共同导致的,double和float的3.35其实相当于3.350xxx和3.349xxx,String.format采用四舍五入的方式进行舍入,取1位小数,doulble的3.350四舍五入为3.4,而float的3.349四舍五入为3.3。
Formatter类的相关源码中可以发现使用的舍入模式是HALF_UP:

 如果我们希望使用其他舍入方式来格式化字符串的话,可以设置DecimalFormat,如下代码:

double n1 = 3.35;
float n2 = 3.35f;
DecimalFormat format = new DecimalFormat("#.##");
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(n1));
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(n2));

当我们把这2个浮点数向下舍入取2位小数时,输出分别是3.35和3.34,还是有浮点数无法精确存储的问题。

因此,即使通过DecimalFormat来精确控制舍入方式,double和float的问题也可能产生意想不到的结果,所以浮点数的字符串格式化也要通过BigDecimal进行。

比如下面这段代码,使用BigDecimal来格式化数字3.35,分别使用向下舍入合和四舍五入方式取1位小数进行格式化:

BigDecimal n1 = new BigDecimal("3.35");
BigDecimal n2 = n1.setScale(1, BigDecimal.ROUND_DOWN);
System.out.println(n2);
BigDecimal n3 = n1.setScale(1, BigDecimal.ROUND_HALF_UP);
System.out.println(n3);

得到的结果是3.3和3.4,符合预期。

3、不要使用BigDecimal的equals判等

BigDecimal的equals方法比较是BigDecimal的value和scale,如果只希望比较value,可以使用compareTo方法。

BigDecimal的equals和hashCode方法会同时考虑value和scale,如果结合HashSet或HashMap使用的话就可能会出现麻烦。比如把值为1.0的BigDecimal加入HashSet,然后判断其是否存在值为1的BigDecimal,得到的结果是false。

Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(new BigDecimal("1.0"));
System.out.println(hashSet.contains(new BigDecimal("1"))); // 打印false

解决这个问题的办法有两个:

  • 第一个方法:使用TreeSet替换HashSet。TreeSet不使用hashCode方法,也不使用equals比较元素,而是使用compareTo方法,所以不会有问题。
Set<BigDecimal> hashSet = new TreeSet<>();
hashSet.add(new BigDecimal("1.0"));
System.out.println(hashSet.contains(new BigDecimal("1"))); // 打印true
  • 第二个方法:把BigDecimal存入HashSet或者HashMap前,先使用stripTrailingZeros方法去掉尾部的零,比较的时候也去掉尾部的0,确保value相同的BigDecimal,其scale也是一样的:
Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(new BigDecimal("1.0").stripTrailingZeros());
System.out.println(hashSet.contains(new BigDecimal("1.0000").stripTrailingZeros())); // 打印true

4、使用Math.xxExact方法进行数值运算

数值计算要小心溢出,不管是int还是long,所有的基本数值类型都是有超出表达范围的可能性。

比如,对Long的最大值进行+1操作,结果是一个负数,因为Long的最大值+1变为了Long的最小值。显然发生了溢出,但是是默默的溢出,并没有任何异常,这类问题非常容易被忽略,改进方式有以下两种。

  • 方法一:考虑使用Math类的addExact、subtractExact等xxExact方法进行数值运算,这些方法进行数值运算,这些方法可以在数值溢出时主动抛出异常。
    try {
            long n = Long.MAX_VALUE;
            System.out.println(Math.addExact(n, 1));
        } catch (Exception ex) {
            ex.printStackTrace();
        }

执行后,得到ArithmeticException,这是一个RuntimeException:
java.lang.ArithmeticException: long overflow
	at java.lang.Math.addExact(Math.java:809)
    ……
  • 方法二:使用大数类BigInteger。BigDecimal是处理浮点数的专家,而BigInteger则是对大数进行科学计算的专家。

如下代码,使用BigInteger对Long最大值进行+1操作;如果希望把计算结果转换成一个Long变量的话,可以使用BigInteger的longValueExact方法,在转换出现溢出时,同样会抛出ArithmeticException:

        BigInteger n = new BigInteger(String.valueOf(Long.MAX_VALUE));
        System.out.println(Long.MAX_VALUE);
        System.out.println(n.add(BigInteger.ONE));

        try {
            long l = n.add(BigInteger.ONE).longValueExact();
        } catch (Exception ex) {
            ex.printStackTrace();
        }


执行结果:
9223372036854775807
9223372036854775808
java.lang.ArithmeticException: BigInteger out of long range
	at java.math.BigInteger.longValueExact(BigInteger.java:4632)

可以看到,通过BigInteger对Long的最大值加1一点问题都没有,当尝试把结果转换为Long型时,则会提示BigInteger out of long range。

总结:

  1. 切记,要精确表示浮点数应该使用BigDecimal。并且,使用BigDecimal的Double入参的构造方法同样存在精度丢失问题,应该使用String入参的构造方法或者BigDecimal.valueOf方法来初始化。
  2. 对浮点数做精确计算,参与计算的各种数值应该始终使用BigDecimal,所有的计算都要通过BigDecimal的方法进行,切勿只是让BigDecimal来走过场。任何一个环境出现精度损失,最后的计算结果可能都会出现误差。
  3. 对于浮点数的格式化,如果使用String.format的话,需要认识到他使用的是四舍五入,可以考虑使用DecimalFormat来明确指定舍入方式。但考虑到精度问题,更建议使用BigDecimal来表示浮点数,并使用其setScale方法指定舍入的为数和方式。
  4. 进行数值运算时要小心溢出问题,虽然溢出后不会出现异常,但是得到的计算结果是完全错误的。可以考虑使用Math.xxxExact方法来进行运算,在溢出时能抛出异常,更建议对于可能会出现溢出的大数运算使用BigInteger类。

总之,对于金融、科学计算等场景,需要尽可能使用BigInteger和BigDecimal,避免由精度和溢出问题引发难以发现但影响重大的Bug。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值