iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0

昨日講解了Java的資料型別分別基礎類型與引用類型兩大類,今天要介紹基於基礎類型延伸的包裝類別(Wrapper Class),將基礎類型封裝成類別,使其能擁有自己的行為,也因為這個特性,Java可以在編譯時實現基礎類型與包裝類別的轉換,這個轉換的操作被稱為自動裝箱(Boxing)與自動拆箱(UnBoxing),看似簡單的設計,卻提升了開發便利性,現在進入第六天的重點「裝箱與拆箱」

在談Java資料型別與初始值

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方法拆箱在進行比對,可以斷定只要是基礎類型進行比對,會比對實際資料值,並不是延續引用類型比對「記憶體位址」

以上就是藉由反編譯內容,檢視編譯時如何實現自動裝箱與拆箱

參考:

  1. GPT幫忙解釋反編譯的指令

上一篇
第五日 基礎類型與引用類型
下一篇
第七日 從ByteCode看型別轉換
系列文
掌握Java神器,駕馭SpringBoot猛獸30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
yale918
iT邦新手 4 級 ‧ 2023-09-20 00:21:13

推推蛙哥!

我要留言

立即登入留言