Java包裝類、拆箱和裝箱詳解

簡述

雖然Java語言是面向對象編程語言,但其中的八種基本數據類型並不支持面向對象編程,基本類型的數據不具有 「對象」的特性---不攜帶屬性、沒有方法可調用。java

Java爲每種基本數據類型分別設計了對於的類,稱之爲包裝類。基本數據類型及對應的包裝類面試

輸入圖片說明

每一個包裝類的對象能夠封裝一個相應的基本類型的數據,並提供了其它一些有用的方法。包裝類對象一經建立,其內容(所封裝的基本類型數據值)不可改變。編程

基本類型和對應的包裝類能夠相互裝換:緩存

  • 由基本類型向對應的包裝類轉換稱爲裝箱,例如把 int 包裝成 Integer 類的對象;
  • 包裝類向對應的基本類型轉換稱爲拆箱,例如把 Integer 類的對象從新簡化爲 int。

什麼是自動裝箱和拆箱

自動裝箱就是Java自動將原始類型值轉換成對應的對象,好比將int的變量轉換成Integer對象,這個過程叫作裝箱,反之將Integer對象轉換成int類型值,這個過程叫作拆箱。由於這裏的裝箱和拆箱是自動進行的非人爲轉換,因此就稱做爲自動裝箱和拆箱。原始類型byte,short,char,int,long,float,double和boolean對應的封裝類爲Byte,Short,Character,Integer,Long,Float,Double,Boolean。app

自動裝箱拆箱要點

  • 自動裝箱時編譯器調用valueOf將原始類型值轉換成對象,同時自動拆箱時,編譯器經過調用相似intValue(),doubleValue()這類的方法將對象轉換成原始類型值。
  • 自動裝箱是將boolean值轉換成Boolean對象,byte值轉換成Byte對象,char轉換成Character對象,float值轉換成Float對象,int轉換成Integer,long轉換成Long,short轉換成Short,自動拆箱則是相反的操做。

什麼時候發生自動裝箱和拆箱

自動裝箱和拆箱在Java中很常見,好比咱們有一個方法,接受一個對象類型的參數,若是咱們傳遞一個原始類型值,那麼Java會自動講這個原始類型值轉換成與之對應的對象。最經典的一個場景就是當咱們向ArrayList這樣的容器中增長原始類型數據時或者是建立一個參數化的類,好比下面的ThreadLocal。編程語言

ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(1); //autoboxing - primitive to object
intList.add(2); //autoboxing
 
ThreadLocal<Integer> intLocal = new ThreadLocal<Integer>();
intLocal.set(4); //autoboxing
 
int number = intList.get(0); // unboxing
int local = intLocal.get(); // unboxing in Java

舉例說明

賦值時

這是最多見的一種狀況,在Java 1.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

方法調用時

這是另外一個經常使用的狀況,當咱們在方法調用時,咱們能夠傳入原始數據值或者對象,一樣編譯器會幫咱們進行轉換。ui

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

show方法接受Integer對象做爲參數,當調用show(3)時,會將int值轉換成對應的Integer對象,這就是所謂的自動裝箱,show方法返回Integer對象,而int result = show(3);中result爲int類型,因此這時候發生自動拆箱操做,將show方法的返回的Integer對象轉換成int值。設計

自動裝箱的弊端

自動裝箱有一個問題,那就是在一個循環中進行自動裝箱操做的狀況,以下面的例子就會建立多餘的對象,影響程序的性能。code

Integer sum = 0;
 for(int i=1000; i<5000; i++){
   sum+=i;
}

上面的代碼sum+=i能夠當作sum = sum + i,可是+這個操做符不適用於Integer對象,首先sum進行自動拆箱操做,進行數值相加操做,最後發生自動裝箱操做轉換成Integer對象。其內部變化以下

sum = sum.intValue() + i;
Integer sum = new Integer(result);

因爲咱們這裏聲明的sum爲Integer類型,在上面的循環中會建立將近4000個無用的Integer對象,在這樣龐大的循環中,會下降程序的性能而且加劇了垃圾回收的工做量。所以在咱們編程時,須要注意到這一點,正確地聲明變量類型,避免由於自動裝箱引發的性能問題。

重載與自動裝箱

當重載趕上自動裝箱時,狀況會比較有些複雜,可能會讓人產生有些困惑。在1.5以前,value(int)和value(Integer)是徹底不相同的方法,開發者不會由於傳入是int仍是Integer調用哪一個方法困惑,可是因爲自動裝箱和拆箱的引入,處理重載方法時稍微有點複雜。一個典型的例子就是ArrayList的remove方法,它有remove(index)和remove(Object)兩種重載,咱們可能會有一點小小的困惑,其實這種困惑是能夠驗證並解開的,經過下面的例子咱們能夠看到,當出現這種狀況時,不會發生自動裝箱操做。

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
 
Output:
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
 
    }
 
}
 
Output:
i1==i2 : true
num1 == num2 : true
obj1 == obj2 : true
one == anotherOne : false

值得注意的是第三個小例子,這是一種極端狀況。obj1和obj2的初始化都發生了自動裝箱操做。可是處於節省內存的考慮,JVM會緩存-128到127的Integer對象。由於obj1和obj2其實是同一個對象。因此使用」==「比較返回true。

生成無用對象增長GC壓力

由於自動裝箱會隱式地建立對象,像前面提到的那樣,若是在一個循環體中,會建立無用的中間對象,這樣會增長GC壓力,拉低程序的性能。因此在寫循環時必定要注意代碼,避免引入沒必要要的自動裝箱操做。

三.面試中相關的問題

雖然大多數人對裝箱和拆箱的概念都清楚,可是在面試和筆試中遇到了與裝箱和拆箱的問題卻不必定會答得上來。下面列舉一些常見的與裝箱/拆箱有關的面試題。

1.下面這段代碼的輸出結果是什麼?

public class Main {
    public static void main(String[] args) {
 
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
 
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

也許有些朋友會說都會輸出false,或者也有朋友會說都會輸出true。可是事實上輸出結果是:

true
false

爲何會出現這樣的結果?輸出結果代表i1和i2指向的是同一個對象,而i3和i4指向的是不一樣的對象。此時只需一看源碼便知究竟,下面這段代碼是Integer的valueOf方法的具體實現:

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

而其中IntegerCache類的實現爲:

private static class IntegerCache {
        static final int high;
        static final Integer cache[];
 
        static {
            final int low = -128;
 
            // high value may be configured by property
            int h = 127;
            if (integerCacheHighPropValue != null) {
                // Use Long.decode here to avoid invoking methods that
                // require Integer's autoboxing cache to be initialized
                int i = Long.decode(integerCacheHighPropValue).intValue();
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - -low);
            }
            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() {}
    }

從這2段代碼能夠看出,在經過valueOf方法建立Integer對象的時候,若是數值在[-128,127]之間,便返回指向IntegerCache.cache中已經存在的對象的引用;不然建立一個新的Integer對象。

上面的代碼中i1和i2的數值爲100,所以會直接從cache中取已經存在的對象,因此i1和i2指向的是同一個對象,而i3和i4則是分別指向不一樣的對象。

2.下面這段代碼的輸出結果是什麼?

public class Main {
    public static void main(String[] args) {
 
        Double i1 = 100.0;
        Double i2 = 100.0;
        Double i3 = 200.0;
        Double i4 = 200.0;
 
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

也許有的朋友會認爲跟上面一道題目的輸出結果相同,可是事實上卻不是。實際輸出結果爲:

false
false

至於具體爲何,讀者能夠去查看Double類的valueOf的實現。

在這裏只解釋一下爲何Double類的valueOf方法會採用與Integer類的valueOf方法不一樣的實現。很簡單:在某個範圍內的整型數值的個數是有限的,而浮點數卻不是。

注意,Integer、Short、Byte、Character、Long這幾個類的valueOf方法的實現是相似的。

Double、Float的valueOf方法的實現是相似的。

3.下面這段代碼輸出結果是什麼:

public class Main {
    public static void main(String[] args) {
 
        Boolean i1 = false;
        Boolean i2 = false;
        Boolean i3 = true;
        Boolean i4 = true;
 
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

輸出結果是:

true
true

至於爲何是這個結果,一樣地,看了Boolean類的源碼也會一目瞭然。下面是Boolean的valueOf方法的具體實現:

public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

而其中的 TRUE 和FALSE又是什麼呢?在Boolean中定義了2個靜態成員屬性:

public static final Boolean TRUE = new Boolean(true);
 
    /** 
     * The <code>Boolean</code> object corresponding to the primitive 
     * value <code>false</code>. 
     */
    public static final Boolean FALSE = new Boolean(false);

至此,你們應該明白了爲什麼上面輸出的結果都是true了。

4.談談Integer i = new Integer(xxx)和Integer i =xxx;這兩種方式的區別。

固然,這個題目屬於比較寬泛類型的。可是要點必定要答上,我總結一下主要有如下這兩點區別:

1)第一種方式不會觸發自動裝箱的過程;而第二種方式會觸發;

2)在執行效率和資源佔用上的區別。第二種方式的執行效率和資源佔用在通常性狀況下要優於第一種狀況(注意這並非絕對的)。

5.下面程序的輸出結果是什麼?

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;
  
// 會自動拆箱(會調用intValue方法)
System.out.println(c==d);
// 會自動拆箱後再自動裝箱
System.out.println(e==f);
// 雖然「==」比較的是引用的是不是同一對象,但這裏有算術運算,若是該引用爲包裝器類型則會致使自動拆箱
System.out.println(c==(a+b));
// equals 比較的是引用的對象的內容(值)是否相等,但這裏有算術運算,若是該引用爲包裝器類型則會導
 // 致自動拆箱,再自動裝箱
// a+b觸發自動拆箱獲得值後,再自動裝箱與c比較
System.out.println(c.equals(a+b));
// 首先a+b觸發自動拆箱後值爲int型,因此比較的是值是否相等
System.out.println(g==(a+b));
// 首先a+b觸發自動拆箱後值爲int型,自動裝箱後爲Integer型,而後g爲Long型
System.out.println(g.equals(a+b));
// 首先a+h觸發自動拆箱後值爲long型,由於int型的a會自動轉型爲long型的g而後自動裝箱後爲Long型,
 // 而g也爲Long型
System.out.println(g.equals(a+h));
  
}

先別看輸出結果,讀者本身想一下這段代碼的輸出結果是什麼。這裏面須要注意的是:當 「==」運算符的兩個操做數都是 包裝器類型的引用,則是比較指向的是不是同一個對象,而若是其中有一個操做數是表達式(即包含算術運算)則比較的是數值(即會觸發自動拆箱的過程)。另外,對於包裝器類型,equals方法並不會進行類型轉換。明白了這2點以後,上面的輸出結果便一目瞭然:

true
false
true
true
true
false
true

第一個和第二個輸出結果沒有什麼疑問。第三句因爲 a+b包含了算術運算,所以會觸發自動拆箱過程(會調用intValue方法),所以它們比較的是數值是否相等。而對於c.equals(a+b)會先觸發自動拆箱過程,再觸發自動裝箱過程,也就是說a+b,會先各自調用intValue方法,獲得了加法運算後的數值以後,便調用Integer.valueOf方法,再進行equals比較。同理對於後面的也是這樣,不過要注意倒數第二個和最後一個輸出的結果(若是數值是int類型的,裝箱過程調用的是Integer.valueOf;若是是long類型的,裝箱調用的Long.valueOf方法)。

System.out.println(g==(a+b));的分析:a+b 拆箱,(a+b) 會在裝箱。Long 是裝箱對象,此時會指向同一個地址。這次的值是小於128的。

注意

這裏面須要注意的是:當 「==」運算符的兩個操做數都是包裝器類型的引用,則是比較指向的是不是同一個對象,而若是其中有一個操做數是表達式(即包含算術運算)則比較的是數值(即會觸發自動拆箱的過程)另外,對於包裝器類型,equals方法並不會進行類型轉換。

相關文章
相關標籤/搜索