Java浮点型数据的精度问题

问题

在java中运行代码: System.out.println(2.00-1.10);
输出结果:0.8999999999999999
很奇怪,并不是我们想要的值0.9

再运行代码: System.out.println(2.00f-1.10f);
输出结果:0.9
又正确了,为什么会导致这种问题?程序中为什么要尽量避免浮点数比较?

float、double

单精度浮点数 - 维基百科双精度浮点数 - 维基百科

在java中浮点型默认是double的,及2.00和1.10都要在计算机里转换进行二进制存储,这就涉及到数据精度,出现这个现象的原因正是浮点型数据的精度问题。先了解下float、double的基本知识:

float和double是java的基本类型,用于浮点数表示,在java中float占4个字节32位,double占8个字节64位,一般比较适合用于工程测量计算中,其在内存里的存储结构如下:

float:

符号位(1 bit) 指数(8 bit) 尾数(23 bit)

double:

符号位(1 bit) 指数(11 bit) 尾数(52 bit)

注意:从左到右是从低位到高位,而在计算机内部是采用逆序存储的。

精度丢失

十进制整数转二进制数

算法很简单。举个例子,11表示成二进制数:

1
2
3
4
5
11/2=5 余 1
5/2=2 余 1
2/2=1 余 0
1/2=0 余 1
0结束

11二进制表示为(从下往上):1011

只要遇到除以后的结果为0了就结束了。所有整数除以2最终一定能够得到0,即所有整数都能用二进制精确表示,但小数并不都能用二进制精确表示。

十进制小数转二进制数

算法是乘以2直到基数为0(没有小数为止)。举个例子,0.9表示成二进制数

1
2
3
4
5
6
7
8
0.9*2=1.8 取整数部分1,基数=1.8-1=0.8
0.8*2=1.6 取整数部分1,基数=1.6-1=0.6
0.6*2=1.2 取整数部分1,基数=1.2-1=0.2
0.2*2=0.4 取整数部分0,基数=0.4
0.4*2=0.8 取整数部分0,基数=0.8
0.8*2=1.6 取整数部分1,基数=1.6-1=0.6 开始循环
0.6*2=1.2 取整数部分1,基数=1.2-1=0.2
.........

0.9二进制表示为(从上往下):111001100…..1100…..

上面的计算过程循环了,也就是说*2永远不可能消灭小数部分。

由此可知,有些小数无法用二进制精确表示。就像十进制无法精确表示出1/3,同样二进制也无法精确表示1/10。

二进制存储

System.out.println(2.00-1.10); 中的1.10不能被计算机精确存储,以double类型数据1.10举例,计算机如何将浮点型数据转换成二进制存储,

1.10整数部分就是1,转换成二进制1

小数部分:0.1

1
2
3
4
5
6
7
0.1*2=0.2 取整数部分0,基数=0.2
0.2*2=0.4 取整数部分0,基数=0.4
0.4*2=0.8 取整数部分0,基数=0.8
0.8*2=1.6 取整数部分1,基数=1.6-1=0.6
0.6*2=1.2 取整数部分1,基数=1.2-1=0.2
0.2*2=0.4 取整数部分0,基数=0.4 开始循环
.........

直至基数为0。1.1用二进制表示为:1.0001100110011….0011…(后面表示省略)

0.1 = 0*2^(-1)+0*2^(-2)+0*2^(-3)+1*2^(-4)+1*2^(-5)+......... 而double类型表示小数部分只有52位,当向后计算52位后基数还不为0,那后面的部分只能舍弃,从这里可以看出float、double并不能准确表示每一位小数,对于有的小数只能无限趋向它(所以有的数运行正常,有的数不正常)。在计算机中加减成除运算实际上最后都要在计算机中转换成二进制的加运算,由此,当计算机运行 System.out.println(2.00-1.10); 时会拿他们在计算机内存中的二进制表示计算,而1.10的二进制表示本身就不准确,所以会出现 0.8999999999999999 的结果。

但为什么 System.out.println(2.00f-1.10f); 得出的结果是 0.9 呢。因为float精度没有double精度那么大,小数部分0.1二进制表示被舍去的比较多

但是这不意味着float就是准确的 ,比如

1
2
float f = 1.0f - 0.9f;
System.out.println(f); // 运行结果为0.100000024

因此,有个原则:

  • 程序中应尽量避免浮点数的比较
  • float、double类型的运算往往都不准确

那该如何表示一个精准的?

在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用 java.math.BigDecimal

BigDecimal 有很多构造方法,其中两个是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Translates a {@code double} into a {@code BigDecimal} which
* is the exact decimal representation of the {@code double}'s
* binary floating-point value. The scale of the returned
* {@code BigDecimal} is the smallest value such that
* <tt>(10<sup>scale</sup> &times; val)</tt> is an integer.
* <p>
* <b>Notes:</b>
* <ol>
* <li>
* The results of this constructor can be somewhat unpredictable.
* One might assume that writing {@code new BigDecimal(0.1)} in
* Java creates a {@code BigDecimal} which is exactly equal to
* 0.1 (an unscaled value of 1, with a scale of 1), but it is
* actually equal to
* 0.1000000000000000055511151231257827021181583404541015625.
* This is because 0.1 cannot be represented exactly as a
* {@code double} (or, for that matter, as a binary fraction of
* any finite length). Thus, the value that is being passed
* <i>in</i> to the constructor is not exactly equal to 0.1,
* appearances notwithstanding.
*
* <li>
* The {@code String} constructor, on the other hand, is
* perfectly predictable: writing {@code new BigDecimal("0.1")}
* creates a {@code BigDecimal} which is <i>exactly</i> equal to
* 0.1, as one would expect. Therefore, it is generally
* recommended that the {@linkplain #BigDecimal(String)
* <tt>String</tt> constructor} be used in preference to this one.
*
* <li>
* When a {@code double} must be used as a source for a
* {@code BigDecimal}, note that this constructor provides an
* exact conversion; it does not give the same result as
* converting the {@code double} to a {@code String} using the
* {@link Double#toString(double)} method and then using the
* {@link #BigDecimal(String)} constructor. To get that result,
* use the {@code static} {@link #valueOf(double)} method.
* </ol>
*
* @param val {@code double} value to be converted to
* {@code BigDecimal}.
* @throws NumberFormatException if {@code val} is infinite or NaN.
*/
public BigDecimal(double val) {
this(val,MathContext.UNLIMITED);
}

public BigDecimal(String val) {
this(val.toCharArray(), 0, val.length());
}

源码中方法注释已描述的相当明确,通常建议优先使用String来够造BigDecimal

1
2
3
4
5
6
7
8
9
10
11
12
BigDecimal b = new BigDecimal(0.1);
System.out.println(b); // 输出 0.1000000000000000055511151231257827021181583404541015625

BigDecimal bg1 = new BigDecimal(0.9);
BigDecimal bg2 = new BigDecimal(1.0);
System.out.println(bg2.subtract(bg1)); // 输出 0.09999999999999997779553950749686919152736663818359375

BigDecimal bg3 = new BigDecimal("0.9");
BigDecimal bg4 = new BigDecimal("1.0");
System.out.println(bg4.subtract(bg3)); // 输出 0.1

System.out.println(BigDecimal.valueOf(0.1)); // 输出 0.1

Oracle Sql中不存在精度损失问题?

number类型数据计算似乎不存在精度损失问题,但是效率不如 浮点类型(binary_float、binary_double) ,可以在运算时转化为浮点型进行运算。