你還在為開發中頻繁切換環境打包而煩惱嗎?快來試試 Environment Switcher 吧!使用它可以在app運行時一鍵切換環境,而且還支持其他貼心小功能,有了它媽媽再也不用擔心頻繁環境切換了。https://github.com/CodeXiaoMai/EnvironmentSwitcher
自動裝箱和拆箱從 Java 1.5 開始引入,目的是將原始類型值自動地轉換成對應的對象。自動裝箱與拆箱的機制可以讓我們在 Java 的變量賦值或者是方法調用等情況下使用原始類型或者對象類型更加簡單直接。
如果你在 Java1.5 下進行過編程的話,你一定不會陌生這一點,你不能直接地向集合(Collections)中放入原始類型值,因為集合只接收對象。通常這種情況下你的做法是,將這些原始類型的值轉換成對象,然后將這些轉換的對象放入集合中。使用 Integer、Double、Boolean 等這些類我們可以將原始類型值轉換成對應的對象,但是從某些程度可能使得代碼不是那么簡潔精煉。為了讓代碼簡練,Java 1.5 引入了具有在原始類型和對象類型自動轉換的裝箱和拆箱機制。但是自動裝箱和拆箱并非完美,在使用時需要有一些注意事項,如果沒有搞明白自動裝箱和拆箱,可能會引起難以察覺的 bug。
本文將介紹,什么是自動裝箱和拆箱,自動裝箱和拆箱發生在什么時候,以及要注意的事項。
什么是自動裝箱和拆箱
自動裝箱就是 Java 自動將原始類型值轉換成對應的對象,比如將 int 類型的變量轉換成 Integer 對象,這個過程叫做裝箱,反之將 Integer 對象轉換成 int 類型值,這個過程叫做拆箱。因為這里的裝箱和拆箱是自動進行的非人為轉換,所以就稱作為自動裝箱和拆箱。原始類型 byte,short,char,int,long,float,double 和boolean 對應的封裝類為 Byte、Short、Character、Integer、Long、Float、Double、Boolean。
自動裝箱拆箱要點
- 自動裝箱時編譯器調用 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
方法調用時
這是另一個常用的情況,當我們在方法調用時,我們可以傳入原始數據值或者對象,同樣編譯器會幫我們進行轉換。
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 值。
自動裝箱的弊端
自動裝箱有一個問題,那就是在一個循環中進行自動裝箱操作的情況,如下面的例子就會創建多余的對象,影響程序的性能。
Integer sum = 0;
for(int i=1000; i<5000; i++){
sum += i;
}
上面的代碼 sum += i
可以看成 sum = sum + i
,但是 +
這個操作符不適用于Integer 對象,首先 sum 進行自動拆箱操作,然后進行數值相加操作,最后發生自動裝箱操作轉換成 Integer 對象。其內部變化如下:
int result = sum.intValue() + i;
Integer sum = new Integer(result);
由于我們這里聲明的 sum 為 Integer 類型,在上面的循環中會創建將近 4000 個無用的 Integer 對象,在這樣龐大的循環中,會降低程序的性能并且加重了垃圾回收的工作量。因此在我們編程時,需要注意到這一點,正確地聲明變量類型,避免因為自動裝箱引起的性能問題。
重載與自動裝箱
當重載遇上自動裝箱時,情況會比較復雜,可能會讓人產生困惑。在 Java 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。
容易混亂的對象和原始數據值
另一個需要避免的問題就是混亂使用對象和原始數據值,一個具體的例子就是當我們在對一個原始數據值和一個對象進行比較時,如果這個對象沒有進行初始化或者為Null,在自動拆箱過程中 obj.xxxValue() 會拋出 NullPointerException,如下面的代碼:
private static Integer count;
//NullPointerException on unboxing
if( count <= 0){
System.out.println("Count is not started yet");
}
緩存的對象
這個問題就是我們上面提到的極端情況,在 Java 中,會對 -128 到 127 的
Integer 對象進行緩存,當創建新的 Integer 對象時,如果符合這個這個范圍,并且已有存在的相同值的對象,則返回這個對象,否則創建新的 Integer 對象。
在 Java 中另一個節省內存的例子就是字符串常量池。
生成無用對象增加GC壓力
因為自動裝箱會隱式地創建對象,像前面提到的那樣,如果在一個循環體中,會創建無用的中間對象,這樣會增加 GC 壓力,降低程序的性能。所以在寫循環時一定要注意,避免引入不必要的自動裝箱操作。
如想了解垃圾回收和內存優化,可以查看Google IO:Android內存管理主題演講記錄
總的來說,自動裝箱和拆箱著實為開發者帶來了很大的方便,但是在使用時也是需要格外留意,避免引起出現文章提到的問題。