A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

下面的程序有一个单重的循环,它记录迭代的次数,并在循环终止时打印这个数。那么,这个程序会打印出什么呢?
public class Count {
        public static void main(String[] args) {
                final int start = 2000000000;
                int count = 0;
                for (float f = start; f < start + 50; f++)
                        count++;
                System.out.println(count);
        }
}
表面的分析也许会认为这个程序将打印50,毕竟,循环变量(f)被初始化为2,000,000,000,而终止值比初始值大50,并且这个循环具有传统的“半开”形
式:它使用的是 < 操作符,这是的它包括初始值但是不包括终止值。
然而,这种分析遗漏了关键的一点:循环变量是float 类型的,而非int 类型的。
很明显,增量操作(f++)不能正常工作。F 的初始值接近于Integer.MAX_VALUE,因此它需要用31 位来精确表示,而float 类型只能提供24 位的精度。对如此巨大的一个float 数值进行增量操作将不会改变其值。因此,这个程序看起来应该无限地循环下去,因为f 永远也不可能解决其终止值。


但是,如果你运行该程序,就会发现它并没有无限循环下去,事实上,它立即就终止了,并打印出0。怎么回事呢?
问题在于终止条件测试失败了,其方式与增量操作失败的方式非常相似。这个循环只有在循环索引f 比(float)(start + 50)小的情况下才运行。在将一个int与一个float 进行比较时,会自动执行从int 到float 的提升[JLS 15.20.1]。


遗憾的是,这种提升是会导致精度丢失的三种拓宽原始类型转换的一种[JLS5.1.2]。(另外两个是从long 到float 和从long 到double。)
f 的初始值太大了,以至于在对其加上50,然后将结果转型为float 时,所产生的数值等于直接将f 转换成float 的数值。换句话说,(float)2000000000 ==
2000000050,因此表达式f < start + 50 即使是在循环体第一次执行之前就是false,所以,循环体也就永远的不到机会去运行。
订正这个程序非常简单,只需将循环变量的类型从float 修改为int 即可。这样就避免了所有与浮点数计算有关的不精确性:
for (int f = start; f < start + 50; f++)
count++;
如果不使用计算机,你如何才能知道2,000,000,050 与2,000,000,000 有相同的float 表示呢?关键是要观察到2,000,000,000 有10 个因子都是2:它是一个2
乘以9 个10,而每个10 都是5×2。这意味着2,000,000,000 的二进制表示是以10 个0 结尾的。50 的二进制表示只需要6 位,所以将50 加到2,000,000,000
上不会对右边6 位之外的其他为产生影响。特别是,从右边数过来的第7 位和第8 位仍旧是0。提升这个31 位的int 到具有24 位精度的float 会在第7 位和第
8 位之间四舍五入,从而直接丢弃最右边的7 位。而最右边的6 位是2,000,000,000 与2,000,000,050 位以不同之处,因此它们的float 表示是相同
的。
这个谜题寓意很简单:不要使用浮点数作为循环索引,因为它会导致无法预测的行为。如果你在循环体内需要一个浮点数,那么请使用int 或long 循环索引,
并将其转换为float或double。在将一个int或long转换成一个float或double时,你可能会丢失精度,但是至少它不会影响到循环本身。当你使用浮点数时,
要使用double 而不是float,除非你肯定float 提供了足够的精度,并且存在强制性的性能需求迫使你使用float。适合使用float 而不是double 的时刻是
非常非常少的。

3 个回复

倒序浏览
好牛逼的样子  顶顶
回复 使用道具 举报
学习了      
回复 使用道具 举报
好多大神啊,技术分还差一点啊
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马