基本型態包裹器

May 24, 2022

使用基本型態目的在於效率,然而更多時候,會使用類別建立實例,因為物件本身可以攜帶更多資訊,如果要讓基本型態像物件一樣操作,可以使用 LongIntegerDoubleFloatBooleanByte 等類別來包裹(Wrap)基本型態。

包裹器

LongIntegerDoubleFloatBooleanByte 類別主要目的,就是提供物件實例作為「殼」,將基本型態包裹在物件之中,如此就可以操作這個物件,就像是將基本型態當作物件操作。來看個簡單的例子:

package cc.openhome;

public class IntegerDemo {
    public static void main(String[] args) {
        int data1 = 10;
        int data2 = 20;
        
        var wrapper1 = Integer.valueOf(data1);
        var wrapper2 = Integer.valueOf(data2);
        
        System.out.println(data1 / 3);
        System.out.println(wrapper1.doubleValue() / 3);
        System.out.println(wrapper1.compareTo(wrapper2));
    }
}

基本型態包裹器都是歸類於 java.lang 套件中,如果要使用 Integer 包裹 int 型態資料,方法之一是透過 Integer.valueOf

不要使用 new 建立基本型態包裹器,從 Java SE 9 開始,基本型態包裹器的建構式都標示為棄用(Deprecated)。

若運算式中都是 int,就只會在 int 空間中做運算,結果會是 int 整數,因此 data1 / 3 就會顯示 3 的結果。可以操作 IntegerdoubleValue 將包裹值以 double 型態傳回,如此就會在 double 空間中做相除,結果就會顯示 3.33333333333…。

Integer 提供 compareTo 方法,可與另一個 Integer 物件進行比較,如果包裹值相同就傳回 0,小於 compareTo 傳入物件包裹值就傳回 -1,否則就是 1,與 ==!= 只能比較是否相等或不相等,compareTo 方法傳回更多資訊。

自動裝箱

除了使用 Integer.valueOf,也可以直接透過自動裝箱(Auto boxing)包裹基本型態:

Integer number = 10;

編譯器會自動判斷是否能進行自動裝箱,在上例中你的 number 會參考 Integer 實例;同樣的動作可適用於 booleanbyteshortcharlongfloatdouble 等基本型態,分別會使用對應的 BooleanByteShortCharacterIntegerLongFloatDouble 包裹基本型態。

方才的範例可以寫為:

Integer data1 = 10;
Integer data2 = 20;
System.out.println(data1.doubleValue() / 3);
System.out.println(data1.compareTo(data2));

程式看來簡潔許多,data1data2 在運行時會參考 Integer 實例,可以直接進行物件操作。自動裝箱運用的方法還可以如下:

int number = 10;
Integer wrapper = number;

自動拆箱

可以自動裝箱,也可以自動拆箱(Auto unboxing),也就是自動取出包裹器中的基本形態資訊。例如:

Integer wrapper = 10;  // 自動裝箱
int foo = wrapper;     // 自動拆箱

wrapper 會參考至 Integer,若被指定給 int 型的變數 foo,則會自動取得包裹的 int 型態再指定給 foo

在運算時,也可以進行自動裝箱與拆箱,例如:

Integer number = 10;
System.out.println(number + 10);
System.out.println(number++);

上例中會顯示 20 與 10,編譯器會自動自動裝箱與拆箱,也就是 10 會先裝箱,然後在 number + 10 時會先對 number 拆箱,再進行加法運算;number++ 該行也是先對i拆箱再進行遞增運算。再來看一個例子:

Boolean foo = true;
System.out.println(foo && false);

同樣地,foo 會參考至 Boolean 實例,在進行 && 運算時,會先將 foo 拆箱,再與 false 進行 && 運算,結果會顯示 false

裝箱、拆箱的內幕

裝箱與拆箱的功能事實上是編譯器蜜糖(Compiler sugar),也就是編譯器讓你撰寫程式時吃點甜頭,編譯時期依撰寫的語法,決定是否進行裝箱或拆箱動作。例如:

Integer i = 100;

編譯器會自動將程式碼展開為:

Integer i = Integer.valueOf(100);

下面的程式是可以通過編譯的:

Integer i = null;
int j = i;

但是在執行時期會有錯誤,編譯器會將之展開為:

Integer integer = null;
int i = integer.intValue();

在 Java 程式碼中,null 代表一個特殊物件,任何類別宣告的參考名稱都可以參考至 null,表示該名稱沒有參考至任何物件實體,這相當於有個名牌沒有任何人佩戴。在上例中,由於 i 並沒有參考至任何物件,就不可能操作 intValue 方法,就相當於有個名牌沒有人佩戴,你卻要求戴名牌的人舉手,這是一種錯誤,會出現 NullPointerException 的錯誤訊息。

編譯器蜜糖通常提供了方便性,但也因此隱藏了一些細節,別只顧著吃糖而忽略了該知道的觀念。來看看,如果你如下撰寫,結果會是如何?

Integer i1 = 100;
Integer i2 = 100;
if (i1 == i2) {
    System.out.println("i1 == i2");
}
else {
    System.out.println("i1 != i2");
}

如果只看 Integer i1 = 100,就好像在看 int i1 = 100,直接使用 == 進行比較,有的人會理所當然回答顯示 i1 == i2,那麼底下這個呢?

Integer i1 = 200;
Integer i2 = 200;
if (i1 == i2) {
    System.out.println("i1 == i2");
}
else {
    System.out.println("i1 != i2");
}

程式碼只不過將 100 改為 200,但執行結果會顯示 i1 != i2,這是為何?先前提過,裝箱是編譯器蜜糖,以上例來說,實際上會使用 Integer.valueOf 來建立 Integer 實例,查看 Integer.java 的 valueOf 的實作內容:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

這段程式碼簡單來說,就是如果傳入的 intIntegerCache.lowIntegerCache.high 之間,那就嘗試看看先前快取(Cache)中有沒有包裹過相同的值,如果有就直接傳回,否則就使用 new 建構新的 Integer 實例。

IntegerCache.low 預設值是 -128,執行時期無法更改,IntegerCache.high 預設值是 127,可以於啟動 JVM時,使用系統屬性 java.lang.Integer.IntegerCache.high 來指定。例如:

> java -Djava.lang.Integer.IntegerCache.high=300 cc.openhome.Demo

如上指定之後,Integer 就會針對 -128 到 300 範圍中建立的包裹器進行快取,而針對先前 i1i2 包裹 200 時,使用 == 比較的結果,就又顯示 i1 == i2 了。

別使用 ==!= 來比較兩個物件實質內容值是否相同(因為 ==!= 是比較物件參考),而要使用 equals。例如以下的程式碼:

Integer i1 = 200;
Integer i2 = 200;
if (i1.equals(i2)) {
    System.out.println("i1 == i2");
}
else {
    System.out.println("i1 != i2");
}

無論實際上 i1i2 包裹的值座落在哪個範圍,只要 i1i2 包裹的值相同,equals 比較的結果就會是 true

分享到 LinkedIn 分享到 Facebook 分享到 Twitter