合計數的玩笑

出自《java puzzle》


下面的程序在一個類中計算並緩存了一個合計數,並且在另一個類中打印了這個合計數。那麼,這個程序將打印出什麼呢?這裏給一點提示:你可能已經回憶起來了,在代數學中我們曾經學到過,從1到n的整數總和是n(n+1)/2。

class Cache {
static {
initializeIfNecessary();
}
private static int sum;
public static int getSum() {
initializeIfNecessary();
return sum;
}
private static boolean initialized = false;
private static synchronized void initializeIfNecessary() {
if (!initialized) {
for (int i = 0; i < 100; i++)
sum += i;
initialized = true;
}
}
}
public class Client {
public static void main(String[] args) {
System.out.println(Cache.getSum());
}
}


草草地看一遍,你可能會認爲這個程序從1加到了100,但實際上它並沒有這麼做。再稍微仔細地看一看那個循環,它是一個典型的半開循環,因此它將從0循環到99。有了這個印象之後,你可能會認爲這個程序打印的是從0到99的整數總和。用前面提示中給出的公式,我們知道這個總和是99×100/2,即4,950。

但是,這個程序可不這麼想,它打印的是9900,是我們所預期值的整整兩倍。是什麼導致它如此熱情地翻倍計算了這個總和呢?

該程序的作者顯然在確保sum在被使用前就已經在初始化這個問題上,經歷了衆多的麻煩。該程序結合了惰性初始化和積極初始化,甚至還用上了同步,以確保緩存在多線程環境下也能工作。看起來這個程序已經把所有的問題都考慮到了,但是它仍然不能正常工作。它到底出了什麼問題呢?

該程序受到了類初始化順序問題的影響。爲了理解其行爲,我們來跟蹤其執行過程。在可以調用Client.main之前,VM必須初始化Client類。這項初始化工作異常簡單,我們就不多說什麼了。Client.main方法調用了Cache.getsum方法,在getsum方法可以被執行之前,VM必須初始化Cache類。
回想一下,類初始化是按照靜態初始器在源代碼中出現的順序去執行這些初始器的。Cache類有兩個靜態初始器:在類頂端的一個static語句塊,以及靜態域initialized的初始化。靜態語句塊是先出現的,它調用了方法initializeIfNecessary,該方法將測試initialized域。因爲該域還沒有被賦予任何值,所以它具有缺省的布爾值false。與此類似,sum具有缺省的int值0。因此,initializeIfNecessary方法執行的正是你所期望的行爲,將4,950添加到了sum上,並將initialized設置爲true。

在靜態語句塊執行之後,initialized域的靜態初始器將其設置回false,從而完成Cache的類初始化。遺憾的是,sum現在包含的是正確的緩存值,但是initialized包含的卻是false:Cache類的兩個關鍵狀態並未同步。

此後,Client類的main方法調用Cache.getSum方法,它將再次調用initializeIfNecessary方法。因爲initialized標誌是false,所以initializeIfNecessary方法將進入其循環,該循環將把另一個4,950添加到sum上,從而使其值增加到了9,900。getSum方法返回的就是這個值,而程序打印的也是它。
很明顯,該程序的作者認爲Cache類的初始化不會以這種順序發生。由於不能在惰性初始化和積極初始化之間作出抉擇,所以作者同時運用這二者,結果產生了大麻煩。要麼使用積極初始化,要麼使用惰性初始化,但是千萬不要同時使用二者。

如果初始化一個域的時間和空間代價比較低,或者該域在程序的每一次執行中都需要用到時,那麼使用積極初始化是恰當的。如果其代價比較高,或者該域在某些執行中並不會被用到,那麼惰性初始化可能是更好的選擇[EJ Item 48]。另外,惰性初始化對於打破類或實例初始化中的循環也可能是必需的。

通過重排靜態初始化的順序,使得initialized域在sum被初始化之後不被複位到false,或者通過移除initialized域的顯式靜態初始化操作,Cache類就可以得到修復。儘管這樣所產生的程序可以工作,但是它們仍舊是混亂的和病構的。Cache類應該被重寫爲使用積極初始化,這樣產生的版本很明顯是正確的,而且比最初的版本更加簡單。

使用這個版本的Cache類,程序就可以打印出我們所期望的4950:


class Cache {
private static final int sum = computeSum();
private static int computeSum() {
int result = 0;
for (int i = 0; i < 100; i++)
result += i;
return result;
}
public static int getSum() {
return sum;
}
}


請注意,我們使用了一個助手方法來初始化sum。助手方法通常都優於靜態語句塊,因爲它讓你可以對計算命名。只有在極少的情況下,你才必須使用一個靜態語句塊來初始化一個靜態域,此時請將該語句塊緊隨該域聲明之後放置。這提高了程序的清晰度,並且消除了像最初的程序中出現的靜態初始化與靜態語句塊互相競爭的可能性。

總之,請考慮類初始化的順序,特別是當初始化顯得很重要時更是如此。請你執行測試,以確保類初始化序列的簡潔。請使用積極初始化,除非你有某種很好的理由要使用惰性初始化,例如性能方面的因素,或者需要打破初始化循環。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章