java浮点数剖析

定点数表达法的缺点在于其形式过于僵硬,固定的小数点位置决定了固定位数的整数部分和小数部分,不利于同时表达特别大的数或者特别小的数。
计算机系统采纳了所谓的浮点数表达方式。这种表达方式利用科学计数法来表达实数,即用一个尾数(Mantissa也叫有效数字 ),一个基数(Base),一个指数(Exponent)以及

一个表示正负的符号来表达实数。浮点数利用指数达到了浮动小数点的效果,从而可以灵活地表达更大范围的实数。

当一个浮点数的尾数为0,不论其阶码为何值,该浮点数的值都为0。当阶码的值为它能表示的最小一个值或更小的值时,不管其尾数为何值,计算机都把该浮点数看成零值,通常称

其为机器零,此时该浮点数的所有各位(包括阶码位和尾数位)都清为0值。

Java 平台上的浮点数类型 float 和 double 采纳了 IEEE 754 标准中所定义的单精度 32 位浮点数和双精度 64 位浮点数的格式。

在 IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符号,指

数和尾数。这样,通过尾数和可以调节的指数就可以表达给定的数值了。具体的格式参见下面的图例:


 
在上面的图例中,第一个域为符号域。其中 0 表示数值为正数,而 1 则表示负数。

第二个域为指数域,对应于我们之前介绍的二进制科学计数法中的指数部分。其中单精度数为 8 位,双精度数为 11 位。以单精度数为例,8 位的指数为可以表达 0 到 255 之间

的 255 个指数值(注:指数8位的最高位都是数值位,没有符号位)。但是,指数可以为正数,也可以为负数。为了处理负指数的情况,实际的指数值按要求需要加上一个偏差

(Bias)值作为保存在指数域中的值,单精度数的偏差值为 127(2^7-1),而双精度数的偏差值为1023(2^10-1)。比如,单精度的实际指数值 0 在指数域中将保存为 127;而

保存在指数域中的 64 则表示实际的指数值 -63(64-127=-63)。 偏差的引入使得对于单精度数,实际可以表达的指数值的范围就变成 -127(表示小数点需向左移动127位) 到

128(表示小数点需向右移动128位) 之间(包含两端)。我们不久还将看到,实际的指数值 -127(全 0)以及 +128(全 1)保留用作特殊值的处理。这样,实际可以表达的有效

指数范围就在 -127 和 127 之间。

第三个域为尾数域,其中单精度数为 23 位长,双精度数为 52 位长。除了我们将要讲到的某些特殊值外,IEEE 标准要求浮点数必须是规范的。这意味着尾数的小数点左侧必须为

1,因此我们在保存尾数的时候,可以省略小数点前面这个 1,从而腾出一个二进制位来保存更多的尾数。这样我们实际上用 23 位长的尾数域表达了 24 位的尾数。比如对于单精

度数而言,二进制的 1001.101(对应于十进制的 9.625)可以表达为 1.001101 × 2^3,所以实际保存在尾数域中的值为 00110100000000000000000,即去掉小数点左侧的 1,并

用 0 在右侧补齐。
值得注意的是,对于单精度数,由于我们只有 24 位的尾数(其中一位隐藏),所以可以表达的最大尾数为 2^24- 1 = 16,777,215。特别的,16,777,216 是偶数,所以我们可以

通过将它除以 2 并相应地调整指数来保存这个数,这样 16,777,216 同样可以被精确的保存。相反,数值 16,777,217 则无法被精确的保存。由此,我们可以看到单精度的浮点数

可以表达的十进制数值中,真正有效的数字不高于 8 位。事实上,对相对误差的数值分析结果显示有效的精度大约为 7.22 位(由于位数不可取小数,所以单精度的精度为7,即

可精确到小数点后7位)。参考下面的示例:

Java代码  

  1. System.out.println(16777215f);//1.6777215E7
  2. System.out.println(16777216f);//1.6777216E7
  3. System.out.println(16777217f);//1.6777216E7
  4. System.out.println(16777218f);//1.6777218E7
  5. System.out.println(16777219f);//1.677722E7
  6. System.out.println(16777220f);//1.677722E7
  7. System.out.println(16777221f);//1.677722E7
  8. System.out.println(16777222f);//1.6777222E7
  9. System.out.println(16777223f);//1.6777224E7
  10. System.out.println(16777224f);//1.6777224E7
  11. System.out.println(16777225f);//1.6777224E7

请看结果推导分析:

111111111111111111111111          16777215f
1.11111111111111111111111          刚好是23位,不会丢失精度,能精确表示
0 23+127 11111111111111111111111
0 10010110 11111111111111111111111

1000000000000000000000000         16777216f
1.00000000000000000000000 0        去掉的是0,所以还是能准确表示
0 24+127 00000000000000000000000
0 10010111 00000000000000000000000

1000000000000000000000001          16777217f
1.00000000000000000000000 1        不能准确表示。先试着进位
1.00000000000000000000001          由于进位后,结果的最末们不是0,所以直接舍掉
1.00000000000000000000000          到这里结果就是16777216f

1000000000000000000000010         16777218f
1.0000000000000000000001 0         去掉的是0,所以还是能准确表示
0 24+127 00000000000000000000001
0 10010111 00000000000000000000001

1000000000000000000000011         16777219f
1.0000000000000000000001 1         不能准确表示。先试着进位
1.0000000000000000000010           进位后的结果,最末们为0,所以进位成功
0 24+127 00000000000000000000010
0 10010111 00000000000000000000010
1.000000000000000000000100*2^24    16777219f存储在内存中的结果实质上为16777220

........

根据标准要求,无法精确保存的值必须向最接近的可保存的值进行舍入。这有点像我们熟悉的十进制的四舍五入,0就舍,但1不一定就进,而是在前后两个等距接近的可保存的值

中,取其中最后一位有效数字为零者(即先试着进1,会得到最后结果,然后看这个结果的尾数最后位是否为0,如果是则进位,否则直接舍去)。从上面的示例中可以看出,奇数

都被舍入为偶数(舍入到偶数有助于从某些角度减小计算中产生的舍入误差累积问题。因此为 IEEE 标准所采用),且有舍有进。我们可以将这种舍入误差理解为"半位"的误差。

所以,为了避免 7.22 对很多人造成的困惑,有些文章经常以 7.5来说明单精度浮点数的精度问题。

假定我们有一个 32 位的数据,用十六进制表示为 0xC0B40000,并且我们知道它实际上是一个单精度的浮点数。为了得到该浮点数实际表达的实数,我们首先将它变换为二进制形

式:
1100 0000 1011 0100 0000 0000 0000 0000
接着按照浮点数的格式切分为相应的域:
1 10000001 01101000000000000000000
符号域 1 意味着负数;指数域为 129 意味着实际的指数为 2 (减去偏差值 127);尾数域为 01101 意味着实际的二进制尾数为 1.01101 (加上隐含的小数点前面的 1)。所以

,实际的实数为:
-1.01101 × 2^2 = -101.101 = -5.625
或使用Java也可计算出:

Java代码  

  1. /*
  2. 注:Float.intBitsToFloat方法是将内存中int数值的二进制看作是float的
  3. 二进制制,这样很方便使用一个二进制位来构造一个float。
  4. */
  5. System.out.println(Float.intBitsToFloat(0xc0B40000));//-5.625

从实数向浮点数变换稍微麻烦一点。假定我们需要将实数 -9.625 表达为单精度的浮点数格式。方法是首先将它用二进制浮点数表达,然后变换为相应的浮点数格式。
首先,将小数点左侧的整数部分变换为其二进制形式,9 的二进制性形式为 1001。处理小数部分的算法是将我们的小数部分乘以基数 2,记录乘积结果的整数部分,接着将结果的

小数部分继续乘以 2,并不断继续该过程:
0.625 × 2 = 1.25   1
0.25× 2 = 0.5        0
0.5× 2 = 1            1
                            0
当最后的结果为零时,结束这个过程。这时右侧的一列数字就是我们所需的二进制小数部分,即 0.101。这样,我们就得到了完整的二进制形式 1001.101。用规范浮点数表达为

1.001101 × 2^3。
因为是负数,所以符号域为 1。指数为 3,所以指数域为 3 + 127 = 130,即二进制的 10000010。尾数省略掉小数点左侧的 1 之后为 001101,右侧用零补齐。最终结果为:
1 10000010 00110100000000000000000
最后可以将浮点数形式表示为十六进制的数据如下:
1100 0001 0001 1010 0000 0000 0000 0000
或使用Java也可计算出:

Java代码  

  1. /*
  2. 注:Float.floatToIntBits:将内存中的float型数值的二进制看作是对应
  3. 的int类型的二进制,这样很方便的将一个float的内存数据以二进制来表示。
  4. 11000001000110100000000000000000
  5. */
  6. System.out.println(Integer.toBinaryString(Float.floatToIntBits(-9.625F)));

很简单?等等!你可能已经注意到了,在上面这个我们有意选择的示例中,不断的将产生的小数部分乘以 2 的过程掩盖了一个事实。该过程结束的标志是小数部分乘以 2 的结果

为 1,不难想象,很多小数根本不能经过有限次这样的过程而得到结果(比如最简单的 0.1)。我们已经知道浮点数尾数域的位数是有限的,为此,浮点数的处理办法是持续该过

程直到由此得到的尾数足以填满尾数域,之后对多余的位进行舍入。换句话说,除了我们之前讲到的精度问题之外,十进制到二进制的变换也并不能保证总是精确的,而只能是近

似值。事实上,只有很少一部分十进制小数具有精确的二进制浮点数表达。再加上浮点数运算过程中的误差累积,结果是很多我们看来非常简单的十进制运算在计算机上却往往出

人意料。这就是最常见的浮点运算的"不准确"问题。参见下面的 Java 示例:

Java代码  

  1. // 34.6-34.0=0.5999985
  2. System.out.print("34.6-34.0=" + (34.6f-34.0f));

产生这个误差的原因是 34.6 无法精确的表达为相应的浮点数,而只能保存为经过舍入的近似值。这个近似值与 34.0 之间的运算自然无法产生精确的结果。

当指数、尾数都为0,则规定该浮点数为0。
当指数255(全1),且尾数为0时,表示无穷大,用符号位来确定是正无穷大还是负无穷大。
当指数255(全1),且尾数不为0时,表示NaN(不是一个数)。
最大的float数:0,11111110,1111111 11111111 11111111   用10进制表示约为   +3.4E38
最小的float数:1,11111110,1111111 11111111 11111111   用10进制表示约为   -3.4E38
绝对值最小的float数:0,00000000,0000000 00000000 00000001和1,00000000,0000000   00000000 00000001

浮点数的精度:
单精度数的尾数用23位存储,加上默认的小数点前的1位1,2^(23+1) = 16777216。因为 10^7 < 16777216 < 10^8,所以说单精度浮点数的有效位数(精度)是7位(即小数点后7

位)。
双精度的尾数用52位存储,2^(52+1) = 9007199254740992,10^16 < 9007199254740992 < 10^17,所以双精度的有效位数(精度)是16位(即小数点后16位)。
如果你在浮点数的有效位后增加数字的话,结果是不会变化的:

Java代码  

  1. System.out.println(1.67772156F);//1.6777215
  2. System.out.println(1.67772150123456789D);//1.6777215012345679

float取值范围:
负数取值范围为 -3.4028235E+38 ~ -1.401298E-45,正数取值范围为 1.401298E-45 ~ 3.4028235E+38。
Float.MIN_VALUE:保持 float 类型数据的最小正非零值的常量,最小正非零值为2^-149。该常量等于十六进制的浮点文本 0x0.000002P-126f,也等于 Float.intBitsToFloat

(0x1)=1.4e-45f。(二进制为:0,00000000,0000000 00000000 00000001)
Float.MAX_VALUE:保持 float 类型的最大正有限大值的常量,最大正有限大值为(2-2^-23)*2^127。该常量等于十六进制的浮点文本 0x1.fffffeP+127f,也等于

Float.intBitsToFloat(0x7f7fffff)=3.4028235e+38f。(二进制为:0,11111110,1111111 11111111 11111111)

Java代码  

  1. public class FloatTest {
  2. public static void main(String[] args) {
  3. // 只要指数为255,而尾数不为0时,该数不是一个数
  4. System.out.println(format(Integer.toBinaryString(0x7FC00000)) + " "
  5. + Float.intBitsToFloat(0x7FC00000));// NaN
  6. System.out.println(format(Integer.toBinaryString(0xFFC00000)) + " "
  7. + Float.intBitsToFloat(0xFFC00000));// NaN
  8. System.out.println(format(Integer.toBinaryString(0x7F800001)) + " "
  9. + Float.intBitsToFloat(0x7F800001));// NaN
  10. // 指数为255,尾数为0时,该数就是无穷,符号位区分正无穷与负无穷
  11. System.out.println(format(Integer.toBinaryString(0x7F800000)) + " "
  12. + Float.intBitsToFloat(0x7F800000));// Infinity
  13. System.out.println(format(Integer.toBinaryString(0xFF800000)) + " "
  14. + Float.intBitsToFloat(0xFF800000));// Infinity
  15. //规定指数与尾数都为0时,规定结果就是0
  16. System.out.println(format(Integer.toBinaryString(0x0)) + " "
  17. + Float.intBitsToFloat(0x0));
  18. // 正的最小float,趋近于0
  19. System.out.println(format(Integer.toBinaryString(0x1)) + " "
  20. + Float.intBitsToFloat(0x1));// 1.4E-45
  21. // 正的最大float
  22. System.out.println(format(Integer.toBinaryString(0x7f7fffff)) + " "
  23. + Float.intBitsToFloat(0x7f7fffff));// 3.4028235E38
  24. // 负的最大float,趋近于0
  25. System.out.println(format(Integer.toBinaryString(0x80000001)) + " "
  26. + Float.intBitsToFloat(0x80000001));// -1.4E-45
  27. // 负的最小float
  28. System.out.println(format(Integer.toBinaryString(0xFf7fffff)) + " "
  29. + Float.intBitsToFloat(0xFf7fffff));// -3.4028235E38
  30. }
  31. private static String format(String str) {
  32. StringBuffer sb = new StringBuffer(str);
  33. int sub = 32 - str.length();
  34. if (sub != 0) {
  35. for (int i = 0; i < sub; i++) {
  36. sb.insert(0, 0);
  37. }
  38. }
  39. sb.insert(1, " ");
  40. sb.insert(10, " ");
  41. return sb.toString();
  42. }
  43. }

2^-149=(0.00000000000000000000001)
*2^-127=(00000000000000000000000.1) *2^-149=0x0.000002P-126f=0.0000 0000
0000 0000 0000 0010*2^126
(2-2^-23)*2^127=(10.00000000000000000000000-0.00000000000000000000001) *2^127
0x1.fffffeP+127f=0x1.1111 1111 1111 1111 1111 1110P+127

double取值范围:
负值取值范围-1.79769313486231570E+308 ~ -4.94065645841246544E-324,正值取值范围为 4.94065645841246544E-324 ~ 1.79769313486231570E+308。
Double.MIN_VALUE:保持 double 类型数据的最小正非零值的常量,最小正非零值为 2^-1074。它等于十六进制的浮点字面值 0x0.0000000000001P-1022,也等于

Double.longBitsToDouble(0x1L)。
Double.MAX_VALUE保持 double 类型的最大正有限值的常量,最大正有限值为 (2-2^-52)*2^1023。它等于十六进制的浮点字面值 0x1.fffffffffffffP+1023,也等于

Double.longBitsToDouble(0x7fefffffffffffffL)。

Java中的小数精确计算

Java代码  

    1. public class FloatCalc {
    2. //默认除法运算精度
    3. private static final int DEF_DIV_SCALE = 10;
    4. /**
    5. *   提供精确的加法运算。
    6. *   @param   v1   被加数
    7. *   @param   v2   加数
    8. *   @return   两个参数的和
    9. */
    10. public static double add(double v1, double v2) {
    11. BigDecimal b1 = new BigDecimal(Double.toString(v1));
    12. BigDecimal b2 = new BigDecimal(Double.toString(v2));
    13. return b1.add(b2).doubleValue();
    14. }
    15. /**
    16. *   提供精确的减法运算。
    17. *   @param   v1   被减数
    18. *   @param   v2   减数
    19. *   @return   两个参数的差
    20. */
    21. public static double sub(double v1, double v2) {
    22. BigDecimal b1 = new BigDecimal(Double.toString(v1));
    23. BigDecimal b2 = new BigDecimal(Double.toString(v2));
    24. return b1.subtract(b2).doubleValue();
    25. }
    26. /**
    27. *   提供精确的乘法运算。
    28. *   @param   v1   被乘数
    29. *   @param   v2   乘数
    30. *   @return   两个参数的积
    31. */
    32. public static double mul(double v1, double v2) {
    33. BigDecimal b1 = new BigDecimal(Double.toString(v1));
    34. BigDecimal b2 = new BigDecimal(Double.toString(v2));
    35. return b1.multiply(b2).doubleValue();
    36. }
    37. /**
    38. *   提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
    39. *   小数点以后10位,以后的数字四舍五入。
    40. *   @param   v1   被除数
    41. *   @param   v2   除数
    42. *   @return   两个参数的商
    43. */
    44. public static double div(double v1, double v2) {
    45. return div(v1, v2, DEF_DIV_SCALE);
    46. }
    47. /**
    48. *   提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
    49. *   定精度,以后的数字四舍五入。
    50. *   @param   v1   被除数
    51. *   @param   v2   除数
    52. *   @param   scale   表示表示需要精确到小数点以后几位。
    53. *   @return   两个参数的商
    54. */
    55. public static double div(double v1, double v2, int scale) {
    56. if (scale < 0) {
    57. throw new IllegalArgumentException(
    58. "The scale must be a positive integer or zero");
    59. }
    60. BigDecimal b1 = new BigDecimal(Double.toString(v1));
    61. BigDecimal b2 = new BigDecimal(Double.toString(v2));
    62. return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    63. }
    64. /**
    65. *   提供精确的小数位四舍五入处理。
    66. *   @param   v   需要四舍五入的数字
    67. *   @param   scale   小数点后保留几位
    68. *   @return   四舍五入后的结果
    69. */
    70. public static double round(double v, int scale) {
    71. if (scale < 0) {
    72. throw new IllegalArgumentException(
    73. "The scale must be a positive integer or zero");
    74. }
    75. BigDecimal b = new BigDecimal(Double.toString(v));
    76. BigDecimal one = new BigDecimal("1");
    77. return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    78. }
    79. }
时间: 2024-11-09 00:44:12

java浮点数剖析的相关文章

js,java,浮点数运算错误及应对方法

js,java,浮点数运算错误及应对方法 一,浮点数为什么会有运算错误 IEEE 754 标准规定了计算机程序设计环境中的二进制和十进制的浮点数自述的交换.算术格式以及方法. 现有存储介质都是2进制.2进制的进制基数是2,那么一个数字只要被因素包含大于2的质数的数除,都会产生无限循环小数.无限循环小数和无理数都无法,和非无限循环的有理数一起用同一种方式存储到存储介质上的同时还保持计算的兼容性. 对于无限循环小数,可以设计一种格式存储到介质上,但是同时又要和非无限循环的有理数能够计算,效率应该会变

浮点数剖析

IEEE 754[编辑] 维基百科,自由的百科全书 IEEE二进制浮点数算术标准(IEEE 754)是1980年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用.这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的"浮点数运算符":它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式). IEEE 754规定了四种表示浮点数值的方式:单精确度(32位

JAVA 浮点数转化为百分数,分离整数和小数部分

JAVA 浮点数转化为百分数 public class DoubleToPercentformat { /** * 将double类型数据转换为百分比格式,并保留小数点前IntegerDigits位和小数点后FractionDigits位 * @param d * @param IntegerDigits * @param FractionDigits * @return */ public static String getPercentFormat(double d,int IntegerD

Java 浮点数精度丢失

Java 浮点数精度丢失 问题引入 昨天帮室友写一个模拟发红包抢红包的程序时,对金额统一使用的 double 来建模,结果发现在实际运行时程序的结果在数值上总是有细微的误差,程序运行的截图: 输入依次为:红包个数,抢红包的人数,选择固定金额红包还是随机金额红包,每个红包的金额(此例只有一个红包). 注意到程序最后的结果是有问题的,我们只有一个金额为 10 的红包,一个人去抢,所以正确结果应该为这个人抢到了 10 RMB. 为了使问题更加明显,我们测试一个更加简单的例子: public class

Java浮点数相等性的判断

Java浮点数相等性的判断 问题描述如下: 给定两个变量double a.double b,它们对应的装箱类型变量分别是Double x.Double y,问: 是否存在一组a.b.x.y,满足 a==b && !x.equals(y) ? 是否存在一组a.b.x.y,满足 a!=b && x.equals(y) ? 乍看之下似乎是不可能的,实际上确实存在这样的值,参考以下代码 public static void main() { double a = 0.0; doub

Java浮点数内存存储

转自: [解惑]剖析float型的内存存储和精度丢失问题 1.小数的二进制表示问题 首先我们要搞清楚下面两个问题: (1)  十进制整数如何转化为二进制数 算法很简单.举个例子,11表示成二进制数: 11/2=5   余   1 5/2=2   余   1 2/2=1   余   0 1/2=0   余   1 0   结束 所以:11二进制表示为(从下往上):1011 这里提一点:只要遇到除以后的结果为0了就结束了,大家想一想,所有的整数除以2是不是一定能够最终得到0.换句话说,所有的整数转变

Java 浮点数 float或double类型的表示范围和精度

隐约记得,浮点数判断大小好像有陷阱,因为底层的二进制数不能精确表示所有的小数.有时候会产生让人觉得莫名其妙的事情. 如在java中, 0.99999999f==1f //true 0.9f==1f //false 要明白这些,首先要搞清楚float和double在内存结构 1.内存结构 float和double的范围是由指数的位数来决定的. float的指数位有8位,而double的指数位有11位,分布如下: float: 1bit(符号位) 8bits(指数位) 23bits(尾数位) dou

第二章 Java浮点数精确计算

1.实际意义 在实际开发中,如果需要进行float或double的精确计算(尤其是财务计算),直接使用float或double是不行的(具体的例子看下边的代码的main方法的测试结果),需要使用BigDecimal. 2.代码 package com.xxx.util; import java.math.BigDecimal; /** * 浮点数精准算法 */ public class BigDecimalArithUtil { private static final int DIV_SCAL

Java 浮点数的范围和精度

本篇先介绍IEEE754标准中针对浮点数的规范,然后以问答形式补充有关浮点数的知识点. (一)IEEE754标准 IEEE 754 标准即IEEE浮点数算术标准,由美国电气电子工程师学会(IEEE)计算机学会旗下的微处理器标准委员会发布. 以32位float数据为例,在内存中的存储形式是1bit的符号位(S),8bit表示指数部分(Exp),23表示小数部分的尾数(Fraction). 表一 单精度浮点数在内存中存储形式 1bit符号 8bit指数部分 23bit尾数 符号位——S取0时表示负数