Java中的浮点数剖解

Java中的浮点数剖析

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

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


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

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

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

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

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


Java中的浮点数剖解
 
在上面的图例中,第一个域为符号域。其中 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位)。参考下面的示例:

  System.out.println(16777215f);//1.6777215E7
  System.out.println(16777216f);//1.6777216E7
  System.out.println(16777217f);//1.6777216E7
  System.out.println(16777218f);//1.6777218E7
  System.out.println(16777219f);//1.677722E7
  System.out.println(16777220f);//1.677722E7
  System.out.println(16777221f);//1.677722E7
  System.out.println(16777222f);//1.6777222E7
  System.out.println(16777223f);//1.6777224E7
  System.out.println(16777224f);//1.6777224E7
  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也可计算出:

/*
注:Float.intBitsToFloat方法是将内存中int数值的二进制看作是float的
二进制制,这样很方便使用一个二进制位来构造一个float。
*/
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也可计算出:

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

 

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

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

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

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

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

// 34.6-34.0=0.5999985
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位)。
如果你在浮点数的有效位后增加数字的话,结果是不会变化的:

System.out.println(1.67772156F);//1.6777215
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)

public class FloatTest {
	public static void main(String[] args) {
		// 只要指数为255,而尾数不为0时,该数不是一个数
		System.out.println(format(Integer.toBinaryString(0x7FC00000)) + " "
				+ Float.intBitsToFloat(0x7FC00000));// NaN
		System.out.println(format(Integer.toBinaryString(0xFFC00000)) + " "
				+ Float.intBitsToFloat(0xFFC00000));// NaN
		System.out.println(format(Integer.toBinaryString(0x7F800001)) + " "
				+ Float.intBitsToFloat(0x7F800001));// NaN
		// 指数为255,尾数为0时,该数就是无穷,符号位区分正无穷与负无穷
		System.out.println(format(Integer.toBinaryString(0x7F800000)) + " "
				+ Float.intBitsToFloat(0x7F800000));// Infinity
		System.out.println(format(Integer.toBinaryString(0xFF800000)) + " "
				+ Float.intBitsToFloat(0xFF800000));// Infinity
		//规定指数与尾数都为0时,规定结果就是0
		System.out.println(format(Integer.toBinaryString(0x0)) + " "
				+ Float.intBitsToFloat(0x0));		
		// 正的最小float,趋近于0
		System.out.println(format(Integer.toBinaryString(0x1)) + " "
				+ Float.intBitsToFloat(0x1));// 1.4E-45
		// 正的最大float
		System.out.println(format(Integer.toBinaryString(0x7f7fffff)) + " "
				+ Float.intBitsToFloat(0x7f7fffff));// 3.4028235E38
		// 负的最大float,趋近于0
		System.out.println(format(Integer.toBinaryString(0x80000001)) + " "
				+ Float.intBitsToFloat(0x80000001));// -1.4E-45
		// 负的最小float
		System.out.println(format(Integer.toBinaryString(0xFf7fffff)) + " "
				+ Float.intBitsToFloat(0xFf7fffff));// -3.4028235E38
	}

	private static String format(String str) {
		StringBuffer sb = new StringBuffer(str);
		int sub = 32 - str.length();
		if (sub != 0) {
			for (int i = 0; i < sub; i++) {
				sb.insert(0, 0);
			}
		}
		sb.insert(1, " ");
		sb.insert(10, " ");
		return sb.toString();
	}
}


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中的小数精确计算

public class FloatCalc {

	//默认除法运算精度   
	private static final int DEF_DIV_SCALE = 10;

	/**   
	*   提供精确的加法运算。   
	*   @param   v1   被加数   
	*   @param   v2   加数   
	*   @return   两个参数的和   
	*/

	public static double add(double v1, double v2) {
		BigDecimal b1 = new BigDecimal(Double.toString(v1));
		BigDecimal b2 = new BigDecimal(Double.toString(v2));
		return b1.add(b2).doubleValue();
	}

	/**   
	*   提供精确的减法运算。   
	*   @param   v1   被减数   
	*   @param   v2   减数   
	*   @return   两个参数的差   
	*/

	public static double sub(double v1, double v2) {
		BigDecimal b1 = new BigDecimal(Double.toString(v1));
		BigDecimal b2 = new BigDecimal(Double.toString(v2));
		return b1.subtract(b2).doubleValue();
	}

	/**   
	  *   提供精确的乘法运算。   
	  *   @param   v1   被乘数   
	  *   @param   v2   乘数   
	  *   @return   两个参数的积   
	  */

	public static double mul(double v1, double v2) {
		BigDecimal b1 = new BigDecimal(Double.toString(v1));
		BigDecimal b2 = new BigDecimal(Double.toString(v2));
		return b1.multiply(b2).doubleValue();
	}

	/**   
	  *   提供(相对)精确的除法运算,当发生除不尽的情况时,精确到   
	  *   小数点以后10位,以后的数字四舍五入。   
	  *   @param   v1   被除数   
	  *   @param   v2   除数   
	  *   @return   两个参数的商   
	  */

	public static double div(double v1, double v2) {
		return div(v1, v2, DEF_DIV_SCALE);
	}

	/**   
	  *   提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指   
	  *   定精度,以后的数字四舍五入。   
	  *   @param   v1   被除数   
	  *   @param   v2   除数   
	  *   @param   scale   表示表示需要精确到小数点以后几位。   
	  *   @return   两个参数的商   
	  */

	public static double div(double v1, double v2, int scale) {
		if (scale < 0) {
			throw new IllegalArgumentException(
					"The scale must be a positive integer or zero");
		}
		BigDecimal b1 = new BigDecimal(Double.toString(v1));
		BigDecimal b2 = new BigDecimal(Double.toString(v2));
		return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
	}

	/**   
	  *   提供精确的小数位四舍五入处理。   
	  *   @param   v   需要四舍五入的数字   
	  *   @param   scale   小数点后保留几位   
	  *   @return   四舍五入后的结果   
	  */

	public static double round(double v, int scale) {
		if (scale < 0) {
			throw new IllegalArgumentException(
					"The scale must be a positive integer or zero");
		}
		BigDecimal b = new BigDecimal(Double.toString(v));
		BigDecimal one = new BigDecimal("1");
		return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
	}
}

 

1 楼 jychenok 2010-08-02  
讲的理论不错,搞清楚了缘由。
其实在项目中一般不会弄得那么复杂,直接使用BigDecimal
BigDecimal.valueOf(3.6).subtract(BigDecimal.valueOf(3.0)
只要保证valueOf的参数精度是正确的,那运算结果就不会出错
我觉得不用new那么多次BigDecimal,用valueOf即可
2 楼 hekp 2011-03-24  
多谢分享,很到位了。Java中的浮点数剖解