什么的拆裝箱
我第一次聽到這個概念是一臉懵逼的,而其實最常使用的地方就是我們熟悉的包裝類的使用中.比如將int的變量轉換成Integer對象,這個過程叫做裝箱,反之將Integer對象轉換成int類型值,這個過程叫做拆箱。因為這里的裝箱和拆箱是自動進行的非人為轉換,所以就稱作為自動裝箱和拆箱。
需要明確的是自動拆裝箱是在JDK1.5以后引入的,對于之前版本的Java,需要格外注意格式的轉換。
為何需要自動裝箱和拆箱?
方便
首先就是方便程序員的編碼,我們在編碼過程中,可以不需要考慮包裝類和基本類型之間的轉換操作,這一步由編譯器自動替我們完成,開發人員可以有更多的精力集中與具體的業務邏輯。否則的話,一個簡單的數字賦值給包裝類就得寫兩句代碼,即:首先生成包裝類型對象,然后將對象轉換成基本數據類型。而這種操作是代碼中使用頻率很高的操作,導致代碼書寫量增多。
節約空間
我們在查閱對應包裝類的源代碼時可以看到,大部分包裝類型的valueOf方法都會有緩存的操作,即:將某段范圍內的數據緩存起來,創建時如果在這段范圍內,直接返回已經緩存的這些對象,這樣保證在一定范圍內的數據可以直接復用,而不必要重新生成。
這么設計的目的因為:小數字的使用頻率很高,將小數字緩存起來,讓其僅有一個對象,可以起到節約存儲空間的作用。這里其實采用的是一種叫做享元模式的設計模式。可以去具體了解以下這種設計模式,這里就不再過多贅述。
實現原理
Java中是怎么實現這個自動裝箱和拆箱的過程的呢?這里需要借助與一些反編譯工具,例如javap命令或者其他一些反編譯的工具,我這里使用的是idea的bytecode插件,如果需要,可以到這里下載。在它的release中直接下載zip壓縮包就行,然后作為插件安裝在idea中就行,安裝完成重啟idea后,在需要反編譯的java代碼中右鍵,可以找到"Show Bytecode outline-dev"菜單選項,直接點擊就可以看到反編譯后的代碼。
裝箱
首先看下面兩句代碼:
Integer i = 20;
int j = 2;
在進行反編譯之后可以得到:
Integer i = Integer.valueOf((int)10);
int j = 2;
可以看到對于數值類型直接賦值給包裝類型,有一個自動裝箱的操作,而自動裝箱的操作就是利用了Integer中的valueOf方法,這就是前面在節約空間那部分提到的valueOf方法。Integer的valueOf方法中具有緩存的功能,也就是說在數值為-128到127之間的數據,都是被構造成同一個對象,這就是上面提到的享元模式的設計思路:
public static Integer valueOf(int i) {
//IntegerCache.low = -128, IntegerCache.high = 127
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
這個概念在刷面試題的時候,都被強調爛了,基本常見的筆試題目就是比較幾個integer對象之間==操作:
Integer a = 100;
Integer b = 100;
Integer c = 128;
Integer d = 128;
System.out.println(a == b); //true
System.out.println(c == d); //false
注意:也可以使用new Integer(num)的方式創建Integer對象,但是在JDK1.9之后,這個構造方法被標記為Deprecated,也就是過時了,所以以后盡量不要使用這種方式創建對象。它的注釋中建議使用valueOf進行構建對象。利用構造器構造出來的對象不會經過取緩存操作,所以對于new Integer(100)的操作,得到的Integer對象與a或b進行==比較時,得到的會是false。
其實其他七種包裝類型的valueOf方法大多都是這個享元設計模式的邏輯,但是有兩個除外:Float和Double。這個其實也很好理解:因為Integer這種類型的數據,-128到127之間的數據是有限個,總共就256個數字,但是對于Float和Double這種類型,它們之間的數據個數就無法計算了,所以它兩個就沒有采用這種緩存的方式。下面是其他包裝類型中的valueOf方法的源碼:
//Short
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}
//Byte
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}
//Character
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
//Long
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
//Boolean
public static Boolean valueOf(boolean b) {
//public static final Boolean TRUE = new Boolean(true);
//public static final Boolean FALSE = new Boolean(false);
return (b ? TRUE : FALSE);
}
//Float
public static Float valueOf(float f) {
return new Float(f);
}
//Double
public static Double valueOf(double d) {
return new Double(d);
}
通過上面的代碼截圖可以看到,對于Float和Double都是直接使用了構造器直接構造對應包裝類型的對象。對于Boolean類型,就是固定的兩個TRUE和FALSE兩個常量,它們不會出現變化,這也屬于一種緩存。
對于Byte類型,它是直接全部緩存了,這里使用了cache數組,它在Byte類中定義和初始化如下:
static final Byte cache[] = new Byte[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Byte((byte)(i - 128));
}
所以cache數組中存儲的就是-128到127范圍的所有數。在構建時直接定位到具體的數組位置中去,并將該位置上的數值直接返回即可。
其余數據類型基本邏輯都差不多了,都有一個緩存值范圍,如果超過了,就利用構造器直接構造,否則直接返回緩存的對象。
拆箱
上面介紹的valueOf方法是裝箱操作的時候使用的,還有一個拆箱操作,看下面這個例子:
Integer a = 100;
int b = 20;
int c = a + b;
上面代碼反編譯之后就得到:
Integer a = Integer.valueOf((int)100);
int b = 20;
int c = a.intValue() + b;
可以看到第一步進行了自動裝箱操作,在第三行中,基本數據類型和包裝類型進行運算,需要將包裝類型進行拆箱操作,用到了intValue方法。這個方法其實在源碼中很簡單,就是一句話,返回value。我們知道任何包裝類型,內部都有一個基本數據類型的字段用于存儲對應基本類型的值,這個字段就是value。
相應的其他包裝類型在進行拆箱的時候,都會調用對應的xxxValue方法,例如:byteValue、shortValue等等方法。其實內部邏輯都是一樣,直接返回存儲的value值。
自動裝箱和拆箱的時機
直接賦值
這個情況其實在前面介紹自動裝箱的操作的時候,舉例代碼中就是這種情況,將一個字面量直接賦值給對應包裝類型會觸發自動裝箱操作。
函數參數
//自動拆箱
public int getNum1(Integer num) {
return num;
}
//自動裝箱
public Integer getNum2(int num) {
return num;
}
集合操作
在Java的集合中,泛型只能是包裝類型,但是我們在存儲數據的時候,一般都是直接存儲對應的基本類型數據,這里就有一個自動裝箱的過程。
運算符運算
上面在拆箱操作的時候利用的就是這個特性,當基本數據類型和對應的包裝類型進行算術運算時,包裝類型會首先進行自動拆箱,然后再與基本數據類型的數據進行運算。
說到運算符,這里對于自動拆箱有一個需要注意的地方:
Integer a = null;
int b = a;// int b = a.intValue();
這種情況編譯是可以通過的,但是在運行的時候會拋出空指針異常,這就是自動拆箱導致的這種錯誤。因為自動拆箱會調用intValue方法,但是此時a是null,所以會拋異常。平時在使用的時候,注意非空判斷即可。
自動裝拆箱帶來的問題
==比較
首先就是前面提到的關于==操作符的結果問題,因為自動裝箱的機制,我們不能依賴于==操作符,它在一定范圍內數值相同為true,但是在更多的空間中,數值相同的包裝類型對象比較的結果為false。如果需要比較,可以考慮使用equals比較或者將其轉換成對應的基本類型再進行比較可以保證結果的一致性。
空指針
這是上面在說到運算符的時候提到的一種情況,因為有自動拆箱的機制,如果初始的包裝類型對象為null,那么在自動拆箱的時候的就會報NullPointerException,在使用時需要格外注意,在使用之前進行非空判定,保證程序的正常運行。
內存浪費
這里有個例子:
Integer sum = 0;
for(int i=1000; i<5000; i++){
sum+=i;
}
上面代碼中的 sum+=i 這個操作其實就是拆箱再裝箱的過程,拆箱過程是發生在相加的時候,sum本身是Integer,自動拆箱成int與 i 相加。將得到的結果賦值給sum的時候,又會進行自動裝箱,所以上面的for循環體中一句話,在編譯后會變為兩句:
int result = sum.intValue() + i;
Integer sum = new Integer(result);
所以在進行了5000次循環后,會出現大量的無用對象造成內容空間的浪費,同時加重了垃圾回收的工作量,所以在日常編碼過程中需要格外注意,避免出現這種浪費現象。
方法重載問題
最典型的就是ArrayList中出現的remove方法,它有remove(int index)和remove(Object obj)方法,如果此時恰巧ArrayList中存儲的就是Integer元素,那么會不會出現混淆的情況呢?其實這個只需要做一個簡單的測試就行:
public static void test(Integer num) {
System.out.println("Integer參數的方法被調用...");
}
?
public static void test(int num) {
System.out.println("int參數的方法被調用...");
}
public static void main(String[] args) {
int i = 2;
test(i); //int參數的方法被調用...
Integer j = 4;
test(j);//Integer參數的方法被調用...
}
所以可以發現,當出現這種情況的時候,是不會發生自動裝箱和拆箱操作的。可以正常區分。