Java的自動裝箱/拆箱

概述

自JDK1.5開始, 引入了自動裝箱/拆箱這一語法糖, 它使程序員的代碼變得更加簡潔, 再也不須要進行顯式轉換。基本類型與包裝類型在某些操做符的做用下, 包裝類型調用valueOf()方法將原始類型值轉換成對應的包裝類對象的過程, 稱之爲自動裝箱; 反之調用xxxValue()方法將包裝類對象轉換成原始類型值的過程, 則稱之爲自動拆箱。html

實現原理

首先咱們用javap -c AutoBoxingDemo命令將下面代碼反編譯: java

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類的緩存實現機制: oracle

/**
 * 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一個新的包裝類實例。ide

而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); } }

在上面示例中, 關於結果的解析已經闡述的很清楚了, 主要有兩個地方具備迷惑性。當"=="運算符的兩個操做數都是包裝器類型的引用,則比較指向的是不是同一個對象,而若是其中有一個操做數是表達式(即包含算術運算)則比較的是數值(即會先觸發自動拆箱的過程)。this

對於包裝類型,當equals()方法比較的是同一類型時(好比Integer與Integer比較), 實際比較的是他們的數值是否相等; 如比較的不是同一類型(好比Integer與Long比較), 則不會進行類型轉換,直接返回false。下面是Integer類的equals()方法的源代碼: spa

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

另外咱們也能夠反編譯以上代碼, 穿透語法糖的糖衣能幫助咱們更容易瞭解這些具備迷惑性現象的背後原理: .net

自動裝箱/拆箱帶來的問題

自動拆箱下算術運算引發的空指針問題

private Double distinct;
private void setParam(Double dSrc, boolean flag) {
    this.distinct = (flag) ? dSrc : 0d;
}

上面這段代碼乍一看是沒問題的, 但實際當dSrc爲null時, 調用該方法會拋出空指針異常, 咱們對其進行反編譯:

能夠看出, 當對包裝類進行諸如三目運算符的算術運算時, 當數據類型不一致時, 編譯器會自動拆箱轉換爲基本類型再進行運算, 因此當dSrc傳入null值時, 調用doubleValue()方法拆箱就會報NP空指針異常。

這裏咱們能夠在進行算術運算時, 統一數據類型, 避免編譯器進行自動拆箱, 來解決拆箱下三目運算符的空指針問題。仍是上面這個栗子, 咱們將 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中的自動裝箱與拆箱

做者:張小凡
出處:https://www.cnblogs.com/qingshanli/ 本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。若是以爲還有幫助的話,能夠點一下右下角的【推薦】。

相關文章
相關標籤/搜索