`

Java中的浮点数剖析

    博客分类:
  • 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位)。参考下面的示例:

  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();
	}
}

 

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

相关推荐

    JAVA中浮点数的运算

    为了更好地理解浮点数运算,我们可以分析`DecimalConvertUtils.java`的代码,看它是如何处理浮点数到`BigDecimal`的转换,以及在`BigDecimal`上进行运算的实现。通常,这样的工具类会包含静态方法,便于在不同地方...

    用java从文件中读取浮点数

    在Java编程语言中,从文件中读取浮点数是一个常见的操作需求,尤其是在处理大量数据或进行数据分析时。本文将详细解析如何利用Java标准库中的类来实现这一功能,包括必要的异常处理,确保代码的健壮性和实用性。 ##...

    IEEE754浮点数_数值转换_浮点数转换_

    **正文** IEEE754浮点数标准是计算机科学中用于表示和操作浮点数的国际标准,由电气...无论是软件开发、数据分析还是硬件设计,都可能涉及到浮点数的处理和转换,因此深入学习这一主题将对提升技术能力产生积极影响。

    4字节浮点数内码转换

    浮点数内码转换是计算机科学中的一个重要概念,特别是在处理数值计算和数据存储时。4字节浮点数,也称为单精度浮点数,遵循国际标准IEEE 754,这种格式在大多数现代计算机系统中广泛使用。4字节浮点数能够表示大约6...

    HEX与浮点数相互转换

    理解并熟练掌握HEX与浮点数的相互转换对于任何从事软件开发、系统分析或者底层编程的人来说都是基础技能。它有助于深入理解计算机内部的工作机制,特别是在处理数值精度问题、调试内存数据或与硬件交互时。

    Java中的浮点数分析

     浮点数分为单精度和双精度,Java中的单精度和双精度分别为float和double.你们知道float和double是怎么存储的吗? float占4个字节,double占8个字节,为了方便起见,这里就只讨论float类型. float其实和一个int型的...

    Java写的数值分析中的一些程序

    这个压缩包中的“Math”文件很可能包含了一系列与数值分析相关的Java程序,虽然没有注释,但我们可以根据Java的常用库和数值分析的基本概念来解析这些程序可能涉及的知识点。 首先,数值分析是应用数学的一个分支,...

    单精度&双精度浮点数与十六进制数相互转换

    在实际编程中,大多数编程语言如C++、Java、Python等都提供了内置函数来进行浮点数和十六进制之间的转换。例如,在Python中,可以使用`float.hex()`和`float.fromhex()`方法实现这一过程。 理解这些转换机制对编程...

    IEEE754浮点数与十进制数互相转换原理分析

    在计算机科学中,IEEE754标准是用于表示浮点数的一种国际标准,它定义了浮点数的存储格式,使得不同计算机系统之间可以一致地表示和交换浮点数值。这个标准广泛应用于各种编程语言和硬件平台,包括处理器、GPU以及...

    浮点数转换工具.7z

    5. **编程语言中的浮点数操作**: 不同的编程语言对浮点数处理有不同的内置函数和库,转换工具可能提供了这些语言间数据类型的转换,比如C++的`double`到Java的`double`。 6. **应用场景**: 这样的工具对于程序员...

    含界面的java的词法分析器

    在这个场景中,我们关注的是一个基于Java实现的、带有用户界面的C语言词法分析器。词法分析器是编译器或解释器的一部分,它的任务是从源代码中识别出一个个有意义的单元,即“词法单元”或“记号”(Token),为后续...

    64位十六进制转浮点数

    转换工具和编程语言(如Python、Java、C++等)通常内置了直接进行这种转换的功能,但理解转换原理有助于更好地理解和处理浮点数相关的计算问题,尤其是在进行数值计算、数据分析或者低级系统编程时。在编程中,可以...

    java词法分析举例

    Java词法分析是编程语言处理过程中的重要步骤,它涉及将源代码文本转换为一系列有意义的标记,这些标记称为符号或token。在这个过程中,词法分析器(也称为扫描器或lexer)会识别并分离出诸如关键字、标识符、常量、...

    模拟浮点数

    在给定的描述中,虽然没有具体的信息,但我们可以假设这篇博文可能讨论了如何在Java中实现一个模拟浮点数的类或方法。链接指向了一个ITEYE上的博客文章,遗憾的是,由于这是一个假定的场景,我们无法访问这个链接...

    IEEE_754_16进制转十进制浮点数 工具

    这个工具对于理解和分析浮点数在计算机系统中的表示方式非常有用,尤其在编程、硬件设计或科学计算领域。 IEEE 754是国际电工委员会(IEEE)制定的一套浮点数运算标准,广泛应用于各种计算机系统中。该标准定义了...

    算法-计算浮点数相除的余(信息学奥赛一本通-T1029)(包含源程序).rar

    在信息学奥赛中,计算浮点数相除的余数是一个常见的问题,这涉及到数值计算、浮点数处理和编程技巧。浮点数运算在计算机科学中扮演着至关重要的角色,尤其是在解决数学和工程问题时。这个题目可能是为了帮助参赛者...

    Java简易词法分析器

    Java简易词法分析器是一种用于解析源代码的工具,它主要负责识别程序中的基本元素,如标识符、关键字、常量、运算符等,这些元素构成了编程语言的词汇部分,也就是我们常说的“词法”。在Java编程环境中,词法分析是...

    java词法分析器_ - Java - Java_开发文档.rar_java 词法分析_java词法分析_词法分析_词法分析 j

    Java 词法分析是编程语言编译或解释过程中的关键步骤,主要负责将源代码转换成一系列有意义的标记,为语法分析提供输入。在Java语言中,词法分析器(也称为扫描器或lexer)的任务是识别源代码中的关键字、标识符、...

    编译原理实验词法分析器 java语言版

    在这个实验中,词法分析器被设计用来处理Java语言中的小数和科学记数法,这是编程中常见的数值表示方式。 小数和科学记数法在Java中是非常重要的数据类型,它们用于表示浮点数值。小数即常规的十进制形式,例如123....

Global site tag (gtag.js) - Google Analytics