在本文中,筆者向你們介紹下Java中一個很是重要也很是有趣的特性,就是自動裝箱與拆箱,並從源碼中解讀自動裝箱與拆箱的原理,同時這種特性也留有一個陷阱。開發者若是不注意,就會很容易跌入這個陷阱。 java
自動裝箱(Autoboxing)
定義
你們在平時編寫Java程序時,都經常以如下方式來定義一個Integer對象: 數組
從上面的代碼中,你們能夠得知,i爲一個Integer類型的引用,100爲Java中的基礎數據類型(primitive data type)。而這種直接將一個基礎數據類型傳給其相應的封裝類(wrapper class)的作法,即是自動裝箱(Autoboxing)。 app
在jdk 1.5中,自動裝箱首次被引入。而在jdk 1.5以前,若是你想要定義一個value爲100的Integer對象,則須要這樣作: 性能
- Integer i=new Integer (100);
原理
咱們在以上代碼「Integer i=100;」處打一個斷點,跟蹤一下。
接下來,咱們能夠看到,程序跳轉到了Integer類的valueOf(int i)方法中 ui
- /**
- * Returns a <tt>Integer</tt> instance representing the specified
- * <tt>int</tt> value.
- * If a new <tt>Integer</tt> instance is not required, this method
- * should generally be used in preference to the constructor
- * {@link #Integer(int)}, as this method is likely to yield
- * significantly better space and time performance by caching
- * frequently requested values.
- *
- * @param i an <code>int</code> value.
- * @return a <tt>Integer</tt> instance representing <tt>i</tt>.
- * @since 1.5
- */
- public static Integer valueOf(int i) {
- if(i >= -128 && i <= IntegerCache.high)
- return IntegerCache.cache[i + 128];
- else
- return new Integer(i);
- }
換句話說,裝箱就是jdk本身幫你完成了調用Integer.valueOf(100)。 this
拆箱(Unboxing)
定義
- Integer integer100=100;
- int int100=integer100;
從上面的代碼中,你們可看出integer100爲一個Integer類型的引用,int100爲一個int類型的原始數據類型。可是,咱們能夠將一個Integer類型的對象賦值給其相應原始數據類型的變量。這即是拆箱。 spa
拆箱與裝箱是相反的操做。裝箱是將一個原始數據類型賦值給相應封裝類的變量。而拆箱則是將一個封裝類的變量賦值給相應原始數據類型的變量。裝箱、拆箱的名字也取得至關貼切。 .net
原理
筆者相信你們也都猜到了,拆箱過程當中jdk爲咱們作了什麼。咱們仍是經過實驗來證實咱們的猜測吧。 指針
在以上代碼的第二行代碼打上斷點,即在「int int100=integer100;」上打上斷點,跟蹤一下。 code
咱們能夠看到,程序跳轉到了Integer的intValue()方法。
- /**
- * Returns the value of this <code>Integer</code> as an
- * <code>int</code>.
- */
- public int intValue() {
- return value;
- }
也就是,jdk幫咱們完成了對intValue()方法的調用。對於以上的實驗而言,即是調用integer100的intValue()方法,將其返回值賦給了int100。
擴展
實驗1
- Integer integer400=400;
- int int400=400;
- System.out.println(integer400==int400);
在以上代碼的第三行中,integer400與int400執行了==運行。而這兩個是不一樣類型的變量,究竟是integer400拆箱了,仍是int400裝箱了呢?運行結果是什麼呢?
==運算是判斷兩個對象的地址是否相等或者判斷兩個基礎數據類型的值是否相等。因此,你們很容易推測到,若是integer400拆箱了,則說明對比的是兩個基礎類型的值,那此時必然相等,運行結果爲true;若是int400裝箱了,則說明對比的是兩個對象的地址是否相等,那此時地址必然不相等,運行結果爲false。(至於爲何筆者對它們賦值爲400,就是後面將要講到的陷阱有關)。
咱們實際的運行結果爲true。因此是integer400拆箱了。對代碼跟蹤的結果也證實這一點。
實驗2
- Integer integer100=100;
- int int100=100;
- System.out.println(integer100.equals(int100));
在以上代碼的第三行中,integer100的方法equals的參數爲int100。咱們知道equals方法的參數爲Object,而不是基礎數據類型,於是在這裏必然是int100裝箱了。對代碼跟蹤的結果也證實了這一點。
其實,若是一個方法中參數類型爲原始數據類型,所傳入的參數類型爲其封裝類,則會自動對其進行拆箱;相應地,若是一個方法中參數類型爲封裝類型,所傳入的參數類型爲其原始數據類型,則會自動對其進行裝箱。
實驗3
- Integer integer100 = 100;
- int int100 = 100;
- Long long200 = 200l;
- System.out.println(integer100 + int100);
- System.out.println(long200 == (integer100 + int100));
- System.out.println(long200.equals(integer100 + int100));
在第一個實驗中,咱們已經得知,當一個基礎數據類型與封裝類進行==運算時,會將封裝類進行拆箱。那若是+、-、*、/呢?咱們在這個實驗中,就可知道。
若是+運算,會將基礎數據類型裝箱,那麼:
- 第4行中,integer100+int100就會獲得一個類型爲Integer且value爲200的對象o,並執行這個對象的toString()方法,並輸出」200」;
- 第5行中,integer100+int100就會獲得一個類型爲Integer且value爲200的對象o,==運算將這個對象與long200對象進行對比,顯然,將會輸出false;
- 第6行中,integer100+int100就會獲得一個類型爲Integer且value爲200的對象o,Long的equals方法將long200與o對比,由於兩都是不一樣類型的封裝類,於是輸出false;
若是+運算,會將封裝類進行拆箱,那麼:
- 第4行中,integer100+int100就會獲得一個類型爲int且value爲200的基礎數據類型b,再將b進行裝箱獲得o,執行這個對象的toString()方法,並輸出」200」;
- 第5行中,integer100+int100就會獲得一個類型爲int且value爲200的基礎數據類型b1,==運算將long200進行拆箱獲得b2,顯然b1==b2,輸出true;
- 第6行中,integer100+int100就會獲得一個類型爲int且value爲200的基礎數據類型b,Long的equals方法將b進行裝箱,但裝箱所獲得的是類型爲Integer的對象o,由於o與long200爲不一樣的類型的對象,因此輸出false;
程序運行的結果爲:
於是,第二種推測是正確,即在+運算時,會將封裝類進行拆箱。
陷阱
陷阱1
- Integer integer100=null;
- int int100=integer100;
這兩行代碼是徹底合法的,徹底可以經過編譯的,可是在運行時,就會拋出空指針異常。其中,integer100爲Integer類型的對象,它固然能夠指向null。但在第二行時,就會對integer100進行拆箱,也就是對一個null對象執行intValue()方法,固然會拋出空指針異常。因此,有拆箱操做時必定要特別注意封裝類對象是否爲null。
陷阱2
- Integer i1=100;
- Integer i2=100;
- Integer i3=300;
- Integer i4=300;
- System.out.println(i1==i2);
- System.out.println(i3==i4);
由於i一、i二、i三、i4都是Integer類型的,因此咱們想,運行結果應該都是false。可是,真實的運行結果爲「System.out.println(i1==i2);」爲 true,可是「System.out.println(i3==i4);」爲false。也就意味着,i1與i2這兩個Integer類型的引用指向了同一個對象,而i3與i4指向了不一樣的對象。爲何呢?不都是調用Integer.valueOf(int i)方法嗎?
讓咱們再看看Integer.valueOf(int i)方法。
- /**
- * Returns a <tt>Integer</tt> instance representing the specified
- * <tt>int</tt> value.
- * If a new <tt>Integer</tt> instance is not required, this method
- * should generally be used in preference to the constructor
- * {@link #Integer(int)}, as this method is likely to yield
- * significantly better space and time performance by caching
- * frequently requested values.
- *
- * @param i an <code>int</code> value.
- * @return a <tt>Integer</tt> instance representing <tt>i</tt>.
- * @since 1.5
- */
- public static Integer valueOf(int i) {
- if(i >= -128 && i <= IntegerCache.high)
- return IntegerCache.cache[i + 128];
- else
- return new Integer(i);
- }
咱們能夠看到當i>=-128且i<=IntegerCache.high時,直接返回IntegerCache.cache[i + 128]。其中,IntegerCache爲Integer的內部靜態類,其原碼以下:
- 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() {}
- }
咱們能夠清楚地看到,IntegerCache有靜態成員變量cache,爲一個擁有256個元素的數組。在IntegerCache中也對cache進行了初始化,即第i個元素是值爲i-128的Integer對象。而-128至127是最經常使用的Integer對象,這樣的作法也在很大程度上提升了性能。也正由於如此,「Integeri1=100;Integer i2=100;」,i1與i2獲得是相同的對象。
對比擴展中的第二個實驗,咱們得知,當封裝類與基礎類型進行==運行時,封裝類會進行拆箱,拆箱結果與基礎類型對比值;而兩個封裝類進行==運行時,與其它的對象進行==運行同樣,對比兩個對象的地址,也即判斷是否兩個引用是否指向同一個對象。