Java 有基本型別和參考型別(物件),基本型別在運算的時候效率更高,但多數套件(java.util)只能處理物件,兩者之間轉換需求,會用到包裝類別。
包裝類別產生的物件會包含一個字段,而這個字段可以儲存資料型別,也就是說,基本型別可以包裝到包裝類別裡。
Java API 的 java.lang package 底下有與 8 個基本型別相對應的類別,統稱包裝類別,之所以需要包裝類別是因為:
- 將基本型別包裝進物件
基本型別變數是以傳值(pass by value)方式在函數中進行資料傳遞;優點在於效率較高,但不能以方法對自己有任何操作,但轉成物件,就可通過方法去做到這件事(被當參數放進函式時或轉換基本型別的值)。
基本資料型別 | Wrapper class 類別 | 建構子參數型別 |
---|---|---|
byte | Byte | byte, String |
short | Short | short, String |
int | Integer | int, String |
long | Long | long, String |
float | Float | float, double, String |
double | Double | double, String |
char | Character | char |
boolean | Boolean | boolean, String |
- java.util API 只能處理物件
Java API 的 java.util 套件,例如 Scanner、Date、Collection 等,會需要以物件傳入,才能使用方法。 - Collection Framework(即 java.util.Collection),像是 ArrayList 跟 Vector,只能儲存物件(參考型別)或是非基本型別
- 物件可以在多執行緒的情況下支持同步
可實作 serializable 介面,後面文章會詳述。
轉換過程稱為裝箱(boxing),例如常用的 Integer.valueOf()
,而裝箱後的物件即可使用本身的方法操作(也有常數可使用,例如 Integer.MAX_VALUE)。
class IntergerWrapping {
public static void main() {
int num = 1;
int num2 = 1;
Integer integerWrapper = Integer.valueOf(num);
Integer integerWrapper2 = Integer.valueOf(num2);
System.out.println(integerWrapper.doubleValue() / 3); // 0.3333333333333333
System.out.println(integerWrapper.compareTo(integerWrapper2)); // 0
System.out.println(integerWrapper.hashCode() == integerWrapper2.hashCode()); // true
System.out.println(integerWrapper.equals(integerWrapper2)); // true
System.out.println(integerWrapper == integerWrapper2); // true
}
}
觀察上面的程式:
Integer.valueOf(num)
:手動呼叫 Integer 的 valueOf() 方法包裝 num 這個基本型別變數doubleValue()
:取得該物件 double 型別的值,可以換成同類型的方法,例如這邊可用intValue()
或floatValue()
compareTo()
:可與另一個同類別物件比較,相同回傳 0,小於參數的值回傳 -1,大於則 1- 轉換成類別後仍然存在相同記憶體位址
Autoboxing 自動裝箱
Java 1.5 前的版本將參考型別變數與基本型別變數視為完全不同的存在(完全不能轉換),以 i++ 這個運算式,在當時編譯會回報錯誤。
Integer i = new Integer(55);
i++; // 編譯器認為 i 是包裝類別,不具基本型別運算功能
實際執行時,需先以 intValue()
取得 i 值,進行 +1 運算後,再重新包裝類別。
i = Integer.valueOf(i,intValue() + 1);
// 雖然 new Integer() 當時可行,但官方不建議,Java 9 開始 Wrapper Class 的建構子都標示 Deprecated
針對無法轉換的不便,逐漸新增功能,至 Java 5 開始提供自動裝箱/拆箱的特性,編譯器會參考宣告數的型別,自動判斷能否裝箱,就是自動裝箱機制,相當於語法糖的概念,宣告包裝類別或當成參數放入時就會用到。
Integer num1 = new Integer.valueOf(55); // Manual
Integer num2 = 55; // Autoboxing
System.out.println(num1 == num2); // true
Autoboxing 應用在許多場景,紀錄幾個常見的:
Autoboxing 的應用 - 1
ArrayList 類別的 add 方法,以值傳入編譯器會自動轉換成 Integer 類別,再放入 ArrayList,也是運用 Autoboxing。
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Integer max = Collections.max(list);
Autoboxing 的應用 - 2
Scanner 搭配邏輯判斷也會運用 Autoboxing。
Scanner sc = new Scanner(System.in);
Integer i = sc.nextInt(); // Autoboxing
int i = sc.nextInt(); // Autoboxing + Autounboxing
System.out.println(i);
Unboxing 自動拆箱
自動裝箱的相反即為自動拆箱,就是自動取出包裝類別的基本型別資料,變數(integerWrapper)會參考自身類別。
int num = 1;
Integer integerWrapper = Integer.valueOf(num); // 裝箱
Integer integerWrapper2 = Integer.valueOf(num); // 自動裝箱
int numAgain = integerWrapper2; // 自動拆箱
進行運算時會自動裝箱及拆箱
Integer num2 = 10;
System.out.println(number++); // 自動拆箱
Boolean value = true;
System.out.println( value && true); // 自動拆箱
編譯器的自動裝箱、拆箱功能細節
Integer i = null;
int j = i;
這段程式碼編譯成功,但執行會失敗,中間編譯器會展開這段程式,因為 intValue()
沒有參考物件(localObject 為 null,null 代表一個特殊物件,任何類別宣告變數都可以指向 null,表示沒有參考至物件實體)。
Object localObject = null;
int i = localObject.intValue();
Overload(重載)的執行判斷
前面文章提到 Overload 的概念,是在同名、同引數個數下,編譯器會將這些函式視為不同,方便開發人員對函式的命名,對於 Wrapper Class 的判斷,會走過這些流程:
- 搜尋同名、同引數函式
- Promotion 型態晉升/Autoboxing
- 作為參數放進函式
情況一
// 1
public void methodA(int x){};
// 2
public void methodA(long x){};
// 3
public void methodA(Integer x){};
// 4
public void methodA(Long x){};
// 5
public void methodA(int... x){};
函數呼叫 methodA(10),編譯器中間做了:
- 找出上面的 1-5 方法,判斷 1 最符合(同為 int,只有一個引數)
- 引數型別一樣是 int,不需要 Promotion 與 Autoboxing
- 放入參數 + 執行 1 方法
情況二
// 2
public void methodA(long x){};
// 3
public void methodA(Integer x){};
// 4
public void methodA(Long x){};
// 5
public void methodA(int... x){};
相同地呼叫 methodA(10),此情況流程:
- 找出方法 2-5,判斷 2 最符合(一樣基本型別)
- 引數型別 long,執行 Promotion
- 放入參數 + 執行 2 方法
情況三
// 3
public void methodA(Integer x){};
// 4
public void methodA(Long x){};
// 5
public void methodA(int... x){};
相同地呼叫 methodA(10),此情況流程:
- 找出方法 3-5,判斷 3 最符合(Integer 為 int 包裝類別)
- 引數型別 Integer,執行 Autoboxing
- 放入參數 + 執行 3 方法
情況四
// 4
public void methodA(Long x){};
// 5
public void methodA(int... x){};
相同地呼叫 methodA(10),此情況流程:
- 找出方法 4、5,判斷 5 最符合(型別為 int,引數可放入一個就好)
- 放入參數 + 執行 5 方法
情況五
// 4
public void methodA(Long x){};
只剩方法 4 的情況下,編譯器一樣會呼叫,但執行會報錯:
- 找出方法 4
- 執行 Autoboxing 轉為 Integer,但 Integer 物件不會自動 Promotion 為 Long(雖然此方法行不通,但編譯器也不會先進行 Promotion 為 long,再 Autoboxing 為 Long)
- 回報
incompatible types: int cannot be converted to Long
的錯誤
常用的包裝類別方法
toString()
:每個包裝類別的預設寫法不同valueOf()
compareTo()
:可與另個同類別物件比較,相同回傳 0,小於參數的值回傳 -1,大於則 1parse[Type]()
:例如parseInt
、parseShort
,可轉換至指定型別equals()
:相對於==
比較記憶體位址 & 值,equals()
只比較值
包裝類別方便程式撰寫的使用,但運算時會影響執行效率,但不能完全取代基本資料型別。