当前位置: 首页 > 科技观察

是不是觉得用了BigDecimal之后,计算结果一定是准确的?

时间:2023-03-21 17:53:57 科技观察

BigDecimal,相信很多人都不陌生,很多人都知道它的用法。这是java.math包中提供的一种类型,可用于精确计算。很多人都知道,在金额表示、金额计算等场景中,不能使用double、float等类型,而应该使用精度支持更好的BigDecimal。因此,在很多支付、电子商务、金融等业务中,BigDecimal的使用非常频繁。但是,如果你误以为只要用BigDecimal表示数字,结果就一定是准确的,那你就大错特错了!在之前的文章中,我们介绍过使用BigDecimal的equals方法无法验证两个数是否真的相等(为什么阿里巴巴禁止使用BigDecimal的equals方法进行相等比较?)。除了这种情况,使用BigDecimal的第一步是创建一个BigDecimal对象。如果这一步出了问题,那么后面的任何计算都会出错!那么如何正确创建BigDecimal呢?关于这个问题,我审查了很多代码,也采访了很多一线开发人员,也有不少人掉坑里。这是一个容易被忽视但意义重大的问题。关于这个问题,在《阿里巴巴Java开发手册》中有一个建议,或者要求:这是一个【强制性】的建议,那么这背后的原理是什么?要弄清楚这个问题,我们主要需要搞清楚以下几个问题:1.为什么double不准确?2、BigDecimal如何保证准确性?知道了这两个问题的答案后,我们大概就知道为什么BigDecimal(double)不能用来创建BigDecimal了。为什么double不准确?首先,计算机只认识二进制,也就是0和1,这个大家肯定都知道。然后,所有数字,包括整数和小数,如果要在计算机中存储和显示,都需要转换成二进制。将十进制整数转换为二进制非常简单。通常是“除以2,取余,倒序排列”。比如10的二进制值是1010,但是小数点用二进制怎么表示呢?十进制小数化成二进制一般采用“乘以2并取整排序”的方法。例如0.625转换成二进制表示为0.101。但是,并不是所有的小数都可以转换成二进制。例如0.1不能直接用二进制表示。它的二进制值为0.000110011001100……这是一个无限循环的小数。因此,计算机没有办法用二进制准确表示0.1。也就是说,在计算机中,很多小数是无法用二进制准确表示的。所以,这个问题必须要解决。好吧,人们想出了一种使用具有一定精度的近似值来表示小数的方法。这就是IEEE754(二进制浮点运算的IEEE标准)规范的主要思想。IEEE754规定了多种表示浮点值的方式,其中最常用的是32位单精度浮点数和64位双精度浮点数。在Java中,float和double分别用来表示单精度浮点数和双精度浮点数。所谓精度不同,可以简单理解为保留的有效位数不同。小数通过保留有效数字近似表示。所以大家就知道为什么double表示的小数不准确了。接下来我们回到BigDecimal的介绍。让我们来看看如何表示一个数字。它如何保证准确性?BigDecimal如何准确计数?如果你看过BigDecimal的源码,其实可以发现,一个BigDecimal就是一个数,由一个“未缩放的值”和一个“缩放”来表示。在BigDecimal中,比例由比例字段表示。未缩放值的表示更复杂。当未缩放的值超过阈值(默认为Long.MAX_VALUE)时,intVal字段用于存储未缩放的值,intCompact字段用于存储Long.MIN_VALUE。否则,将未缩放的值压缩存储在long类型的intCompact字段中,用于后续计算,intVal为空。涉及到的字段有这些:publicclassBigDecimalextendsNumberimplementsComparable{privatefinalBigIntegerintVal;privatefinalintscale;privatefinaltransientlongintCompact;}可以理解无标度值的压缩机制,这不是本文的重点。你只需要知道BigDecimal主要是通过一个无标度的Degree值和scale来表示就行了。那么尺度是多少?除了scale字段,BigDecimal中还提供了scale()方法来返回这个BigDecimal的小数位数。/***返回此{@codeBigDecimal}的比例。如果为零*或正数,则比例为*小数点右边的数字的个数。如果为负数,则*数字的未缩放值乘以*比例的负数的幂。例如,{@code-3}的比例表示未缩放的li0led*valueedismult0**1@returnthescaleofthis{@codeBigDecimal}.*/publicintscale(){returnscale;}那么,比例代表什么?其实上面的注释已经说的很清楚了:如果scale为零或者正数,那么这个值就代表了这个数小数点右边的位数。如果scale为负数,则数字的真实值需要乘以10的负数的绝对值次方。比如scale为-3,则数字需要乘以1000,即末尾有3个零。例如123.123用BigDecimal表示,它的非标度值是123123,它的标度是3。0.1不能用二进制表示,可以用BigDecimal表示,用非标度值1和标度表示1、我们都知道,如果要创建一个对象,就需要用到这个类的构造方法。BigDecimal中有四种构造方法:BigDecimal(int)BigDecimal(double)BigDecimal(long)BigDecimal(String)以上四种方法,创建的BigDecimal的scale是不同的。其中BigDecimal(int)和BigDecimal(long)比较简单,因为都是整数,所以小数位数都是0。BigDecimal(double)和BigDecimal(String)的小数位数有很多学问。BigDecimal(double)有什么问题BigDecimal提供了通过double创建BigDecimal的方法——BigDecimal(double),但是也给我们留下了一个坑!因为我们知道用double表示的小数是不准确的,比如对于数字0.1,double只能表示它的近似值。所以,当我们使用newBigDecimal(0.1)创建一个BigDecimal时,创建的值并不完全等于0.1。相反0.1000000000000000055511151231257827021181583404541015625。这是因为doule本身仅代表一个近似值。?所以,如果我们在代码中使用BigDecimal(double)创建一个BigDecimal,那么就会丢失精度,这是极其严重的。使用BigDecimal(String)创建那么,如何创建一个准确的BigDecimal来表示小数呢?答案是使用String来创建。至于BigDecimal(String),当我们使用newBigDecimal("0.1")创建一个BigDecimal时,创建的值恰好等于0.1。那么他的尺度是1。但是需要注意的是newBigDecimal("0.10000")和newBigDecimal("0.1")这两个数的标度分别是5和1。如果使用BigDecimal的equals方法进行比较,则结果为false。具体原因解决方法参考为什么阿里巴巴禁止使用BigDecimal的equals方法进行等价比较?那么,如果你想创建一个能够准确表示0.1的BigDecimal,请使用以下两种方法:BigDecimalrecommend1=newBigDecimal("0.1");BigDecimalrecommend2=BigDecimal.valueOf(0.1);这里留个思考题,BigDecimal.valueOf()是调用Double.toString方法实现的,那么既然double不准确,那么BigDecimal.valueOf(0.1)怎么保证准确呢?小结因为计算机是用二进制来处理数据的,但是有很多小数,比如0.1二进制就是无限循环的小数,这种数字在计算机中是无法准确表示的。因此,人们在计算机中采用一种近似的方法来表示,于是就有了单精度浮点数和双精度浮点数。因此,作为单精度浮点数的float和作为双精度浮点数的double在表示小数时只是近似值,不是真实值。因此,当使用BigDecimal(Double)创建一个时,生成的BigDecimal会失去精度。而且用一个已经失去精度的数来计算,结果也是不准确的。为避免此问题,可以通过BigDecimal(String)创建BigDecimal。在这种情况下,将准确表示0.1。它的表示是未缩放值1和缩放值1的组合。