龙空技术网

聊聊Java中浮点丢失精度的事

java小悠 155

前言:

如今你们对“java取小数点”可能比较关切,看官们都需要剖析一些“java取小数点”的相关知识。那么小编也在网摘上收集了一些对于“java取小数点””的相关文章,希望姐妹们能喜欢,大家一起来学习一下吧!

在说这个之前,我们先看看十进制到二进制的转换过程

整数的十进制到二进制的转换过程

用白话说这个过程就是不断的除2,得到商继续除,直到商小于1为止,然后他每次结果的余数倒着排列出来就是它的二进制结果了,直接上图

说一下为什么倒着排列就是二进制结果哈

通俗点说就是整数是一步一步除下来的,那回去不得一步一步乘上去?也就是说从上到下就是二进制从低位到高位的过程。

小数十进制到二进制的转换过程

小数的十进制到二进制的转换其实和整数类似,只不过算的方式变成了乘法,也就是用小数不断的乘2,然后得到的结果的整数部分拿出来,接着剩下的小数部分继续乘2,直到小数部分为0为止,直接上图~

二进制结果中的二分之一是转换后的,其实就是2的-1次方,-2次方。。。

当然了,小数转二进制的过程中,很多情况下都是无尽的,接着上图

所以可以看到这样的循环下去是得不到二进制的结果的,所以计算机就要进行取舍。也就是IEEE 754规范

IEEE 754规范

IEEE 754规定了四种标识浮点数值的方式,单精确度(32位),双精确度(64位),延伸单精确度(43比特以上,很少用)和延伸双精确度(79比特以上,通常80位)

最常用的还是单精确度和双精确度,也就是对标的float和double。但是IEEE 754规范并没有解决精确标识小数的问题,只是提供了一种用近似值标识小数的方式。而且精确度不同近似值也会不同。# 为什么会精度丢失?教你看懂 IEEE-754!

下面有个例子来看一下丢失精度的问题,如0.1+0.2 0.1的64位二进制:0.00011001100110011001100110011001100110011001100110011001 0.2的64位二进制:0.00110011001100110011001100110011001100110011001100110011 二者相加的结果为:0.30000000000000004

那么如何解决精度问题呢?

BigDecimal

BigDecimal使用java.math包提供的,在涉及到金钱相关的计算的时候都需要使用它,而且其中提供了大量的方法,比如加减乘除都是可以直接调用的。

先看这个问题,BigDecimal中的比较问题

先看下面这个例子

java复制代码public class ReferenceDemo { public static void main(String[] args) { BigDecimal bigDecimal1 = new BigDecimal(1); BigDecimal bigDecimal2 = new BigDecimal(1); System.out.println(bigDecimal1.equals(bigDecimal2)); BigDecimal bigDecimal3 = new BigDecimal(1); BigDecimal bigDecimal4 = new BigDecimal(1.0); System.out.println(bigDecimal3.equals(bigDecimal4)); BigDecimal bigDecimal5 = new BigDecimal("1"); BigDecimal bigDecimal6 = new BigDecimal("1.0"); System.out.println(bigDecimal5.equals(bigDecimal6)); } }

结果为:

其中第二个例子和第三个例子的不同是需要聊一聊的。为什么会出现这种呢?下面是BigDecimal中的equals的源码。

java复制代码public boolean equals(Object x) { if (!(x instanceof BigDecimal)) return false; BigDecimal xDec = (BigDecimal) x; if (x == this) return true; //关键在这一行,比较了scale 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()); }

由上面的注释可以看到BigDecimal中有一个很关键的属性,就是scale,标度。标度是什么? 首先看一下BigDecimal的结构

java复制代码public class BigDecimal extends Number implements Comparable<BigDecimal> { /** * The unscaled value of this BigDecimal, as returned by {@link * #unscaledValue}. * * @serial * @see #unscaledValue */ private final BigInteger intVal; /** * The scale of this BigDecimal, as returned by {@link #scale}. * * @serial * @see #scale */ private final int scale; // Note: this may have any value, so // calculations must be done in longs /** * If the absolute value of the significand of this BigDecimal is * less than or equal to {@code Long.MAX_VALUE}, the value can be * compactly stored in this field and used in computations. */ private final transient long intCompact; }

我截取了几个关键字段,依次看一下:

intVal: 无标度值

scale: 标度

intCompact: 当intVal超过阈值(默认为Long.MAX_VALUE)时,进行压缩运算,结果存到这个字段上,用于后续计算。

注释中解释到,scale为0或者正数的时候代表数字小数点之后的位数,如果scale为负数,代表数字的无标度值需要乘10的该负数的绝对值的幂,即末尾有几个0

比如123.123这个数,他的intVal就是123123,scale就是3了

而二进制无法标识0.1,通过BigDecimal标识的话,它的intVal就是1,scale也是1。

接着看回上面的例子,传入的参数是字符串的bigDecimal5和bigDecimal6,为什么就返回了false。上图

他们的标度是不同的,所以直接返回了false,那么在看bigDecimal3和bigDecimal4的比较,为什么就返回了true呢,同样上图

可以看到他们的intVal和scale都是相等的,但是明明传入了不同的,有兴趣的可以取看看源码,找一些资料,对于1.0这个数,它本质上也是一个整数,经过一系列的运算他的intVal还是1,scale还是0,所以比较之后返回的是true。

这时候就能看出来equals方法的一些问题了,用equals涉及到scale的比较,实际的结果可能和预期不一样,所在BigDecimal的比较推荐用compareTo方法,如果返回0,代表相等

java复制代码BigDecimal bigDecimal5 = new BigDecimal("1"); BigDecimal bigDecimal6 = new BigDecimal("1.0"); System.out.println(bigDecimal5.compareTo(bigDecimal6));

说到这里同时提一下,不要用传参为double的构造方法,同样会丢失精度,如果需要小数,需要传入字符串的小数来获取BigDecimal的实例对象。

说到这其实应该明白了他是怎么保证精度的了,其实关键点就是scale,这个标度贯穿了整个过程,加减乘除的运算都需要它来把控。上面说了其实2个参数最为关键,一个是无标度值,一个是标度,无标度值就是整数了,以加法为例子,不就可以变成整数的加法了吗,然后用scale控制小数点,说是这么说,实现过程还是很复杂的,有兴趣的可以自己查资料去学习。

除了用字符串代替double来表示BigDecimal的小数,其实也可以通过BigDecimal.valueOf()方法,它传入double之后可以和字符串一样的效果,为啥呢?上代码

java复制代码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)); }

它把传入的double给toString了。。。。

原文链接:

标签: #java取小数点