Java自動拆箱和裝箱

什麼是自動拆箱和裝箱

定義

  • 自動裝箱就是 Java 自動將原始數據類型轉爲對應的包裝類對象 比如將 int 型的變量轉成 Integer對象 自動拆箱反之(從 Java 1.5 開始引入)

過程

  • 自動裝箱時,編譯器調用 valueOf() 將原始數據類型值轉爲對象;同時自動拆箱時,編譯器調用類似 intValue(), doubleValue() 這類方法將對象轉換成原始類型值
基本類型 大小 數值範圍 默認值 包裝類型
boolean ^ true,false false Boolean
byte 8bit -2^7 – 2^7-1 0 Byte
char 16bit \u0000 – \uffff \u0000 Character
short 16bit -2^15 – 2^15-1 0 Short
int 32bit -2^31 – 2^31-1 0 Integer
long 64bit -2^63 – 2^63-1 0 Long
float 32bit IEEE 754 0.0f Float
double 64bit IEEE 754 0.0d Double
void ^ ^ ^ Void

基本類型與裝箱基本類型的區別

  • 基本類型只有值,而裝箱基本類型則具有與它們的值不同的同一性。換句話說,對裝箱基本類型運用 == 操作符幾乎總是錯的

  • 基本類型只有功能完備的值,而裝箱基本類型除了它對應基本類型的所有功能值外,還有一個非功能值 null,這導致了對於包裝基本類型進行拆箱操作後所進行的操作存在 NPE 的風險

  • 基本類型更加省空間和時間,如果對包裝類型進行頻繁的裝箱和拆箱操作會影響性能

何時發生自動裝箱與拆箱?

賦值時

  • 在Java1.5之前,需要手動地進行類型轉換,而現在所有的轉換都是有編譯器來完成
//before autoboxing
Integer iObject = Integer.valueOf(3);
int iPrimitive = iObject.intValue()

//after java5
Integer iObject = 3;      //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion

方法調用時

  • 當在進行方法調用時,可以傳入原始數據值或對象,編譯器同樣會自動進行轉換
public static Integer show(Integer iParam){
   System.out.println("autoboxing example - method invocation i: " + iParam);
   return iParam;
}

//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer

自動裝箱引起的性能問題

  • 如果有人告訴你:“只要修改一個字符,下面這段代碼的運行速度就能提高5倍。”,你覺得可能麼?
long t = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms");
  • 輸出結果:

total:2305843005992468481
processing time: 63556 ms

  • 將Long修改爲long,再來看一下運行結果
long t = System.currentTimeMillis();
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms");
  • 輸出結果:

total:2305843005992468481
processing time: 12229 ms

  • 事實證明,僅僅修改了一個字符,性能提高了不止一倍兩倍。那,就究竟是什麼原因導致的呢?
    因爲,+這個操作符不適用Integer對象,在進行數值相加操作之前會發生自動拆箱操作,轉換成int,相加之後還會發生自動拆箱操作,裝換成Integer對象。其內部變化如下:
sum = sum.longValue() + i;
Long sum = new Long(sum);
  • 很明顯,在上面的循環中會創建2147483647個”Long“類型實例,在這樣龐大的循環中,會降低程序的性能並且加重了垃圾回收的工作量

說明:包含在包裝器中的內容不會改變。即Long對象是不可變的

重載與自動裝箱

  • 在java 5之前,value(int)和value(Integer)是完全不相同的方法,開發者不會因爲傳入是int還是Integer調用哪個方法困惑,但是由於自動裝箱和拆箱的引入,處理重載方法時會不會有什麼變化呢?可通過下面一個例子進行探討:
public void test(int num){
    System.out.println("method with primitive argument");
}
public void test(Integer num){
    System.out.println("method with wrapper argument");
}
//calling overloaded method
AutoboxingTest autoTest = new AutoboxingTest();
int value = 3;
autoTest.test(value);  //no autoboxing 
Integer iValue = value;
autoTest.test(iValue); //no autoboxing
  • 輸出結果

method with primitive argument
method with wrapper argument

  • 從輸出結果可以看出,在重載的情況下,不會發生自動裝箱操作

注意事項

  • 自動裝箱與拆箱在編程過程中給我們帶來了極大的方便,但也存在一些容易讓人出錯的問題

對象相等比較

  • “==”既可用於原始值的比較,也可用於對象間的比較。當進行對象間的比較時,實質上比較的是對象的引用是否相等,而不是比較對象代表的值。如果要比較對象的值,應當使用對象對應的equals方法。可通過以下例子進行探討:
public class AutoboxingTest {
    public static void main(String args[]) {
        // Example 1: == comparison pure primitive – no autoboxing
        int i1 = 1;
        int i2 = 1;
        System.out.println("i1==i2 : " + (i1 == i2)); // true

        // Example 2: equality operator mixing object and primitive
        Integer num1 = 1; // autoboxing
        int num2 = 1;
        System.out.println("num1 == num2 : " + (num1 == num2)); // true

        // Example 3: special case - arises due to autoboxing in Java
        Integer obj1 = 1; // autoboxing will call Integer.valueOf()
        Integer obj2 = 1; // same call to Integer.valueOf() will return same cached Object
        System.out.println("obj1 == obj2 : " + (obj1 == obj2)); // true

        // Example 4: equality operator - pure object comparison
        Integer one = new Integer(1); // no autoboxing
        Integer anotherOne = new Integer(1);
        System.out.println("one == anotherOne : " + (one == anotherOne)); // false
    }
}
  • 輸出結果

i1==i2 : true
num1 == num2 : true
obj1 == obj2 : true
one == anotherOne : false

  • 值得注意的是,在Example 2中,比較是一個對象和一個原始值,出現這種情況比較的應該是對象的值

  • 讓人感到困惑的Example 3,在一開始我們說過,”==”用於對象間的比較時,比較的是它們的引用,那麼爲什麼obj1 == obj2返回的結果卻是true?這是一種極端情況,處於節省內存的考慮,JVM會緩存-128到127的Integer對象。也就是說,在創建obj1對象時,會進行自動裝箱操作,並且將其對象保存至緩存中,在創建obj2對象時,同樣會進行自動裝箱操作,然後在緩存中查找是否有相同值的對象,如果有,那麼obj2對象就會指向obj1對象。obj1和obj2實際上是同一個對象。所以使用”==”比較返回true

  • 而Example 4,是通過使用構造器來創建對象的,而沒有發生自動裝箱操作,不會執行緩存策略,故one和anotherOne是指向不同的引用的

 說明:這種 Integer 緩存策略僅在自動裝箱(autoboxing)的時候有用,使用構造器創建的 Integer 對象不能被緩存

容易混亂的對象和原始數據值

  • 一個很容易犯錯的問題,就是忽略對象與原始數據值之間的差異,在進行比較操作時,對象如果沒有初始化或者爲null,在自動拆箱過程中obj.xxxValue,則會拋出NullPointerException,可通過以下例子進行探討:
private static Integer count;
//NullPointerException on unboxing
if( count <= 0){
  System.out.println("Count is not started yet");
}

生成無用對象增加GC壓力

  • 因爲自動裝箱會隱式地創建對象,像前面提到的那樣,如果在一個循環體中,會創建無用的中間對象,這樣會增加GC壓力,拉低程序的性能。所以在寫循環時一定要注意代碼,避免引入不必要的自動裝箱操作

參考資料

發佈了67 篇原創文章 · 獲贊 26 · 訪問量 73萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章