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

字號:

謎題26和27中的程序一樣,下面的程序有一個單重的循環(huán),它記錄迭代的次數,并在循環(huán)終止時打印這個數。那么,這個程序會打印出什么呢?
    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,畢竟,循環(huán)變量(f)被初始化為2,000,000,000,而終止值比初始值大50,并且這個循環(huán)具有傳統的“半開”形式:它使用的是 < 操作符,這是的它包括初始值但是不包括終止值。
    然而,這種分析遺漏了關鍵的一點:循環(huán)變量是float類型的,而非int類型的?;叵胍幌轮i題28,很明顯,增量操作(f++)不能正常工作。F的初始值接近于Integer.MAX_VALUE,因此它需要用31位來精確表示,而float類型只能提供24位的精度。對如此巨大的一個float數值進行增量操作將不會改變其值。因此,這個程序看起來應該無限地循環(huán)下去,因為f永遠也不可能解決其終止值。但是,如果你運行該程序,就會發(fā)現它并沒有無限循環(huán)下去,事實上,它立即就終止了,并打印出0。怎么回事呢?
    問題在于終止條件測試失敗了,其方式與增量操作失敗的方式非常相似。這個循環(huán)只有在循環(huán)索引f比(float)(START + 50)小的情況下才運行。在將一個int與一個float進行比較時,會自動執(zhí)行從int到float的提升[JLS 15.20.1]。遺憾的是,這種提升是會導致精度丟失的三種拓寬原始類型轉換的一種[JLS 5.1.2]。(另外兩個是從long到float和從long到double。)
    f的初始值太大了,以至于在對其加上50,然后將結果轉型為float時,所產生的數值等于直接將f轉換成float的數值。換句話說,(float)2000000000 == 2000000050,因此表達式f < START + 50即使是在循環(huán)體第一次執(zhí)行之前就是false,所以,循環(huán)體也就永遠的不到機會去運行。
    訂正這個程序非常簡單,只需將循環(huán)變量的類型從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表示是相同的。
    這個謎題寓意很簡單:不要使用浮點數作為循環(huán)索引,因為它會導致無法預測的行為。如果你在循環(huán)體內需要一個浮點數,那么請使用int或long循環(huán)索引,并將其轉換為float或double。在將一個int或long轉換成一個float或double時,你可能會丟失精度,但是至少它不會影響到循環(huán)本身。當你使用浮點數時,要使用double而不是float,除非你肯定float提供了足夠的精度,并且存在強制性的性能需求迫使你使用float。適合使用float而不是double的時刻是非常非常少的。
    對語言設計者的教訓,仍然是悄悄地丟失精度對程序員來說是非常令人迷惑的。請查看謎題31有關這一點的深入討論。