扑朔迷离的Java浮点数

摘要

Java浮点数的定义大体上遵守了二进制浮点运算标准(即IEEE 754标准)。IEEE 754标准提供了浮点数无穷,负无穷,负零和非数字(Not a number,简称NaN)的定义。在Java开发方面,这些东西经常被多数程序员混淆。

在本文中,我们将讨论计算这些特殊的浮点数相关的结果。我们也将指出一些通常的Java浮点数的陷阱。


前言

在Java编程语言中提供了两种内置的表示浮点数的类别:float和double。float占用4字节的存储空间,有23位用来表示精度;double占用8字节的存储空间,有52位用来表示精度。【1】

3.0d是个双精度浮点数常量。3.0,或者3.0f是单精度的浮点数常量。

float f = 3.0f;

double d = 3.0d;

Java核心库提供了两个包装类,java.lang.Float和java.lang.Double。这两个类使得浮点型对象也能够被存储在比如哈希表之类的Java集合中,并且这两个类也提供了一些解析和转换的帮助方法。

  

Float ff = new Float(1.0f); // 创建一个Java "Float" 对象

Double dd = new Double(2.0d); // 创建一个Java "Double" 对象


特殊的浮点数实体

非数字(Not A Number)

"NaN"指"not a number",即非数字。在浮点数运算中,如果有一些输入参数导致这个运算产生某种没有定义的结果,那么"NaN"就产生了。比如,0.0除以0.0就是没有算数定义的,负数的平方根也是没有算数定义的。

0.0 / 0.0   ->  NaN

Math.sqrt(-2.0)  ->  NaN

涉及到NaN的运算

Double.NaN + Double.NaN  ->  NaN

Float.NaN + 2.0  ->  NaN

Float.NaN * 3.0  ->  NaN  

(0.0 / 0.0) * Float.POSITIVE_INFINITY  ->  NaN

Math.abs(0.0 / 0.0) -> NaN

(int) (Double.NaN) -> 0

所有涉及到"NaN"的布尔操作都返回false这个值。

Double.NaN > 1.0  ->  false

Double.NaN < 1.0  ->  false

Double.NaN == 1.0  ->  false

Float.NaN < -3.0  ->  false

Float.NaN > Float.POSITIVE_INFINITY  ->  false

Float.NaN < Float.POSITIVE_INFINITY  ->  false

(0.0 / 0.0) == (0.0 / 0.0)  ->  false
无穷
(Infinity)

如果一个浮点运算产生了一个大到没有办法正常表示出来的浮点数,那么无穷就产生了。无穷是个特殊的值表示概念上的无穷大。

1.0 / 0.0  ->  Infinity

涉及到Infinity的运算

Double.POSITIVE_INFINITY == Double.POSITIVE_INFINITY  ->  true

Float.POSITIVE_INFINITY == Float.POSITIVE_INFINITY  ->  true

(1.0 / 0.0) + 2.0  -> Infinity

(1.0 / 0.0) * 0.5  -> Infinity

(1.0 / 0.0) * 0.0  ->  NaN

(1.0 / 0.0) + (1.0 / 0.0)  ->  Infinity

(1.0 / 0.0) * (1.0 / 0.0)  ->  Infinity

(1.0 / 0.0) - (1.0 / 0.0)  ->  NaN

(1.0 / 0.0) / (1.0 / 0.0)  ->  NaN

(int) (1.0 / 0.0)  ->  2147483647

负无穷(Negative Infinity)

如果一个浮点运算产生了一个没有办法正常表示出来的极端负浮点数,那么负无穷就产生了。负无穷是个特殊的值表示概念上的负无穷大。

-1.0 / 0.0  ->  -Infinity

涉及到Negative Infinity的运算

- Double.POSITIVE_INFINITY == Double.NEGATIVE_INFINITY  ->  true

Float.NEGATIVE_INFINITY < 1.0  ->  true

(-1.0 / 0.0 ) + 2.0  ->  -Infinity

(-1.0 / 0.0) == (-1.0 / 0.0)  ->  true

(-1.0 / 0.0) - (-1.0 / 0.0)  ->  NaN

(-1.0 / 0.0) * 0.5  ->  -Infinity

(-1.0 / 0.0) * 0.0  ->  NaN

Math.abs(-1.0 / 0.0)  ->  Infinity

(int) (-1.0 / 0.0)  ->  -2147483648

负零(Negative Zero)

当一个浮点数运算产生了一个无限接近0并且没有办法正常表示出来的负浮点数,那么负零就产生了。

-2.0 / Float.POSITIVE_INFINITY  ->  -0.0

“-0.0”在数值上等于“0.0”,但是一些涉及到“-0.0”的运算在和相同的涉及到“0.0”的运算又不一样。

(-0.0) == 0.0  ->  true

2.0 / (0.0)  ->  Infinity

2.0 / (-0.0)  ->  -Infinity

涉及到Negative Zero的运算

0.0 > (-0.0)  ->  false

(-0.0) * (-0.0)  ->  0.0

3.0 + (-0.0)  ->  3.0

4.0 * (-0.0)  ->  -0.0

0.0 - 0.0  ->  0.0

(-0.0) - (-0.0)  ->  0.0

(-0.0) + (-0.0)  ->  -0.0

(-0.0) + 0.0  ->  0.0

0.0 + (-0.0)  ->  0.0

(-0.0) - 0.0  ->  -0.0

- (-0.0)  ->  0.0

- (0.0)  ->  -0.0

0.0 / (-0.0)  ->  NaN


Java的Float对象的比较

比较两个Java的Float对象和比较Java的两个浮点数值有不同的语义,回忆一下float类型是Java的一种基本类型,而java.lang.Float类是“Object”类的子类。

一个“NaN”值并不等于它本身,但是一个值为“NaN”的包装对象却和它本身是相等的(译注:通过Float.equals方法比较)。只所以这样定义是因为不这样定义的话一个值为“NaN”的Float对象将无法正确的从hash table中获取出来(译注:参看HashMap或者Hashtable的get方法实现)。

(new Float(0.0 / 0.0)).equals(new Float(0.0 / 0.0))  ->  true

对于java.lang.Float类里面的值是按照从低到高的顺序排序的,负无穷大,负数,负零,零,正数,无穷大,非数值。java.lang.Double也是如此。

(new Float(0.0)).equals(new Float(-0.0))  ->  false

(new Float(-1.0 / 0.0)).compareTo(new Float(-3.0))  ->  -1

(new Float(-3.0)).compareTo(new Float(-0.0))  ->  -1

(new Float(-0.0)).compareTo(new Float(0.0))  ->  -1

(new Float(0.0)).compareTo(new Float(3.0))  ->  -1

(new Float(3.0)).compareTo(new Float(1.0 / 0.0))  ->  -1

(new Float(1.0 / 0.0)).compareTo(new Float(0.0 / 0.0))  ->  -1


一些存在的陷阱

实现abs方法的错误方式

如下是一种错误的方式:

(f >= 0.0) ? f : - f

如果f取值为-0.0将会出现什么情况?因为0.0是和-0.0相等的,上面的表达式将返回-0.0本身,也就是-0.0,相比正确的结果0.0来说这是错误的。

正确的形式如下:

(f <= 0.0) ? 0.0 - f : f

当你在优化浮点数表达式的时候一定要小心。比如,如果你将上面的表达式优化成下面的这种:

(f <= 0.0) ? - f : f

那么你又引入了一个BUG,它会导致abs(0.0)返回-0.0。

优化浮点数的危险性

我们已经领略过了一种情况,(0.0-f)和(-f)并不相等。!(a < b)并不意味着(a >= b)。

! (1.0 < NaN)  ->  true

(1.0 >= NaN)  ->  false

《The Java Language Specification》(译注:这里指的是第一版)这本书308页详细讲述了优化浮点数表达式的危险性。

假定一个浮点数的值和它自身是相等的

“NaN”的值和他自身并不相等。类似的原因,假定一个浮点数减去它自身一定为0.0也是不正确的。“NaN”-“NaN”还是“NaN”,“Infinity”-“Infinity”是“NaN”,尽管两个正无穷是相等的。

不对NaN作检查

public employee(float monthly_salary){

    if (monthly_salary < 0.0)

        throw IllegalArgumentException("illegal monthly salary");

    this.yearly_salary = 12.0 * monthly_salary;

}

上面的一段代码当你传入“NaN”作为参数时并不会捕获到那个错误,但是下面的一段代码会。

public employee(float monthly_salary){

    if (monthly_salary >= 0.0)  {

        this.yearly_salary = 12.0 * monthly_salary;

     else

        throw IllegalArgumentException("illegal monthly salary");

}

看看修复后的代码,如果传入的参数是“Infinity”我们该怎么做?根据API的规范,我们可以做甚多事情,关键点是每次当我们处理浮点数是,我们一定要想想“NaN”,“Infinity”,“-Infinity”,“-0.0”这些情况该如何处理。


引用文献

(译注:这里是作者引用文献,在这里没有进行翻译,请自行查阅相关资料)

James Gosling, Bill Joy, Guy Steele, "The Java Language Specification", Addison Wesley, (1996)
IEEE, "ANSI/IEEE Std 754-1985, An American National Standard IEEE Standard for Binary Floating-Point Arithmetic", (1985)


译者添加

【1】float是32位的,第一位是符号位,以二进制0和1表示(0为正,1为负),有8位阶码,最后就有23位精确数值可以填写。double是64位。第一位也是符号位,阶码为11位,所以有52位精确数值填写。

译者理解的过程得到了其他人的一些帮助,感谢你们。另外由于个人能力问题,翻译出来的文章可能没有完全把握住作者的意思,如有误导请指正,版权规原作者所有,译者仅保留该简体中文版本的署名权利(http://guoh.org/lifelog/)

浮点数比较大小

• 浮点数不能直接用算数操作符比较大小,如:==, !=, >=, <= operators and Equals 方法,要用以下形式:Math.Abs(x - y) < Single.Epsilon;Single.Epsilon 字段表示大于零的最小正 Single 值。此字段为常数。   Double.Epsilon 字段   表示大于零的最小正 Double 值。此字段为常数。

比较浮点数的相等比较

原文地址:https://www.cnblogs.com/luckForever/p/7254308.html