JAVA循環(huán)謎題34:被計(jì)數(shù)擊倒了

字號(hào):

謎題26和27中的程序一樣,下面的程序有一個(gè)單重的循環(huán),它記錄迭代的次數(shù),并在循環(huán)終止時(shí)打印這個(gè)數(shù)。那么,這個(gè)程序會(huì)打印出什么呢?
    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);
     }
    }
    表面的分析也許會(huì)認(rèn)為這個(gè)程序?qū)⒋蛴?0,畢竟,循環(huán)變量(f)被初始化為2,000,000,000,而終止值比初始值大50,并且這個(gè)循環(huán)具有傳統(tǒng)的“半開”形式:它使用的是 < 操作符,這是的它包括初始值但是不包括終止值。
    然而,這種分析遺漏了關(guān)鍵的一點(diǎn):循環(huán)變量是float類型的,而非int類型的。回想一下謎題28,很明顯,增量操作(f++)不能正常工作。F的初始值接近于Integer.MAX_VALUE,因此它需要用31位來精確表示,而float類型只能提供24位的精度。對如此巨大的一個(gè)float數(shù)值進(jìn)行增量操作將不會(huì)改變其值。因此,這個(gè)程序看起來應(yīng)該無限地循環(huán)下去,因?yàn)閒永遠(yuǎn)也不可能解決其終止值。但是,如果你運(yùn)行該程序,就會(huì)發(fā)現(xiàn)它并沒有無限循環(huán)下去,事實(shí)上,它立即就終止了,并打印出0。怎么回事呢?
    問題在于終止條件測試失敗了,其方式與增量操作失敗的方式非常相似。這個(gè)循環(huán)只有在循環(huán)索引f比(float)(START + 50)小的情況下才運(yùn)行。在將一個(gè)int與一個(gè)float進(jìn)行比較時(shí),會(huì)自動(dòng)執(zhí)行從int到float的提升[JLS 15.20.1]。遺憾的是,這種提升是會(huì)導(dǎo)致精度丟失的三種拓寬原始類型轉(zhuǎn)換的一種[JLS 5.1.2]。(另外兩個(gè)是從long到float和從long到double。)
    f的初始值太大了,以至于在對其加上50,然后將結(jié)果轉(zhuǎn)型為float時(shí),所產(chǎn)生的數(shù)值等于直接將f轉(zhuǎn)換成float的數(shù)值。換句話說,(float)2000000000 == 2000000050,因此表達(dá)式f < START + 50即使是在循環(huán)體第一次執(zhí)行之前就是false,所以,循環(huán)體也就永遠(yuǎn)的不到機(jī)會(huì)去運(yùn)行。
    訂正這個(gè)程序非常簡單,只需將循環(huán)變量的類型從float修改為int即可。這樣就避免了所有與浮點(diǎn)數(shù)計(jì)算有關(guān)的不精確性:
    for (int f = START; f < START + 50; f++)
     count++;
    如果不使用計(jì)算機(jī),你如何才能知道2,000,000,050與2,000,000,000有相同的float表示呢?關(guān)鍵是要觀察到2,000,000,000有10個(gè)因子都是2:它是一個(gè)2乘以9個(gè)10,而每個(gè)10都是5×2。這意味著2,000,000,000的二進(jìn)制表示是以10個(gè)0結(jié)尾的。50的二進(jìn)制表示只需要6位,所以將50加到2,000,000,000上不會(huì)對右邊6位之外的其他為產(chǎn)生影響。特別是,從右邊數(shù)過來的第7位和第8位仍舊是0。提升這個(gè)31位的int到具有24位精度的float會(huì)在第7位和第8位之間四舍五入,從而直接丟棄最右邊的7位。而最右邊的6位是2,000,000,000與2,000,000,050位以不同之處,因此它們的float表示是相同的。
    這個(gè)謎題寓意很簡單:不要使用浮點(diǎn)數(shù)作為循環(huán)索引,因?yàn)樗鼤?huì)導(dǎo)致無法預(yù)測的行為。如果你在循環(huán)體內(nèi)需要一個(gè)浮點(diǎn)數(shù),那么請使用int或long循環(huán)索引,并將其轉(zhuǎn)換為float或double。在將一個(gè)int或long轉(zhuǎn)換成一個(gè)float或double時(shí),你可能會(huì)丟失精度,但是至少它不會(huì)影響到循環(huán)本身。當(dāng)你使用浮點(diǎn)數(shù)時(shí),要使用double而不是float,除非你肯定float提供了足夠的精度,并且存在強(qiáng)制性的性能需求迫使你使用float。適合使用float而不是double的時(shí)刻是非常非常少的。
    對語言設(shè)計(jì)者的教訓(xùn),仍然是悄悄地丟失精度對程序員來說是非常令人迷惑的。請查看謎題31有關(guān)這一點(diǎn)的深入討論。