2012年11月4日 星期日

那些年,我們一起寫的BUG-Primitive VS. Wrapper Class

人生只有比較,就失去自己的意義了。
                                                                                           - 俠刀 蜀道行


這個bug是實際發生在我身邊的,事情是這樣的:
有一天,我的兩個同事拿著兩支手機在測一個行為,很簡單的行為。就是從資料庫中抓一個整數值,再用現存的值跟資料庫得到的值做比較,如果一樣就顯示結果A;否則就顯示結果B。
但是兩個人卻測出不同的結果,然後又試了好幾支不同的手機後,結果發現偏偏只有那一支有不一樣的結果,真的是非常的奇怪。然而,在經過我同事細心的測試和追查後,終於找到原因,原來那支手機剛剛被我用過,結果被我搞爛了。

可是為什麼會這麼容易被我搞爛呢?其實是因為程式有bug,在因緣巧合下讓我把它從潛在的狀態中解放出來,所以並不是我的錯喔!而那段程式片段概略如下:

int getFromDB() {
        //do something
        return value;
}

Integer value1 = mValue;
Integer value2 = getFromDB();
if (value1 == value2) {
        System.out.println(“A”);
}
else {
        System.out.println(“B”);
}

根據正確的邏輯,我們要得到的結果是A,而不是B。也就是說,在一般情況下value1和value2會擁有相同的值。上述的程式碼乍看之下,好像沒有什麼問題,但它還是造成了兩個不一樣的結果。我想,眼尖的人也許已經發現到問題在哪裡。問題就發生在if條件判斷式的部分。value1和value2是物件而不是基本型別的變數,如果直接用==比較,比較的是物件的記憶體位址而不是它們實際的值。既然比較的是記憶體位址,兩個不同的變數,應該存在於不同的記憶體位址,應該永遠只會走到B的路線。可是弔詭的事發生了,大部分的手機走的都是A路線,只有那個被我搞爛的手機走到了B路線。

其實這個現象是由於Java中wrapper class和autoboxing的機制所造成。在解釋原因前,首先介紹Java中的wrapper class。wrapper class的出現是因為,Java裡面提供的許多API只能針對物件(例如,Collection),無法適用於基本型別(即int, long, char, boolean等)。為了使這些基本型別也可以適用,所以Java創造了與這些基本型別相對應的wrapper class。

boolean <-> Boolean
byte <-> Byte
char <-> Character
short <->Short
int <-> Integer
long <-> Long
float <-> Float
double <-> Double

至於autoboxing,就是自動的把primitive轉成wrapper的機制;而與unboxing就是自動把wrapper轉成primitive的機制。例如,在Java中如果直接將基礎型別的值或字面常數(例如,1, true, ’a’, “ABC”)賦值給wrapper class的話,Java就會啟動autoboxing的機制,產生一個屬於該型別的物件,並把該值存到該物件中,讓基礎型別變成物件。再深入一點探討的話就會發現,這些自動產生的wrapper class物件其實會構成一個pool。當autoboxing再度發生時,Java就會先檢查pool中是否有相同的值,有的話就直接回傳有相同值的物件;否則的話,就會再產生一個新的物件來存這個值。

因為這樣的機制,當value1和value2值一樣的時候,就會指向同一個記憶體,使得流程往分支A地方走。既然是這樣的話,怎麼會導致另一個同事出現往B走的現象呢?其實這又牽扯到autoboxing的另一個重要的部分。
Autoboxing的原意是要減少實體化的物件數,因此透過建立了一個pool來儲存這些物件,但這也造成另一個問題,也就是這些物件不會隨便被刪除掉。如果沒有限制的建立這些物件,記憶體一定很快就會用光的,所以Java給每個wrapper class的pool設立了最大值的暫存限制。也就是說,當要賦予的值超出某個範圍的話,就會以一般物件建立的方式建立(ex. Integer I = new Integer(321);)。

Boolean:  (全部暫存)
Byte:         (全部暫存)
Character:    [0, 127] 暫存
Short :         [-128, 127] 暫存
Long:           [-128, 127] 暫存
Float:        (沒有暫存)
Double:     (沒有暫存)

因此,當value1或value2的值超過127時,value1和value2就會指向不同的記憶體位置,使得程式往分支B的部分走。到此我們就可以清楚的了解到,造成我的兩個同事測出不同結果的關鍵原因。

透過這個例子,我們可以體悟到一個重要的原則:只要操作到物件之間的比較,盡量使用equals或是compareTo的方法,而不要直接>, <, ==等運算子來比較,除非有特殊的需求。

//Fixed
if (value1.equals(value2)) {
        System.out.println(“A”);
}
else {
        System.out.println(“B”);
}

沒有留言:

張貼留言