什麼是自動拆箱和裝箱
定義
- 自動裝箱就是 Java 自動將原始數據類型轉爲對應的包裝類對象 比如將 int 型的變量轉成 Integer對象 自動拆箱反之(從 Java 1.5 開始引入)
過程
- 自動裝箱時,編譯器調用 valueOf() 將原始數據類型值轉爲對象;同時自動拆箱時,編譯器調用類似 intValue(), doubleValue() 這類方法將對象轉換成原始類型值
基本類型 | 大小 | 數值範圍 | 默認值 | 包裝類型 |
---|---|---|---|---|
boolean | ^ | true,false | false | Boolean |
byte | 8bit | -2^7 – 2^7-1 | 0 | Byte |
char | 16bit | \u0000 – \uffff | \u0000 | Character |
short | 16bit | -2^15 – 2^15-1 | 0 | Short |
int | 32bit | -2^31 – 2^31-1 | 0 | Integer |
long | 64bit | -2^63 – 2^63-1 | 0 | Long |
float | 32bit | IEEE 754 | 0.0f | Float |
double | 64bit | IEEE 754 | 0.0d | Double |
void | ^ | ^ | ^ | Void |
基本類型與裝箱基本類型的區別
基本類型只有值,而裝箱基本類型則具有與它們的值不同的同一性。換句話說,對裝箱基本類型運用 == 操作符幾乎總是錯的
基本類型只有功能完備的值,而裝箱基本類型除了它對應基本類型的所有功能值外,還有一個非功能值 null,這導致了對於包裝基本類型進行拆箱操作後所進行的操作存在 NPE 的風險
基本類型更加省空間和時間,如果對包裝類型進行頻繁的裝箱和拆箱操作會影響性能
何時發生自動裝箱與拆箱?
賦值時
- 在Java1.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
自動裝箱引起的性能問題
- 如果有人告訴你:“只要修改一個字符,下面這段代碼的運行速度就能提高5倍。”,你覺得可能麼?
long t = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms");
- 輸出結果:
total:2305843005992468481
processing time: 63556 ms
- 將Long修改爲long,再來看一下運行結果
long t = System.currentTimeMillis();
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println("total:" + sum);
System.out.println("processing time: " + (System.currentTimeMillis() - t) + " ms");
- 輸出結果:
total:2305843005992468481
processing time: 12229 ms
- 事實證明,僅僅修改了一個字符,性能提高了不止一倍兩倍。那,就究竟是什麼原因導致的呢?
因爲,+這個操作符不適用Integer對象,在進行數值相加操作之前會發生自動拆箱操作,轉換成int,相加之後還會發生自動拆箱操作,裝換成Integer對象。其內部變化如下:
sum = sum.longValue() + i;
Long sum = new Long(sum);
- 很明顯,在上面的循環中會創建2147483647個”Long“類型實例,在這樣龐大的循環中,會降低程序的性能並且加重了垃圾回收的工作量
說明:包含在包裝器中的內容不會改變。即Long對象是不可變的
重載與自動裝箱
- 在java 5之前,value(int)和value(Integer)是完全不相同的方法,開發者不會因爲傳入是int還是Integer調用哪個方法困惑,但是由於自動裝箱和拆箱的引入,處理重載方法時會不會有什麼變化呢?可通過下面一個例子進行探討:
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
- 輸出結果
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
}
}
- 輸出結果
i1==i2 : true
num1 == num2 : true
obj1 == obj2 : true
one == anotherOne : false
值得注意的是,在Example 2中,比較是一個對象和一個原始值,出現這種情況比較的應該是對象的值
讓人感到困惑的Example 3,在一開始我們說過,”==”用於對象間的比較時,比較的是它們的引用,那麼爲什麼obj1 == obj2返回的結果卻是true?這是一種極端情況,處於節省內存的考慮,JVM會緩存-128到127的Integer對象。也就是說,在創建obj1對象時,會進行自動裝箱操作,並且將其對象保存至緩存中,在創建obj2對象時,同樣會進行自動裝箱操作,然後在緩存中查找是否有相同值的對象,如果有,那麼obj2對象就會指向obj1對象。obj1和obj2實際上是同一個對象。所以使用”==”比較返回true
而Example 4,是通過使用構造器來創建對象的,而沒有發生自動裝箱操作,不會執行緩存策略,故one和anotherOne是指向不同的引用的
說明:這種 Integer 緩存策略僅在自動裝箱(autoboxing)的時候有用,使用構造器創建的 Integer 對象不能被緩存
容易混亂的對象和原始數據值
- 一個很容易犯錯的問題,就是忽略對象與原始數據值之間的差異,在進行比較操作時,對象如果沒有初始化或者爲null,在自動拆箱過程中obj.xxxValue,則會拋出NullPointerException,可通過以下例子進行探討:
private static Integer count;
//NullPointerException on unboxing
if( count <= 0){
System.out.println("Count is not started yet");
}
生成無用對象增加GC壓力
- 因爲自動裝箱會隱式地創建對象,像前面提到的那樣,如果在一個循環體中,會創建無用的中間對象,這樣會增加GC壓力,拉低程序的性能。所以在寫循環時一定要注意代碼,避免引入不必要的自動裝箱操作