以妹之名:自動裝箱和自動拆箱
“哥,聽說 Java 的每個基本類型都對應(yīng)了一個包裝類型,比如說 int 的包裝類型為 Integer,double 的包裝類型為 Double,是這樣嗎?”從三妹這句話當中,能聽得出來,她已經(jīng)提前預(yù)習這塊內(nèi)容了。
“是的,三妹?;绢愋秃桶b類型的區(qū)別主要有以下 4 點,我來帶你學習一下。”我回答說。我們家的斜對面剛好是一所小學,所以時不時還能聽到朗朗的讀書聲,讓人心情非常愉快。
“三妹,你準備好了嗎?我們開始吧?!?/p>
“第一,包裝類型可以為 null,而基本類型不可以。別小看這一點區(qū)別,它使得包裝類型可以應(yīng)用于 POJO 中,而基本類型則不行?!?/p>
“POJO 是什么呢?”遇到不會的就問,三妹在這一點上還是非常兢兢業(yè)業(yè)的。
“POJO 的英文全稱是 Plain Ordinary Java Object,翻譯一下就是,簡單無規(guī)則的 Java 對象,只有字段以及對應(yīng)的 setter 和 getter 方法?!?/p>
class Writer {
private Integer age;
private String name;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
和 POJO 類似的,還有數(shù)據(jù)傳輸對象 DTO(Data Transfer Object,泛指用于展示層與服務(wù)層之間的數(shù)據(jù)傳輸對象)、視圖對象 VO(View Object,把某個頁面的數(shù)據(jù)封裝起來)、持久化對象 PO(Persistant Object,可以看成是與數(shù)據(jù)庫中的表映射的 Java 對象)。
“那為什么 POJO 的字段必須要用包裝類型呢?”三妹問。
“《阿里巴巴 Java 開發(fā)手冊》上有詳細的說明,你看。”我打開 PDF,并翻到了對應(yīng)的內(nèi)容,指著屏幕念道。
數(shù)據(jù)庫的查詢結(jié)果可能是 null,如果使用基本類型的話,因為要自動拆箱,就會拋出 NullPointerException 的異常。
“什么是自動拆箱呢?”
“自動拆箱指的是,將包裝類型轉(zhuǎn)為基本類型,比如說把 Integer 對象轉(zhuǎn)換成 int 值;對應(yīng)的,把基本類型轉(zhuǎn)為包裝類型,則稱為自動裝箱。”
“哦?!?/p>
“那接下來,我們來看第二點不同。包裝類型可用于泛型,而基本類型不可以,否則就會出現(xiàn)編譯錯誤。”一邊說著,我一邊在 Intellij IDEA 中噼里啪啦地敲了起來。
“三妹,你瞧,編譯器提示錯誤了。”
List<int> list = new ArrayList<>(); // 提示 Syntax error, insert "Dimensions" to complete ReferenceType
List<Integer> list = new ArrayList<>();
“為什么呢?”三妹及時地問道。
“因為泛型在編譯時會進行類型擦除,最后只保留原始類型,而原始類型只能是 Object 類及其子類——基本類型是個例外?!?/p>
“那,接下來,我們來說第三點,基本類型比包裝類型更高效?!蔽液攘艘豢诓枥^續(xù)說道。
“作為局部變量時,基本類型在棧中直接存儲的具體數(shù)值,而包裝類型則存儲的是堆中的引用?!蔽乙贿呎f著,一邊打開 draw.io 畫起了圖。

很顯然,相比較于基本類型而言,包裝類型需要占用更多的內(nèi)存空間,不僅要存儲對象,還要存儲引用。假如沒有基本類型的話,對于數(shù)值這類經(jīng)常使用到的數(shù)據(jù)來說,每次都要通過 new 一個包裝類型就顯得非常笨重。
“三妹,你想知道程序運行時,數(shù)據(jù)都存儲在什么地方嗎?”
“嗯嗯,哥,你說說唄?!?/p>
“通常來說,有 4 個地方可以用來存儲數(shù)據(jù)?!?/p>
1)寄存器。這是最快的存儲區(qū),因為它位于 CPU 內(nèi)部,用來暫時存放參與運算的數(shù)據(jù)和運算結(jié)果。
2)棧。位于 RAM(Random Access Memory,也叫主存,與 CPU 直接交換數(shù)據(jù)的內(nèi)部存儲器)中,速度僅次于寄存器。但是,在分配內(nèi)存的時候,存放在棧中的數(shù)據(jù)大小與生存周期必須在編譯時是確定的,缺乏靈活性?;緮?shù)據(jù)類型的值和對象的引用通常存儲在這塊區(qū)域。
3)堆。也位于 RAM 區(qū),可以動態(tài)分配內(nèi)存大小,編譯器不必知道要從堆里分配多少存儲空間,生存周期也不必事先告訴編譯器,Java 的垃圾收集器會自動收走不再使用的數(shù)據(jù),因此可以得到更大的靈活性。但是,運行時動態(tài)分配內(nèi)存和銷毀對象都需要占用時間,所以效率比棧低一些。new 創(chuàng)建的對象都會存儲在這塊區(qū)域。
4)磁盤。如果數(shù)據(jù)完全存儲在程序之外,就可以不受程序的限制,在程序沒有運行時也可以存在。像文件、數(shù)據(jù)庫,就是通過持久化的方式,讓對象存放在磁盤上。當需要的時候,再反序列化成程序可以識別的對象。
“能明白嗎?三妹?”
“這節(jié)講完后,我再好好消化一下。”
“那好,我們來說第四點,兩個包裝類型的值可以相同,但卻不相等?!?/p>
Integer chenmo = new Integer(10);
Integer wanger = new Integer(10);
System.out.println(chenmo == wanger); // false
System.out.println(chenmo.equals(wanger )); // true
“兩個包裝類型在使用“==”進行判斷的時候,判斷的是其指向的地址是否相等,由于是兩個對象,所以地址是不同的?!?/p>
“而 chenmo.equals(wanger) 的輸出結(jié)果為 true,是因為 equals() 方法內(nèi)部比較的是兩個 int 值是否相等?!?/p>
private final int value;
public int intValue() {
return value;
}
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
雖然 chenmo 和 wanger 的值都是 10,但他們并不相等。換句話說就是:將“==”操作符應(yīng)用于包裝類型比較的時候,其結(jié)果很可能會和預(yù)期的不符。
“三妹,瞧,((Integer)obj).intValue() 這段代碼就是用來自動拆箱的。下面,我們來詳細地說一說自動裝箱和自動拆箱?!?/p>
既然有基本類型和包裝類型,肯定有些時候要在它們之間進行轉(zhuǎn)換。把基本類型轉(zhuǎn)換成包裝類型的過程叫做裝箱(boxing)。反之,把包裝類型轉(zhuǎn)換成基本類型的過程叫做拆箱(unboxing)。
在 Java 1.5 之前,開發(fā)人員要手動進行裝拆箱,比如說:
Integer chenmo = new Integer(10); // 手動裝箱
int wanger = chenmo.intValue(); // 手動拆箱
Java 1.5 為了減少開發(fā)人員的工作,提供了自動裝箱與自動拆箱的功能。這下就方便了。
Integer chenmo = 10; // 自動裝箱
int wanger = chenmo; // 自動拆箱
來看一下反編譯后的代碼。
Integer chenmo = Integer.valueOf(10);
int wanger = chenmo.intValue();
也就是說,自動裝箱是通過 Integer.valueOf() 完成的;自動拆箱是通過 Integer.intValue() 完成的。
“嗯,三妹,給你出一道面試題吧。”
// 1)基本類型和包裝類型
int a = 100;
Integer b = 100;
System.out.println(a == b);
// 2)兩個包裝類型
Integer c = 100;
Integer d = 100;
System.out.println(c == d);
// 3)
c = 200;
d = 200;
System.out.println(c == d);
“給你 3 分鐘時間,你先思考下,我去抽根華子,等我回來,然后再來分析一下為什么?!?/p>
。。。。。。
“嗯,哥,你過來吧,我說一說我的想法?!?/p>
第一段代碼,基本類型和包裝類型進行 == 比較,這時候 b 會自動拆箱,直接和 a 比較值,所以結(jié)果為 true。
第二段代碼,兩個包裝類型都被賦值為了 100,這時候會進行自動裝箱,按照你之前說的,將“==”操作符應(yīng)用于包裝類型比較的時候,其結(jié)果很可能會和預(yù)期的不符,我想結(jié)果可能為 false。
第三段代碼,兩個包裝類型重新被賦值為了 200,這時候仍然會進行自動裝箱,我想結(jié)果仍然為 false。
“嗯嗯,三妹,你分析的很有邏輯,但第二段代碼的結(jié)果為 true,是不是感到很奇怪?”
“為什么會這樣呀?”三妹急切地問。
“你說的沒錯,自動裝箱是通過 Integer.valueOf() 完成的,我們來看看這個方法的源碼就明白為什么了。”
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
是不是看到了一個之前從來沒見過的類——IntegerCache?
“難道說是 Integer 的緩存類?”三妹做出了自己的判斷。
“是的,來看一下 IntegerCache 的源碼吧?!?/p>
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
}
大致瞟一下這段代碼你就全明白了。-128 到 127 之間的數(shù)會從 IntegerCache 中取,然后比較,所以第二段代碼(100 在這個范圍之內(nèi))的結(jié)果是 true,而第三段代碼(200 不在這個范圍之內(nèi),所以 new 出來了兩個 Integer 對象)的結(jié)果是 false。
“三妹,看完上面的分析之后,我希望你記住一點:當需要進行自動裝箱時,如果數(shù)字在 -128 至 127 之間時,會直接使用緩存中的對象,而不是重新創(chuàng)建一個對象?!?/p>
“自動裝拆箱是一個很好的功能,大大節(jié)省了我們開發(fā)人員的精力,但也會引發(fā)一些麻煩,比如下面這段代碼,性能就很差。”
long t1 = System.currentTimeMillis();
Long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE;i++) {
sum += i;
}
long t2 = System.currentTimeMillis();
System.out.println(t2-t1);
“知道為什么嗎?三妹?!?/p>
“難道是因為 sum 被聲明成了包裝類型 Long 而不是基本類型 long?!比萌粲兴肌?/p>
“是滴,由于 sum 是個 Long 型,而 i 為 int 類型,sum += i 在執(zhí)行的時候,會先把 i 強轉(zhuǎn)為 long 型,然后再把 sum 拆箱為 long 型進行相加操作,之后再自動裝箱為 Long 型賦值給 sum。”
“三妹,你可以試一下,把 sum 換成 long 型比較一下它們運行的時間?!?/p>
。。。。。。
“哇,sum 為 Long 型的時候,足足運行了 5825 毫秒;sum 為 long 型的時候,只需要 679 毫秒?!?/p>
“好了,三妹,今天的主題就先講到這吧。我再去來根華子?!?/p>
PS:點擊「閱讀原文」可直達《教妹學Java》專欄的 GitHub 開源地址,記得 star 哦!
