Java SE基礎鞏固(一):基本型別的包裝類原始碼解讀
Java中變數型別可分為兩類:基本型別和引用型別。基本型別有8種,分別是short,int,long,byte,char,float,double,boolean,同時也有8種引用型別作為其包裝類,例如Integer,Double等。本文要討論的就是這些基本型別和其包裝類。
下面涉及到原始碼的部分也僅僅列出部分有代表性的原始碼,不會有大段的程式碼。我使用的JDK版本是JDK1.8_144
1 Integer(整形)
Integer是基本型別int的包裝型別。下圖是其繼承體系:
Integer繼承了Number類,實現了Comparable即可,擁有可比較的能力,還間接的實現了Serializable介面,即是可序列化的。實際其他幾個數字相關的包裝型別都是這麼一個繼承體系,所以如果下文中沒有特殊說明,就表示繼承體系和Integer一樣。
先來看看valueOf()方法,在Integer類裡有三個過載的valueOf()方法,但最終都會呼叫public static Integer valueOf(int i)。下面是public static Integer valueOf(int i)的原始碼:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
複製程式碼
可以看到,該方法接受一個基本型別int引數,並返回一個Integer物件,需要注意的是那個if判斷,這裡會判斷這個i是否在[IntegerCache.low,IntegerCache.high]區間裡,如果在就直接返回快取值,那這個low和hight的值是多少呢?我們到IntegerCache類裡看看:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i,127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i,Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int,ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128,127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
複製程式碼
這是Integer的一個內部靜態類,從程式碼中,不難看出low的值是-128,high的值預設是127,其中high值可以通過java.lang.Integer.IntegerCache.high屬性來設定,這個類的邏輯程式碼幾乎都在static塊裡,也就是說在類載入的時候會被執行了,執行的結果是什麼呢?關注這幾行程式碼:
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
複製程式碼
將區間在[low,hight]的值都構造成Integer物件,並儲存在cache陣列裡。這時候再回頭看看valueOf()方法,發現如果引數i的值在[low,hight]區間裡,那麼就會返回cache裡對應的Integer物件,話句話說,如果引數在這個區間範圍裡,那麼呼叫該方法的時候就不需要建立物件了,直接使用快取裡的物件,減少了建立物件的開銷。
那為什麼要搞這麼複雜呢?因為Java5提供了自動裝箱和自動拆箱的語法糖,而這個方法其實就是為自動裝箱服務的(從註釋可以看到,這個方法從java5才開始有的,由此可推斷應該和自動裝箱拆箱機制有關),Integer是一個非常常用的類,有了這個快取機制,就不需要每次在自動裝箱或者手動呼叫的時候建立物件了,大大提高了物件複用率,也提供了執行效率。[-128,127]是一個比較保守的範圍,使用者可以通過修改java.lang.Integer.IntegerCache.high來修改high值,但最好不要太大,因為畢竟Intger物件也是要佔用不少記憶體的,如果上界設定的太大,而大的值又不經常使用,那麼這個快取就有些得不償失了。
再來看看另一個方法intValue(),這個方法就非常簡單了:
public int intValue() {
return value;
}
複製程式碼
value是Integer類的私有欄位,型別是int,就是代表了實際的值,該方法實際上就是自動拆箱的時候呼叫的方法。
其他方法就沒什麼可說的了,大多數是一些功能性的方法(例如max,min,toBinaryString)和一些常量(例如MAX_VALUE,MIN_VALUE),感興趣的朋友可以自行檢視,演演算法邏輯也都不難。
其他幾個就不多說了,在理解了Integer的原始碼之後,其他的都不難看懂。
2 自動裝箱和自動拆箱
自動裝箱和自動拆箱是Java5提供的語法糖,當我們需要在基本型別和包裝類之間進行比較、賦值等操作的時候,編譯器會幫我們自動進行型別轉換,裝箱就是將基本型別轉換成包裝型別,拆箱就是與之相反的一個過程。例如,我們有下面這樣的程式碼:
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
int a = 1,b = 2,c = 3;
list.add(a);
list.add(b);
list.add(c);
}
複製程式碼
編譯後的.class檔案內容如下(做了轉碼的,實際上.class檔案應該是位元組碼,但為了方便檢視,就將其轉成了java程式碼的形式):
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();
byte var2 = 1;
byte var3 = 2;
byte var4 = 3;
var1.add(Integer.valueOf(var2));
var1.add(Integer.valueOf(var3));
var1.add(Integer.valueOf(var4));
}
複製程式碼
這裡就發生了裝箱操作,編譯器幫我們呼叫了Integer.valueOf()方法將int型別轉換了Integer型別以適應List容器(容器的型別引數不能是基本型別)。
拆箱也類似,只是方向和裝箱相反而已,一般發生在將包裝型別的值賦值給基本型別時候,例如:
Integer a = 1;
int b = a; //自動拆箱
複製程式碼
如果沒有自動拆箱,上面的程式碼肯定是無法編譯通過,因為型別不匹配,又沒有進行強轉。
關於自動裝箱和拆箱更多的內容,網上有很多資料(非常非常多),建議有興趣的朋友可以到網上搜索搜尋。
3 選擇基本型別還是包裝型別呢?
首先,我們先明確一個基本原則:如果某個場景中不能使用基本型別和包裝型別的其中一種,那麼就只能選擇另外一種。例如,容器的泛型型別引數不能是基本型別,那就不要有什麼猶豫了,直接使用包裝型別吧。
當兩種型別都可用的時候,建議使用基本型別,主要原因有如下3個:
-
包裝型別是一個類,要使用某個值就必須要建立物件,即使有快取的情況下可以節省建立物件的時間開銷,但空間開銷仍然存在,一個物件包含了物件頭,例項資料和物件填充,但實際上我們僅僅需要例項資料而已,對於Integer來說,我們僅僅需要value欄位的值而已,也就是說使用基本型別int只佔用了4個位元組的空間,而使用物件需要幾倍於基本型別的空間(大家可以計算一下)。
-
接著第1條,物件是有可能為null的,這樣在使用的時候有時候不得不對其進行空值判斷,少數幾個還好,當業務邏輯複雜的時候(或者程式設計師寫程式碼寫上頭的時候),就非常容易遺漏,一旦遺漏就很有可能發生空指標異常,如果沒有嚴密的測試,還真不一定能測試出來。
-
包裝類在進行比較操作的時候有時候會讓人迷惑,我們舉個例子,假設有如下程式碼:
public static void main(String[] args) { Integer d = 220; Integer e = 220; int f = 220; System.out.println(d == e); System.out.println(e == f); } 複製程式碼
如果沒有遇到過這樣的問題,可能下意識的說出執行的結果是true和true,但實際上,輸出的結果是false,true。為什麼?對於第一個比較,d和e都是Integer物件,直接用==比較的實際上引用的值,而不是實際的值,在將220賦值給她們的時候實際上發生過自動裝箱,即呼叫過valueOf()方法,而220超出了預設的快取區間,那麼就會建立新的Interge物件,這兩個a和b的引用肯定就不相等,所以會輸出false。第二個比較中,f是基本型別,在這裡情況下,會發生自動拆箱,==兩邊的變數實際上基本型別int,直接比較時沒有問題的,所以輸出結果會是true。
總之,如果能用基本型別,最好還是使用基本型別,而不是其包裝類,如果實在無法使用基本型別,例如容器的泛型型別引數,才使用包裝類。
4 小結
本文介紹了Integre包裝類,對其原始碼進行了簡單分析,之所以沒有對其他基本型別的保證類進行分析是因為他們的實現非常相似,重複介紹就沒有意思了。還簡單討論了一下自動裝箱和自動拆箱,這個特性方便了程式設計師,提高了開發效率,但如果使用不當可能會造成令人迷惑的問題,算是有利有弊吧。對於在基本型別和包裝型別之間的選擇,我個人傾向於優先使用基本型別,原因也在文中有比較詳細的解釋。