精度不精导致数据偏差的事实
//这是一段java代码
float a=1;
a+=0.999_999_999_999_995;
Systme.Out.println(a); //结果为2
浮点数存储原理
浮点类型的底层存储规定占位
float占用32bit,即4字节,存储规则:1+8+23,首位代表正负0正1负,8代表指位数,23来存储尾数。
double占用64bit,即8字节,存储规则为:1+11+52
如13.14如何存储在float呢:
13.14等于0.1314乘以10的2次方,指数为2,小数部分为0.1314
正数且指数位为2,所以前9位为0000 0001 0,真的是这样的?根据IEEE7规定,指数部分要加上指数可以表示范围的一半(float要加(二进制数1111 1111-1再除2,为127),double是1023),目的是可以表示指数为负的情况,打个比方让你拿十进制0-9来表示-4到5的数,是不是可以取4为中间数,当保存的数为3时,你可以加4,读取的时候再减4得到,当保存的数是-3时,可以加4为1,读取的时候减4就可以获得-3了,这样做的目的是,有限的数位可以一半表示负数,一半表示负数,节省了一个比特的占位空间。
127换成二进制为0111 1111,加上2就是1000 0001,所以13.14在float里存储的前9位比特为0100 0000 1,现在计算小数部分,通过小数的计算,就可以明白为什么浮点数的保存有时会存在精度丢失,而有时不会:
float存储小数有23位,为了方便推导,我们假设有4位来存储小数,小数部分为0.1314,现在一直乘以2,如果每次乘以2,结果整数位是为1,那么小数位填一个0,如果为1,填1,比如:0.13142=0.2624,由于整数为0,所以4位小数的第一位为0,0.2624再乘以2,结果为0.5248,所以下一位为0,即n位前2为此时为00,0.52482=1.0496,此时整数位为1,所以下一位为1,此时已计算了3位:001,再拿0.04962=0.0992,由于假设4位比特存储小数,最后小数部分为0010所以0.0992就舍弃了,也就是导致精度丢失的原因。
那么什么时候精度不会丢失呢,当某次乘以2的时候,恰好小数位为0,就没有精度丢失了,比如0.5的存储,0.52=1,那么小数位存储1,就可以代表不失精度的原数了。
为什么存储小数要一直乘以2来计算(这一点我查不到相关的解释,up主也没解释,哭笑不得),我们来用一下类比法,假设把一个十进制的数1.234来转换成n机制的数,n包括2,假设n=10,十进制转成十进制,是不是很荒谬,但是只要合乎常理n可以代表任何数的转换。现在拿float来存储1.234,但是每位比特位不是只能存储1和0,而是0到9,按照原来的逻辑已经不是再乘2了,乘10,0.123410^1,所以指数位为1+127,小数部分乘以10,0.123410=1.234,所以小数部分第一位为1,以此类推得到所有位:1234,所以小数位为1234,有没有体会到什么,是不是和二机制的原理一模一样的。(也许有一天我忘记了,也可以回头看看,这就是日志的好处吧,不知道自己还能不能看懂😂)
在 Java 中,小数常量默认为double类型,如何解决精度不够导致偏差问题
1.使用 BigDecimal 类型
由于 BigDecimal 类型是基于十进制表示的,因此可以避免使用 IEEE 754 浮点标准所带来的计算机舍入误差问题,并且可以提供高精度约数和精确的数值范围控制。因此,处理精度较高的小数,建议使用 BigDecimal 类型而非 double 类型,以保证计算精度。
2.同样使用于其他语言,高精度算法
把类似于0.9999999999999的数组用字符串表示“0.9999999999999”,再经过逻辑运算,其中涉及到一些算法知识,感兴趣可以换个频道学学,给个思路:把每位数拆成字符存储到数组里,高位放在后面,低位放在前面,想想平时怎么怎么进行加减乘除的,也许你以自己的实力也可以计算高精度的数值。