在本文中,筆者向大家介紹下Java中一個非常重要也非常有趣的特性,就是自動裝箱與拆箱,並從源碼中解讀自動裝箱與拆箱的原理,同時這種特性也留有一個陷阱。開發者如果不注意,就會很容易就跌入這個陷阱。
自動裝箱(Autoboxing)
定義
大家在平時編寫Java程式時,都常常以以下方式來定義一個Integer物件:
Integer i=100;
從上面的程式碼中,大家可以得得知,i為一個Integer類型的引用,100為Java中的基礎資料型別(primitive data type)。而這種直接將一個基礎資料型別傳給其對應的封裝類別(wrapper class)的做法,便是自動裝箱(Autoboxing)。
在jdk 1.5中,自動裝箱首次引入。而在jdk 1.5之前,如果你想要定義一個value為100的Integer對象,則需要這樣做:
Integer i=new Integer (100);
原理
我們在上述程式碼「Integer i=100; 「處打一個斷點,跟蹤一下。
接下來,我們可以看到,程式跳到了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); }
換句話說,裝箱就是jdk自己幫你完成了調用Integer.valueOf 100)。
拆箱(Unboxing)
定義
Integer integer100=100; int int100=integer100;
從上面的程式碼中,大家可看出integer100為一個Integer類型的引用,int100為一個int型別的原始資料型別。但是,我們可以將一個Integer類型的物件賦值給其對應原始資料類型的變數。這便是拆箱。
拆箱與裝箱是相反的操作。裝箱是將一個原始資料類型賦值給對應封裝類別的變數。而拆箱則是將一個封裝類別的變數賦值給對應原始資料型別的變數。裝箱、拆箱的名字也取得相當貼切。
原理
筆者相信大家也都猜到了,拆箱過程中jdk為我們做了什麼。我們還是透過實驗來證明我們的猜想吧。
在上述程式碼的第二行程式碼打上斷點,也就是在「int int100=integer100;」上打上斷點,追蹤一下。
我們可以看到,程式跳到了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;
程序运行的结果为:
200
true
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);
因为i1、i2、i3、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得到是相同的对象。
对比扩展中的第二个实验,我们得知,当封装类与基础类型进行==运行时,封装类会进行拆箱,拆箱结果与基础类型对比值;而两个封装类进行==运行时,与其它的对象进行==运行一样,对比两个对象的地址,也即判断是否两个引用是否指向同一个对象。
以上这篇淺談Java自動裝箱與拆箱及其陷阱就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持PHP中文网。
更多淺談Java自動裝箱與拆箱及其陷阱相关文章请关注PHP中文网!