標籤:lan ref min tutorial 概述 sse 引用 說明 unbox
概述
自JDK1.5開始, 引入了自動裝箱/拆箱這一文法糖, 它使程式員的代碼變得更加簡潔, 不再需要進行顯式轉換。基本類型與封裝類型在某些操作符的作用下, 封裝類型調用valueOf()方法將原始類型值轉換成對應的封裝類對象的過程, 稱之為自動裝箱; 反之調用xxxValue()方法將封裝類對象轉換成原始類型值的過程, 則稱之為自動拆箱。
實現原理
首先我們用javap -c AutoBoxingDemo命令將下面代碼反編譯:
public class AutoBoxingDemo { public static void main(String[] args) { Integer m = 1; int n = m; }}
反編譯後結果:
從反編譯後的位元組碼指令中可以看出, Integer m = 1; 其實底層就是調用了封裝類Integer的valueOf()方法進行自動裝箱, 而 int n = m; 則是底層調用了封裝類的intValue()方法進行自動拆箱。
其中Byte、Short、Integer、Long、Boolean、Character這六種封裝類型在進行自動裝箱時都使用了緩衝策略, 下面是Integer類的緩衝實現機制:
/** * This method will always cache values in the range -128 to 127, * inclusive, and may cache other values outside of this range. */public static Integer valueOf(int i) { assert IntegerCache.high >= 127; if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i);}private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); } private IntegerCache() {}}
從Integer的原始碼我們能得知, 當進行自動裝箱的數值在[-128, 127]之間時, 調用valueOf()方法返回的是Integer緩衝中已存在的對象引用。否則每次都是new一個新的封裝類執行個體。
而Double、Float這兩種封裝類型因為是浮點數, 不像整數那樣在某個範圍內的數值個數是有限的, 所以它們沒有使用緩衝實現機制, 下面是Double封裝類的自動裝箱的原始碼:
public static Double valueOf(double d) { return new Double(d);}
舉例說明
public class AutoBoxingDemo { public static void main(String[] args) { Integer a = 1; Integer b = 2; Integer c = 3; Integer d = 3; Integer e = 321; Integer f = 321; Long g = 3L; Long h = 2L; Double i = 1.0; Double j = 1.0; Boolean k = true; Boolean l = true;
//數值在[-128, 127]範圍內,自動裝箱時都是從緩衝中擷取對象引用,所以結果為true System.out.println(c==d); //數值在[-128, 127]範圍外,自動裝箱時每次都是new新的對象,所以結果為false System.out.println(e==f); //當"=="運算子的兩個運算元都是封裝器類型的引用,則比較指向的是否是同一個對象,而如果其中有一個運算元是運算式(即包含算術運算)則比較的是數值(即會觸發自動拆箱的過程), 所以結果為true System.out.println(c==(a+b)); //對於封裝類型,當equals()方法比較的是同一類型時(比如Integer與Integer比較),實際比較的是他們的數值是否相等。如比較的不是同一類型,則不會進行類型轉換,直接返回false。所以結果為true System.out.println(c.equals(a+b)); //因為有算術運算,自動拆箱後再比較數值,所以結果為true System.out.println(g==(a+b)); //因為equals()方法比較的是不同封裝類型,不會進行類型轉換,所以結果為false System.out.println(g.equals(a+b)); //因為a+h先觸發自動拆箱,a轉為int類型後,需要隱式向上提升類型為long後再進行運算,最後再自動裝箱轉為Long封裝類型,且兩邊數值相等,所以結果為true System.out.println(g.equals(a+h)); //Double類沒有緩衝,每次都是new一個新的執行個體,所以結果為false System.out.println(i == j); //Boolean自動裝箱,指向的都是同一個執行個體,所以結果為true System.out.println(k == l); }}
在上面樣本中, 關於結果的解析已經闡述的很清楚了, 主要有兩個地方具有迷惑性。當"=="運算子的兩個運算元都是封裝器類型的引用,則比較指向的是否是同一個對象,而如果其中有一個運算元是運算式(即包含算術運算)則比較的是數值(即會先觸發自動拆箱的過程)。
對於封裝類型,當equals()方法比較的是同一類型時(比如Integer與Integer比較), 實際比較的是他們的數值是否相等; 如比較的不是同一類型(比如Integer與Long比較), 則不會進行類型轉換,直接返回false。下面是Integer類的equals()方法的原始碼:
public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false;}
另外我們也可以反編譯以上代碼, 穿透文法糖的糖衣能協助我們更容易瞭解這些具有迷惑性現象的背後原理:
自動裝箱/拆箱帶來的問題
自動拆箱下算術運算引起的null 指標問題
private Double distinct;private void setParam(Double dSrc, boolean flag) { this.distinct = (flag) ? dSrc : 0d;}
上面這段代碼乍一看是沒問題的, 但實際當dSrc為null時, 調用該方法會拋出null 指標異常, 我們對其進行反編譯:
可以看出, 當對封裝類進行諸如三目運算子的算術運算時, 當資料類型不一致時, 編譯器會自動拆箱轉換為基本類型再進行運算, 所以當dSrc傳入null值時, 調用doubleValue()方法拆箱就會報NPnull 指標異常。
這裡我們可以在進行算術運算時, 統一資料類型, 避免編譯器進行自動拆箱, 來解決拆箱下三目運算子的null 指標問題。還是上面這個栗子, 我們將 this.distinct = (flag) ? dSrc : 0d; 修改成 this.distinct = (flag) ? dSrc : Double.valueOf(0); 即可解決, 重新反編譯後如下, 因為類型一致, 沒有再進行自動拆箱:
自動裝箱的弊端
Integer sum = 0; for(int i=1000; i<10000; i++){ sum+=i;}
如上代碼, 當在迴圈中對封裝類型進行算術運算 sum = sum + i; 時, 會先觸發自動拆箱, 進行加法運算後, 再進行自動裝箱, 且因為運算後的sum數值不在緩衝範圍之內, 所以每次都會new一個新的Integer執行個體。所以上面的迴圈結束後, 將會在記憶體中建立9000個無用的Integer執行個體對象, 這樣會大大降低程式的效能, 增加GC的開銷, 所以我們在寫迴圈語句時一定要正確的聲明變數類型, 避免因為自動裝箱而引起不必要的效能問題。
重載與自動裝箱
在JDK1.5之前, 沒有引入自動裝箱/拆箱這一文法糖, 當方法重載時, test(int num) 與 test(Integer num) 的形參沒有任何關係。JDK1.5之後, 當調用重載的方法時, 編譯器不會進行自動裝箱操作, 我們可以通過運行下面的程式碼範例來示範。
public static void testAutoBoxing(int num) { System.out.println("方法形參為原始類型");}public static void testAutoBoxing(Integer num) { System.out.println("方法形參為封裝類型");}public static void main(String[] args) { int m = 2; testAutoBoxing(m); Integer n = m; testAutoBoxing(n);}
運行結果如下:
很明顯, 當調用重載的方法時, 編譯器不會對傳入的實參進行自動裝箱操作。
參考資料
Autoboxing and Unboxing (The Java Tutorials > Lea...
深入剖析Java中的裝箱和拆箱 - 海 子 - 部落格園
Java 自動裝箱與拆箱的實現原理 - 簡書
Java自動拆箱下, 三目運算子的潛規則
Java中的自動裝箱與拆箱
Java的自動裝箱/拆箱