1. 程式人生 > >自動裝箱與拆箱

自動裝箱與拆箱

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

雖然 Java 語言是典型的面向物件程式語言,但其中的八種基本資料型別並不支援面向物件程式設計,基本型別的資料不具備“物件”的特性——不攜帶屬性、沒有方法可呼叫。 沿用它們只是為了迎合人類根深蒂固的習慣,並的確能簡單、有效地進行常規資料處理。

這種藉助於非面向物件技術的做法有時也會帶來不便,比如引用型別資料均繼承了 Object 類的特性,要轉換為 String 型別(經常有這種需要)時只要簡單呼叫 Object 類中定義的toString()即可,而基本資料型別轉換為 String 型別則要麻煩得多。為解決此類問題 ,Java為每種基本資料型別分別設計了對應的類,稱之為包裝類(Wrapper Classes),也有教材稱為外覆類或資料型別類。

è¿éåå¾çæè¿°

基本資料型別及對應的包裝類

è¿éåå¾çæè¿°
每個包裝類的物件可以封裝一個相應的基本型別的資料,並提供了其它一些有用的方法。包裝類物件一經建立,其內容(所封裝的基本型別資料值)不可改變。

基本型別和對應的包裝類可以相互裝換:

  • 由基本型別向對應的包裝類轉換稱為裝箱,例如把 int 包裝成 Integer 類的物件;
  • 包裝類向對應的基本型別轉換稱為拆箱,例如把 Integer 類的物件重新簡化為 int。

包裝類的應用

八個包裝類的使用比較相似,下面是常見的應用場景。

1) 實現 int 和 Integer 的相互轉換

可以通過 Integer 類的構造方法將 int 裝箱,通過 Integer 類的 intValue 方法將 Integer 拆箱。例如:

	public class Demo {
	  public static void main(String[] args) {
		int m = 500;
		Integer obj = new Integer(m); // 手動裝箱
		int n = obj.intValue(); // 手動拆箱
		System.out.println("n = " + n);

		Integer obj1 = new Integer(500);
		System.out.println("obj 等價於 obj1?" + obj.equals(obj1));
	  }
	}

執行結果:
n = 500
obj 等價於 obj1?true

 


這個過程是自動執行的,那麼我們需要看看它的執行過程:

 public class Main {
     public static void main(String[] args) {
     //自動裝箱
     Integer total = 99;
 
     //自定拆箱
     int totalprim = total;
     }
 }
 

 

反編譯class檔案之後得到如下內容:

 1 javap -c StringTest 

這裡寫圖片描述

Integer total = 99; 
執行上面那句程式碼的時候,系統為我們執行了: 
Integer total = Integer.valueOf(99);

int totalprim = total; 
執行上面那句程式碼的時候,系統為我們執行了: 
int totalprim = total.intValue();

我們現在就以Integer為例,來分析一下它的原始碼: 
1、首先來看看Integer.valueOf函式

 public static Integer valueOf(int i) {
 return  i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128];
 }

它會首先判斷i的大小:如果i小於-128或者大於等於128,就建立一個Integer物件,否則執行SMALL_VALUES[i + 128]。

首先我們來看看Integer的建構函式:


 private final int value;
 
 public Integer(int value) {
     this.value = value;
 }
 
 public Integer(String string) throws NumberFormatException {
     this(parseInt(string));
 }

它裡面定義了一個value變數,建立一個Integer物件,就會給這個變數初始化。第二個傳入的是一個String變數,它會先把它轉換成一個int值,然後進行初始化。

下面看看SMALL_VALUES[i + 128]是什麼東西:

 1 private static final Integer[] SMALL_VALUES = new Integer[256]; 

它是一個靜態的Integer陣列物件,也就是說最終valueOf返回的都是一個Integer物件

所以我們這裡可以總結一點:裝箱的過程會建立對應的物件,這個會消耗記憶體,所以裝箱的過程會增加記憶體的消耗,影響效能。

( Java對於Integer與int的自動裝箱與拆箱的設計,是一種模式:叫享元模式(flyweight) ;為了加大對簡單數字的重利用,java定義:在自動裝箱時對於值從–128到127之間的值,它們被裝箱為Integer物件後,會存在記憶體中被重用,始終只存在一個物件 ;
而如果超過了從–128到127之間的值,被裝箱後的Integer物件並不會被重用,即相當於每次裝箱時都新建一個 Integer物件 )

2、接著看看intValue函式

 @Override
 public int intValue() {
     return value;
 }

這個很簡單,直接返回value值即可。

二、相關問題 
上面我們看到在Integer的建構函式中,它分兩種情況: 

1、i >= 128 || i < -128 =====> new Integer(i) 
2、i < 128 && i >= -128 =====> SMALL_VALUES[i + 128]

1 private static final Integer[] SMALL_VALUES = new Integer[256];

SMALL_VALUES本來已經被建立好,也就是說在i >= 128 || i < -128是會建立不同的物件,在i < 128 && i >= -128會根據i的值返回已經建立好的指定的物件。

說這些可能還不是很明白,下面我們來舉個例子吧:


 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);  //true
         System.out.println(i3==i4);  //false
     }
 }

程式碼的後面,我們可以看到它們的執行結果是不一樣的,為什麼,在看看我們上面的說明。 
1、i1和i2會進行自動裝箱,執行了valueOf函式,它們的值在(-128,128]這個範圍內,它們會拿到SMALL_VALUES數組裡面的同一個物件SMALL_VALUES[228],它們引用到了同一個Integer物件,所以它們肯定是相等的。

2、i3和i4也會進行自動裝箱,執行了valueOf函式,它們的值大於128,所以會執行new Integer(200),也就是說它們會分別建立兩個不同的物件,所以它們肯定不等。

下面我們來看看另外一個例子:


 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); //false
         System.out.println(i3==i4); //false
     }
 }

看看上面的執行結果,跟Integer不一樣,這樣也不必奇怪,因為它們的valueOf實現不一樣,結果肯定不一樣,那為什麼它們不統一一下呢? 
這個很好理解,因為對於Integer,在(-128,128]之間只有固定的256個值,所以為了避免多次建立物件,我們事先就建立好一個大小為256的Integer陣列SMALL_VALUES,所以如果值在這個範圍內,就可以直接返回我們事先建立好的物件就可以了。

但是對於Double型別來說,我們就不能這樣做,因為它在這個範圍內個數是無限的。 
總結一句就是:在某個範圍內的整型數值的個數是有限的,而浮點數卻不是。

所以在Double裡面的做法很直接,就是直接建立一個物件,所以每次建立的物件都不一樣。

 public static Double valueOf(double d) {
     return new Double(d);
 }

下面我們進行一個歸類: 
Integer派別:Integer、Short、Byte、Character、Long這幾個類的valueOf方法的實現是類似的。 
Double派別:Double、Float的valueOf方法的實現是類似的。每次都返回不同的物件。

下面對Integer派別進行一個總結,如下圖: 
這裡寫圖片描述

下面我們來看看另外一種情況:


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);//true
        System.out.println(i3==i4);//true
    }
}

可以看到返回的都是true,也就是它們執行valueOf返回的都是相同的物件。

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

可以看到它並沒有建立物件,因為在內部已經提前建立好兩個物件,因為它只有兩種情況,這樣也是為了避免重複建立太多的物件。

 public static final Boolean TRUE = new Boolean(true);
 
 public static final Boolean FALSE = new Boolean(false);

 

上面把幾種情況都介紹到了,下面來進一步討論其他情況。

 Integer num1 = 400;  
 int num2 = 400;  
 System.out.println(num1 == num2); //true
說明num1 == num2進行了拆箱操作
 Integer num1 = 100;  
 int num2 = 100;  
 System.out.println(num1.equals(num2));  //true

我們先來看看equals原始碼:

 @Override
 public boolean equals(Object o) {
     return (o instanceof Integer) && (((Integer) o).value == value);
 }

我們指定equal比較的是內容本身,並且我們也可以看到equal的引數是一個Object物件,我們傳入的是一個int型別,所以首先會進行裝箱,然後比較,之所以返回true,是由於它比較的是物件裡面的value值。

 Integer num1 = 100;  
 int num2 = 100;  
 Long num3 = 200l;  
 System.out.println(num1 + num2);  //200
 System.out.println(num3 == (num1 + num2));  //true
 System.out.println(num3.equals(num1 + num2));  //false

1、當一個基礎資料型別與封裝類進行==、+、-、*、/運算時,會將封裝類進行拆箱,對基礎資料型別進行運算。 
2、對於num3.equals(num1 + num2)為false的原因很簡單,我們還是根據程式碼實現來說明:

 @Override
 public boolean equals(Object o) {
     return (o instanceof Long) && (((Long) o).value == value);
 }

它必須滿足兩個條件才為true: 
1、型別相同 
2、內容相同 

上面返回false的原因就是型別不同。

 Integer num1 = 100;
 Ingeger num2 = 200;
 Long num3 = 300l;
 System.out.println(num3 == (num1 + num2)); //true

我們來反編譯一些這個class檔案:javap -c StringTest 
這裡寫圖片描述

可以看到運算的時候首先對num3進行拆箱(執行num3的longValue得到基礎型別為long的值300),然後對num1和mum2進行拆箱(分別執行了num1和num2的intValue得到基礎型別為int的值100和200),然後進行相關的基礎運算。

我們來對基礎型別進行一個測試:

 int num1 = 100;
 int num2 = 200;
 long mum3 = 300;
 System.out.println(num3 == (num1 + num2)); //true

就說明了為什麼最上面會返回true.

所以,當 “==”運算子的兩個運算元都是 包裝器型別的引用,則是比較指向的是否是同一個物件;

        而如果其中有一個運算元是表示式(即包含算術運算)則比較的是數值(即會觸發自動拆箱的過程)

陷阱1:

  Integer integer100=null;  
  int int100=integer100;

這兩行程式碼是完全合法的,完全能夠通過編譯的,但是在執行時,就會丟擲空指標異常。其中,integer100為Integer型別的物件,它當然可以指向null。但在第二行時,就會對integer100進行拆箱,也就是對一個null物件執行intValue()方法,當然會丟擲空指標異常。所以,有拆箱操作時一定要特別注意封裝類物件是否為null。

總結: 
1、需要知道什麼時候會引發裝箱和拆箱 
2、裝箱操作會建立物件,頻繁的裝箱操作會消耗許多記憶體,影響效能,所以可以避免裝箱的時候應該儘量避免。

3、equals(Object o) 因為原equals方法中的引數型別是封裝型別,所傳入的引數型別(a)是原始資料型別,所以會自動對其裝箱,反之,會對其進行拆箱。

4、當兩種不同型別用==比較時,包裝器類的需要拆箱, 當同種型別用==比較時,會自動拆箱或者裝箱