float和double的比较方式
实际上,这不仅是Java特有的问题。 几乎所有的编程语言都可以看到这个问题。 在计算机内存中,使用IEEE 754
标准格式存储float
和double
数据。 实际的存储和转换如何工作,超出了本文讨论的范围。
现在,只要理解在计算和转换过程中可以在这些数字中引入较小的舍入误差。 这就是为什么仅仅依靠等式运算符(==
)来比较浮点数是不可取的原因。
1. 简单比较[不推荐]
首先看看简单的比较,以便理解简单比较究竟有什么错误和问题。在给定的程序中,使用两种方法创建相同的浮点数(即1.1
):
- 将
0.1
相加11
次。 0.1
乘以11
。
理论上,两种操作都会产生数字:1.1
。 当比较两种方法的结果时,它们应该是相等。
请看下面的代码 -
private static void simpleFloatsComparison() {
//Method 1
double f1 = 0.0;
for (int i = 1; i <= 11; i++) {
f1 += 0.1;
}
//Method 2
double f2 = 0.1 * 11;
System.out.println("f1 = " + f1);
System.out.println("f2 = " + f2);
if (f1 == f2)
System.out.println("f1 and f2 are equal\n");
else
System.out.println("f1 and f2 are not equal\n");
}
执行上面示例代码,得到以下结果 -
f1 = 1.0999999999999999
f2 = 1.1
f1 and f2 are not equal
看看这两个值。 f1
计算结果为:1.0999999999999999
。它正是内部导致四舍五入的问题。 这就是为什么不推荐简单浮点比较的原因。
基于阈值的比较[推荐]
当我们知道问题时,下面来看看如何解决它。 使用编程,不能改变这些浮点数的存储或计算方式。 所以必须调整一个解决方案来确定两个值之间的差异,可以容忍并仍然认为这些数值相等。 这一点在值上的差异被称为阈值。
因此,现在要使用基于阈值的浮点比较,可以使用Math.abs()
方法来计算两个数字之间的差异,并将差异与阈值进行比较。参考以下实现代码 -
private static void thresholdba<x>sedFloatsComparison() {
final double THRESHOLD = 0.0001;
//Method 1
double f1 = 0.0;
for (int i = 1; i <= 11; i++) {
f1 += 0.1;
}
//Method 2
double f2 = 0.1 * 11;
System.out.println("f1 = " + f1);
System.out.println("f2 = " + f2);
if (Math.abs(f1 - f2) < THRESHOLD)
System.out.println("f1 and f2 are equal using threshold\n");
else
System.out.println("f1 and f2 are not equal using threshold\n");
}
执行上面示例代码,得到以下结果 -
f1 = 1.0999999999999999
f2 = 1.1
f1 and f2 are equal using threshold
3. 与BigDecimal比较[推荐]
在BigDecimal
类中,可以指定要使用的舍入模式和准确精度。 使用精确的精度限制,舍入误差大多都可以解决。
BigDecimal
最好的部分是数字不可变的,即如果创建一个值为1.23
的BigDecimal
,该对象将保持为1.23
并且永远不会被更改。 这个类提供了很多方法可以用来对它的值进行数字操作。
可以使用它的compareTo()
方法来比较两个BigDecimal
数字。比较时忽略数值范围。
a.compareTo(b); // returns (-1 if a < b) (0 if a == b) (1 if a > b)
注意:切勿使用
.equals()
方法比较BigDecimals。 那是因为这个等于函数会比较数值范围。 如果比例不同,则.equals()
将返回false
,即使它们是数学上相同的数字。
通过下面一个例子来理解这个比较。
private static void testBdEquality()
{
BigDecimal a = new BigDecimal("2.00");
BigDecimal b = new BigDecimal("2.0");
System.out.println(a.equals(b)); // false
System.out.println(a.compareTo(b) == 0); // true
}
现在来验证一下,使用BigDecimal
类来解决原始问题。
private static void bigDecimalComparison()
{
//Method 1
BigDecimal f1 = new BigDecimal("0.0");
BigDecimal pointOne = new BigDecimal("0.1");
for (int i = 1; i <= 11; i++) {
f1 = f1.add(pointOne);
}
//Method 2
BigDecimal f2 = new BigDecimal("0.1");
BigDecimal eleven = new BigDecimal("11");
f2 = f2.multiply(eleven);
System.out.println("f1 = " + f1);
System.out.println("f2 = " + f2);
if (f1.compareTo(f2) == 0)
System.out.println("f1 and f2 are equal using BigDecimal\n");
else
System.out.println("f1 and f2 are not equal using BigDecimal\n");
}
执行上面示例代码,得到以下结果 -
f1 = 1.1
f2 = 1.1
f1 and f2 are equal using BigDecimal
上面就是比较Java中的浮点数和双精度数据的几种方便了
要想理解float和double的取值范围和计算精度,必须先了解小数是如何在计算机中存储的:
举个例子:78.375,是一个正小数。要在计算机中存储这个数,需要把它表示为浮点数的格式,先执行二进制转换:
PS:
二进制的小数点和十进制的小数点是不同的。二进制小数点后是2的负幂,十进制是10的负幂。
一 小数的二进制转换(浮点数)
78.375的整数部分:
小数部分:
所以,78.375的二进制形式就是1001110.011
然后,使用二进制科学记数法,
注意,转换后用二进制科学记数法表示的这个数,有底有指数有小数部分,这个就叫做浮点数
二 浮点数在计算机中的存储
在计算机中,保存这个数使用的是浮点表示法,分为三大部分:
第一部分用来存储符号位(sign),用来区分正负数,这里是0,表示正数
第二部分用来存储指数(exponent),这里的指数是十进制的6
第三部分用来存储小数(fraction),这里的小数部分是001110011
需要注意的是,指数也有正负之分,后面再讲。
如下图所示(图片来自维基百科):
比如float类型是32位,是单精度浮点表示法:
符号位(sign)占用1位,用来表示正负数,
指数位(exponent)占用8位,用来表示指数,
小数位(fraction)占用23位,用来表示小数,不足位数补0。
而double类型是64位,是双精度浮点表示法:
符号位占用1位,指数位占用11位,小数位占用52位。
到这里其实已经可以隐隐看出:
指数位决定了大小范围,因为指数位能表示的数越大则能表示的数越大嘛!
而小数位决定了计算精度,因为小数位能表示的数越大,则能计算的精度越大咯!
可能还不够明白,举例子吧:
float的小数位只有23位,即二进制的23位,能表示的最大的十进制数为2的23次方,即8388608,即十进制的7位,严格点,精度只能百分百保证十进制的6位运算。
double的小数位有52位,对应十进制最大值为4 503 599 627 370 496,这个数有16位,所以计算精度只能百分百保证十进制的15位运算。
PS: 我们常见的科学计算器,比如高中时候用的,一般最大支持的运算位数就是15位,超过这个就不够准了。在实际编程中,也是用的double类型比较多,因为能够保证15位的运算。如果还需要更高精度的运算,则需要使用其他数据类型,比如java中的BigDecimal类型,能够支持更高精度的运算。
三 指数位的偏移量与无符号表示
需要注意的是指数可能是负数,也有可能是正数,即指数是有符号整数,而有符号整数的计算是比无符号整数麻烦的。所以为了减少不必要的麻烦,在实际存储指数的时候,需要把指数转换成无符号整数。那么怎么转换呢?
注意到float的指数部分是8位,则指数的取值范围是 -126到+127,为了消除负数带来的实际计算上的影响(比如比较大小,加减法等),可以在实际存储的时候,给指数做一个简单的映射,加上一个偏移量,比如float的指数偏移量为127,这样就不会有负数出现了。
比如
指数如果是6,则实际存储的是6+127=133,即把133转换为二进制之后再存储。
指数如果是-3,则实际存储的是-3+127=124,即把124转换为二进制之后再存储。
当我们需要计算实际代表的十进制数的时候,再把指数减去偏移量即可。
对应的double类型,存储的时候指数偏移量是1023。
四 最后
所以用float类型来保存十进制小数78.375的话,需要先转换成浮点数,得到符号位和指数和小数部分。这个例子前面已经分析过,所以:
符号位是0,
指数位是6+127=133,二进制表示为10 000 101,
小数部分是001110011,不足部分请自动补0。
连起来用float表示,加粗部分是指数位,最左边是符号位0,代表正数:
0 10000101 001110011 00000 00000 0000