double会丢失精度,Bigdecimal就一定安全吗

文章探讨了在Java开发中使用BigDecimal处理精确数值计算的优点,强调了避免浮点数精度问题、正确构造BigDecimal对象、指定舍入模式以及注意equals()与compareTo()方法的区别。同时也提到了转换为double可能导致的精度损失和BigDecimal的性能局限性。
摘要由CSDN通过智能技术生成

double会丢失精度,BigDecimal就一定安全吗?

在日常开发中,我们经常要处理金额、利率等精确的数值计算,这要求我们必须使用高精度的计算方式,而为了避免浮点数运算中可能引发的精度丢失问题,Java提供了BigDecimal类,它被设计用于处理精确的十进制数值。

BigDecimal是Java中用于处理高精度数值计算的类,它提供了丰富的方法来支持基本的数学运算,包括加法、减法、乘法和除法等。除了基本运算,BigDecimal还支持比较操作和取整操作,允许设置小数位数、指定舍入模式等。此外,它还提供了一系列附加功能,例如转换为科学计数法、格式化输出以及判断是否为整数等。因此,通过利用BigDecimal的强大功能,我们可以确保在涉及复杂数值计算时获得准确而可靠的结果。

我们都知道在使用double时会出现精度丢失的问题,如Double.valueOf(String);并且在运算特别是减法和除法运算时就会出现各种各样的精度问题,如下:

double aa = 1.0001-0.9;  //期望结果0.1001
double bb = 0.9-0.8; //期望结果0.1
double cc = Double.valueOf("0.100000000000012345"); //期望结果0.100000000000012345
System.out.println(aa);
System.out.println(bb);
System.out.println(cc);
/*****实际结果******/
0.10009999999999997
0.09999999999999998
0.10000000000001234

所以,在进行数值计算特别是涉及到金额和利率的数值计算时,是及其不推荐使用double类型进行运算的。不过使用BigDecimal就能完全避免计算问题而没有后顾之忧吗?当然不是。

在使用BigDecimal的过程中,如果我们使用不当的话,也会造成一些莫名其妙的bug。下面我们就来聊一聊使用BigDecimal不当所带来的"灾难"。

BigDecimal构造器使用不当

假如现在有一个浮点类型值0.01,我们要给转换为BigDecimal,以便我们做一些运算。那么我们就会这么做:

BigDecimal decimal1 = new BigDecimal(0.01);  
System.out.println(decimal1);  
BigDecimal decimal2 = new BigDecimal("0.01");  
System.out.println(decimal2);  
BigDecimal decimal3 = BigDecimal.valueOf(0.01);  
System.out.println(decimal3);

那么这三个BigDecimal的对象的值各是多少呢?输出结果如下:

0.01000000000000000020816681711721685132943093776702880859375
0.01
0.01

为何出现如此差异呢?

当使用new BigDecimal(0.01)时,传入的是浮点数,因为浮点数在计算机中以二进制形式表示,0.01在二进制中是无限循环小数,导致转换为BigDecimal时发生了精度丢失。

而使用new BigDecimal("0.01")时,由于字符串表示形式准确地包含了小数点和所有位数信息,因此这个构造函数能够精确无误地解析并存储这个数值,不存在浮点数精度问题。也是官方推荐的构建BigDecimal的方式。

我们使用BigDecimal.valueOf(0.01)时,方法首先通过 Double.toString(0.01) 将传入的 double 类型数值转换为其标准字符串表示形式,在通过new BigDecimal(Strin val)去构建BigDecimal。其源码如下:

public static BigDecimal valueOf(double val) {  
    // Reminder: a zero double returns '0.0', so we cannot fastpath  
    // to use the constant ZERO.  This might be important enough to    
    // justify a factory approach, a cache, or a few private    
    // constants, later.    
    return new BigDecimal(Double.toString(val));  
}

使用equals()方法进行数值比较

假如我们要比较下面两个值的大小:

BigDecimal decimal6 = new BigDecimal("0.01");  
BigDecimal decimal7 = new BigDecimal("0.010");  
System.out.println(decimal6.equals(decimal7));  
System.out.println(decimal6.compareTo(decimal7));

输出结果如下:

false
0 

结果显示equals()方法返回不相等,而compareTo**()方法返回相等。为什么会出现如此的差异?

我们先看一下BigDecimalequals(),发现BigDecimal重写了equals()方法:

@Override  
public boolean equals(Object x) {  
    if (!(x instanceof BigDecimal))  
        return false;  
    BigDecimal xDec = (BigDecimal) x;  
    if (x == this)  
        return true;  
    // 比较两个值的精度    
    if (scale != xDec.scale)  
        return false;  
    long s = this.intCompact;  
    long xs = xDec.intCompact;  
    if (s != INFLATED) {  
        if (xs == INFLATED)  
            xs = compactValFor(xDec.intVal);  
        return xs == s;  
    } else if (xs != INFLATED)  
        return xs == compactValFor(this.intVal);  

    return this.inflated().equals(xDec.inflated());  
}

方法的注释中写道:与 compareTo 不同,此方法仅当两个 BigDecimal 对象在值和小数位数上相等时才认为它们相等(因此,使用此方法进行比较时,2.0 不等于 2.00)。不仅比较两个 BigDecimal 实例的数值是否相等,还要求它们的标度(scale)完全一致。

compareTo() 方法则专门针对数值大小进行了设计,它只比较两个 BigDecimal 对象的实际数值大小,不关心它们的标度差异。这意味着即使两个 BigDecimal 对象的小数位数不同,只要数值相同,compareTo() 方法也能正确地判断它们在数值上的相等性。

在实际开发中,当你仅需要判断两个 BigDecimal 是否数值相等时,通常推荐使用 compareTo() 方法,并检查返回值是否为 0,而不是直接使用 equals() 方法。如果确实需要同时判断数值与标度,则可以使用 equals() 方法。

除法运算未指定舍入模式

使用BigDecimal进行除法运算时,一定要正确的使用舍入模式,避免舍入误差引起的问题,并且有时候出现结果是无限小数,程序会抛出异常。例如:

BigDecimal decimal8 = new BigDecimal("1.0");  
BigDecimal decimal9 = new BigDecimal("3.0");  
BigDecimal decimal10 = decimal8.divide(decimal9);  
System.out.println(decimal10);

此时会抛出异常:

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

divide()方法的结果是一个非终止(无限)的小数扩展,并且没有精确的十进制表示形式可以完全存储到 BigDecimal 对象中时,就会触发这个异常。如果不指定合适的舍入模式或精度,当除不尽的结果无法准确转换为有限长度的十进制数字时,就会抛出此异常。

为了避免这个异常,我们需要制定结果精度:

BigDecimal decimal8 = new BigDecimal("1.0");  
BigDecimal decimal9 = new BigDecimal("3.0");  
BigDecimal decimal10 = decimal8.divide(decimal9, 2, RoundingMode.HALF_UP);  
System.out.println(decimal10);

输出结果:

0.33

关于舍入模式的值如下:

RoundingMode.UP:向远离零的方向舍入
RoundingMode.DOWN:向靠近零的方向舍入
RoundingMode.CEILING:向正无穷方向舍入
RoundingMode.FLOOR:向负无穷方向舍入
RoundingMode.HALF_UP:四舍五入,如果舍弃部分大于等于 0.5(最常用)
RoundingMode.HALF_DOWN:四舍五入,如果舍弃部分大于 0.5
RoundingMode.HALF_EVEN:银行家舍入法,遵循 IEEE 754 标准

当然我们在指定舍入模式时一定要谨慎,要充分了解精度设置所带来的误差对于业务的影响。

转换double丢失精度

我们在使用doubleValue()方法将BigDecimal转为double可能引入精度丢失。

BigDecimal decimal11 = new BigDecimal("3.141592653589793238");  
double d = decimal11.doubleValue();  
System.out.println(d);

输出:

3.141592653589793

BigDecimal 类型转换为 double 类型时可能会出现精度问题。由于 double 是一种 IEEE 754 标准的二进制浮点数格式,它在计算机内存中是以近似值存储的,因此无法精确表示所有十进制小数。

当一个 BigDecimal 对象包含不能被精确表示为 double 类型的小数值时,转换过程中会发生舍入误差。在实际业务中如果要求高精度计算或绝对无损的数值转换,应该避免直接将 BigDecimal 转换为 double,应当保持使用 BigDecimal 进行计算以确保精度。

总结

在处理精确数值计算时,BigDecimal是Java中一个强大的工具,但在使用过程中需要注意一些潜在的问题。避免使用浮点数构造BigDecimal,而是使用String类型的构造器,在进行除法运算时一定要指定舍入模式,谨慎指定舍入模式,使用compareTo比较BigDecimal的值,要使用BigDecimal进行高精度的运算。

不过,建议在需要精确的小数计算时再使用BigDecimal,BigDecimal的性能比double和float差,在处理庞大,复杂的运算时尤为明显,故一般精度的计算没必要使用BigDecimal,特别是基本的加、乘运算且小数低于16位时还是建议使用double进行快速计算。

另外,BigDecimal都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值