美文网首页吉良吉影Java 杂谈技术干货
Java浮点数计算精度问题总结

Java浮点数计算精度问题总结

作者: realqiyan | 来源:发表于2017-05-28 18:05 被阅读0次

    Java浮点数计算精度问题总结

    首先看看下面几个简单的加法计算的输出结果:

    System.out.println(0.1 + 0.2);  //输出:0.30000000000000004
    System.out.println(1.1 + 1.2);  //输出:2.3
    System.out.println(0.1f + 0.2f);//输出:0.3
    

    浮点数计算可能出问题的根本原因:

    IEEE754的浮点数世界里,0.1(单精度或双精度浮点数)并不是真正的0.1,0.2(单精度或双精度浮点数)也并不是真正的0.2,所以相加的值并不完全等于0.3。

    本文讨论下面这些问题:

    • 为什么浮点数计算会存在精度问题?
    • 为什么相同的两个数字相加,float和double计算的结果不一致?
    • 为什么小数位不变,整数位加1,double计算的结果不一致没有出现.30000000000000004?
    • 如何避免精度问题?

    浮点数标准

    首先简单了解一下浮点数标准,java中浮点数采用的IEEE754标准,该标准的全称为IEEE二进制浮点数算术标准。

    存储格式:符号位+指数位偏移+尾数位

    image.png

    IEEE 754常用的两种表示浮点数值的方式:单精确度(float 32位)、双精确度(double 64位)

    image.png

    规约形式的浮点数:

    如果浮点数中指数部分的编码值在0 < exponent < 2e-2之间,且尾数部分最高有效位(即整数字)是1,那么这个浮点数将被称为规约形式的浮点数。“规约”是指用唯一确定的浮点形式去表示一个值。
    由于这种表示下的尾数有一位隐含的二进制有效数字,为了与二进制科学计数法的尾数相区别,IEEE754称之为有效数(significant)。

    非规约形式的浮点数:

    如果浮点数的指数部分的编码值是0,尾数为非零,那么这个浮点数将被称为非规约形式的浮点数。

    IEEE 754标准规定:

    非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值大1.例如,最小的规约形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规约的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127。实际上非规约形式的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规约浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于1且小于2,而非规约浮点数的尾数小于1且大于0.

    精度

    在二进制,第一个有效数字必定是“1”,因此这个“1”并不会存储。
    单精和双精浮点数的有效数字分别是有存储的23和52个位,加上最左手边没有存储的第1个位,即是24和53个位。

    浮点数的比较

    浮点数基本上可以按照符号位、指数域、尾数域的顺序作字典比较。显然,所有正数大于负数;正负号相同时,指数的二进制表示法更大的其浮点数值更大。

    指数偏移值

    指数偏移值(exponent bias),是指浮点数表示法中的指数域的编码值为指数的实际值加上某个固定的值,IEEE 754标准规定该固定值为:2的e-1次方减1,其中的e为存储指数的位元的长度。

    以单精度浮点数为例,它的指数域是8个位元,固定偏移值是:2的7次方-1=127

    采用指数的实际值加上固定的偏移值的办法表示浮点数的指数,好处是可以用长度为e个位元的无符号整数来表示所有的指数取值,这使得两个浮点数的指数大小的比较更为容易,实际上可以按照字典序比较两个浮点表示的大小。

    浮点数转二进制数

    能精确表示的浮点数

    哪些小数能被精确表示呢?0.5的倍数,且在精度以内。

    方便计算,首先选择可以用浮点数精确表示的数计算:4.25

    step1.首先将数字转为2进制:
    整数部分4:
    4/2=2 余 0
    2/2=1 余 0
    1/2=0 余 1

    小数部分0.25
    0.25 * 2 = 0.5 未进位 0
    0.50 * 2 = 1 进位整数 1

    二进制表示:100.01

    step2.将二进制数转为科学计数法表示
    科学记数法表示:1.0001 * 2^2

    step3.转换为IEEE754格式存储
    符号位 0 (正数0 负数1)
    指数 2 (float指数+127 double指数+1023)
    尾数 0001

    单精度float:符号位0 指数位129(10000001) 尾数001
    0 10000001 00010000000000000000000

    双精度double:符号位0 指数位1025(10000000001) 尾数001
    0 10000000001 0001000000000000000000000000000000000000000000000000

    不能精确表示的浮点数

    举个例子:1/3,十进制就无法精确表示三分之一这个数字。

    二进制也有很多很多小数无法精确表示,包括:0.1和0.2,这也是导致计算出现精度问题的根本原因。

    下面将0.1和0.2转为2进制表示。

    0.1

    0.10 * 2 = 0.20 未进位 0
    0.20 * 2 = 0.40 未进位 0
    0.40 * 2 = 0.80 未进位 0
    0.80 * 2 = 1.60 进位 1
    0.60 * 2 = 1.20 进位 1
    0.20 * 2 = 0.40 未进位 0
    0.40 * 2 = 0.80 未进位 0
    0.80 * 2 = 1.60 进位 1
    0.60 * 2 = 1.20 进位 1
    0.20 * 2 = 0.40 未进位 0
    0.40 * 2 = 0.80 未进位 0
    0.80 * 2 = 1.60 进位 1
    0.60 * 2 = 1.20 进位 1
    0.20 * 2 = 0.40 未进位 0
    0.40 * 2 = 0.80 未进位 0
    0.80 * 2 = 1.60 进位 1
    0.60 * 2 = 1.20 进位 1
    0.20 * 2 = 0.40 未进位 0
    无限循环...
    二进制表示0.1:
    0.00011001100110011001100110011001100110011001100110011001...
    科学记数表示:
    1.1001100110011001100110011001100110011001100110011001... * 2^-4

    转换为IEEE754格式存储:
    符号位 0 (正数0 负数1)
    指数 -4 (float指数+127 double指数+1023)
    尾数 1001100110011001100110011001100110011001100110011001...

    float 单精度浮点数,尾数只能存储23位,多余位数四舍五入:
    0 01111011 10011001100110011001101

    double 双精度浮点数,尾数只能存储52位,多余位数四舍五入:
    0 01111111011 1001100110011001100110011001100110011001100110011010

    0.2

    0.20 * 2 = 0.40 未进位 0
    0.40 * 2 = 0.80 未进位 0
    0.80 * 2 = 1.60 进位 1
    0.60 * 2 = 1.20 进位 1
    0.20 * 2 = 0.40 未进位 0
    0.40 * 2 = 0.80 未进位 0
    0.80 * 2 = 1.60 进位 1
    0.60 * 2 = 1.20 进位 1
    0.20 * 2 = 0.40 未进位 0
    0.40 * 2 = 0.80 未进位 0
    0.80 * 2 = 1.60 进位 1
    0.60 * 2 = 1.20 进位 1
    0.20 * 2 = 0.40 未进位 0
    0.40 * 2 = 0.80 未进位 0
    0.80 * 2 = 1.60 进位 1
    0.60 * 2 = 1.20 进位 1
    0.20 * 2 = 0.40 未进位 0
    无限循环...
    二进制表示0.2:
    0.00110011001100110011001100110011001100110011001100110011...
    科学记数表示:
    1.10011001100110011001100110011001100110011001100110011... * 2^-3

    转换为IEEE754格式存储:
    符号位 0 (正数0 负数1)
    指数 -3 (float指数+127 double指数+1023)
    尾数 10011001100110011001100110011001100110011001100110011...

    float 单精度浮点数,尾数只能存储23位,多余位数四舍五入:
    0 01111100 10011001100110011001101

    double 双精度浮点数,尾数只能存储52位,多余位数四舍五入:
    0 01111111100 1001100110011001100110011001100110011001100110011010

    二进制浮点数相加

    小数点对其,两数相加。

    单精度浮点数:0.1f + 0.2f

       1.10011001100110011001101 2^-4
    + 11.00110011001100110011010 2^-4
    =100.11001100110011001100111 2^-4
    =  1.00110011001100110011010 2^-2
    =  0.0100110011001100110011010 * 
    

    计算结果:
    符号位:0
    指数位:-2+127 = 125
    尾数:00110011001100110011010
    ieee754: 0 01111101 00110011001100110011010
    转换为十进制数:0.300000011920928955078125
    转为float结果:0.3

    双精度浮点数:0.1 + 0.2

       1.1001100110011001100110011001100110011001100110011010 2^-4
    + 11.0011001100110011001100110011001100110011001100110100 2^-4  
    =100.1100110011001100110011001100110011001100110011001110 2^-4
    =  1.0011001100110011001100110011001100110011001100110100 2^-2
    =  0.010011001100110011001100110011001100110011001100110100 *
    

    计算结果:
    符号位:0
    指数位:-2+1023 = 1021
    尾数:0011001100110011001100110011001100110011001100110100
    ieee754: 0 01111111101 0011001100110011001100110011001100110011001100110100
    转换为十进制数:0.3000000000000000444089201865780
    转换为double结果:0.30000000000000004

    image.png

    浮点数计算

    要避免浮点数计算问题,可以通过BigDecimal来计算。

    //正确的姿势:
    System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));//输出:0.3
    
    //错误的姿势:
    System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));    //输出:0.3000000000000000166533453693773481063544750213623046875
    

    下一篇文章来分析BigDecimal如何对浮点数进行计算。

    相关文章

      网友评论

        本文标题:Java浮点数计算精度问题总结

        本文链接:https://www.haomeiwen.com/subject/hzyhfxtx.html