昨日講解了Java的資料型別分別基礎類型與引用類型兩大類,今天要介紹基於基礎類型延伸的包裝類別(Wrapper Class),將基礎類型封裝成類別,使其能擁有自己的行為,也因為這個特性,Java可以在編譯時實現基礎類型與包裝類別的轉換,這個轉換的操作被稱為自動裝箱(Boxing)與自動拆箱(UnBoxing),看似簡單的設計,卻提升了開發便利性,現在進入第六天的重點「裝箱與拆箱」
Java資料類別裝飾類別為Class還是屬於引用類型,因此不會套用「初始值」,唯有基礎型別才會自動預設初始值
這裡順便講解一個相關知識,即使基礎類型有預設初始值,但不是每種情境都會自動套用,現在用下面兩個實作說明,可能發生的問題
// 先建立一個 測試的類別並定義 printNums方法打印其屬性
class TestClass {
private int primativeNum; // 參考基礎類型屬性
private Integer wapperNum; // 參考裝飾類別屬性
public void printNums() {
System.out.println(primativeNum);
// output: 0
System.out.println(wapperNum);
// output: null
}
}
打印結果說明了Integer是屬於引用類型,wapperNum為 Null(沒有引用的記憶體位置), 因此primativeNum會直接輸出套用值
現在測試在 main 方法只宣告相同的類型的變數,不設定初始值的情況
int primativeNum; // 參考基礎類型屬性
Integer wapperNum; // 參考裝飾類別屬性
System.out.println(primativeNum);
// error: variable primativeNum might not havebeen initialized
System.out.println(wapperNum);
// error: variable wapperNum might not have beeninitialized
當在方法定義局部變數時,無論是基礎類型還是引用類型,都必須賦予初始值,否則會拋出數值「未初始化錯誤」
這裡牽涉一個很重要的觀念,經過反編譯(篇幅問題詳細作法後面在講)查看程式實際的執行邏輯,原因是「方法內的區域變數」沒有賦值,會導致編譯時無法確定初始值,就不會執行宣告變數的動作,接著產生錯誤訊息 error: variable 變數名稱 might not have beeninitialized,在來看TestClass類別的執行流程,當類別屬性沒有初始值時,JVM會依據類別屬性參考的類型,決定預設值
前面用反編譯查看程式執行方式,現在依樣畫葫蘆透過程式碼範例說明,用反編譯後的內容看裝箱與拆箱的實現方式
Integer wapperNum = 999;
int primativeNum = wapperNum;
System.out.println(primativeNum == wapperNum);
// output: true
反編譯後的部分內容
0: sipush 999
3: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
6: astore_1
7: aload_1
8: invokevirtual #13 // Method java/lang/Integer.intValue:()I
11: istore_2
12: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
15: iload_2
16: aload_1
17: invokevirtual #13 // Method java/lang/Integer.intValue:()I
20: if_icmpne 27
23: iconst_1
現在來解讀兩個範例,無論是將包裝類賦予基礎類型,或是相反操作兩者皆會等值,實際上原理如同開頭所說Java在編譯時就利用類別內的方法,實現了資料類型轉換,現在透過第二個查看反編譯後的執行過程
配合上個部分撰寫的範例依序解讀指令
一開始執行的程式邏輯 -> Integer wapperNum = 999
執行時定義常數999,用來執行Integer包裝類的valueOf方法,將其常數值封裝至Integer類別內(Integer.value的實際值),這是將基礎類型轉換成包裝類的實際做法,也就是「裝箱」的實際運行原理,最後在將新生成的Integer實際值寫入記憶體編號為1的位置中
接著執行將包裝類賦值給基礎類型的邏輯 -> int primativeNum = wapperNum;
會先從記憶體編號為1的位置載入,取得上一行 wapperNum變數引用的記憶體位置,藉由得到的Integer實例執行intValue方法,將內部的屬性value經過處理,回傳相應的基礎類型,實現「拆箱」的動作,最後將回傳的int值 寫入記憶體編號為2的位置中
接著執行打印那行程式碼 --> System.out.println(primativeNum == wapperNum);
這段實現較為單純,就是從先從記憶體2的位置拿取int值,在從記憶體1拿取Integer實例,並透過intValue方法拆箱在進行比對,可以斷定只要是基礎類型進行比對,會比對實際資料值,並不是延續引用類型比對「記憶體位址」
以上就是藉由反編譯內容,檢視編譯時如何實現自動裝箱與拆箱
參考: