深入理解Java中的包裝類(lèi)與自動(dòng)拆裝箱
文章出處:安卓進(jìn)階學(xué)習(xí)指南
作者:麥田哥(Wheat7)
審核者:shixinzhang Struggle
完稿日期:2017.10.30
今兒來(lái)和大家聊一聊Java中的自動(dòng)拆裝箱問(wèn)題告材,也是我們安卓進(jìn)階學(xué)習(xí)指南的一部分,歡迎大家多多關(guān)注中燥,其中的一些問(wèn)題也是我重新學(xué)習(xí)得到的毡鉴,歡迎大家多多討論
什么是自動(dòng)拆裝箱
自動(dòng)拆裝箱在Java5(就是Java1.5,后邊改了命名)時(shí)被引入犀概,自動(dòng)裝箱就是Java自動(dòng)將基礎(chǔ)類(lèi)型值轉(zhuǎn)換成對(duì)應(yīng)的包裝類(lèi)對(duì)象,比如將int的變量轉(zhuǎn)換成Integer對(duì)象,這個(gè)過(guò)程叫做裝箱财异,反之將Integer對(duì)象轉(zhuǎn)換成int類(lèi)型值,這個(gè)過(guò)程叫做拆箱唱遭。說(shuō)白了戳寸,就是個(gè)語(yǔ)法糖
基本類(lèi)型與引用類(lèi)型
稍有常識(shí)的人都看得出。拷泽。疫鹊。哦,不對(duì)司致,稍有Java基礎(chǔ)的同學(xué)都應(yīng)該知道Java的數(shù)據(jù)類(lèi)型拆吆,大的分類(lèi)就分為基礎(chǔ)類(lèi)型與引用類(lèi)類(lèi)型
基礎(chǔ)類(lèi)型又能分為我們俗稱(chēng)的四類(lèi)八種,分別為四種整型脂矫,byte枣耀,short,int羹唠,long奕枢,他們的區(qū)別是所能存儲(chǔ)的數(shù)據(jù)的長(zhǎng)度不同娄昆,也就是說(shuō)他們?cè)趦?nèi)存中分配的內(nèi)存長(zhǎng)度不同,兩種浮點(diǎn)類(lèi)型缝彬,32位的單精度浮點(diǎn)float萌焰,64位雙精度浮點(diǎn)數(shù)double,1種Unicode編碼的字符單元
char谷浅,最后就是boolean扒俯,真值布爾類(lèi)型。 接下來(lái)是與我們今天主題相關(guān)的重點(diǎn)一疯,就是基礎(chǔ)類(lèi)型是存儲(chǔ)在棧內(nèi)存中的撼玄,在程序啟動(dòng)的時(shí)候就會(huì)被初始化,就是你用或不用墩邀,他都在那里掌猛,在你聲明一個(gè)基本類(lèi)型的時(shí)候,他就被賦予了默認(rèn)的初始值眉睹,比如int類(lèi)型荔茬,就是0
并且我們?cè)贁U(kuò)展討論一下這個(gè)情況
int a = 1
int b = 1
System.out.printf(a == b) ---- true
因?yàn)?== 判斷的是內(nèi)存地址,也就是判斷兩者是否為同一個(gè)對(duì)象竹海,基本類(lèi)型相同的值指向的是同一塊內(nèi)存區(qū)域慕蔚,所以返回的是ture,這也就解釋了我們?yōu)槭裁纯梢杂?=來(lái)判斷基本類(lèi)型斋配,而不能用 == 來(lái)判斷引用類(lèi)型孔飒,而是要用equals()方法
基本類(lèi)型并不具對(duì)象的性質(zhì)
2.引用類(lèi)型又有類(lèi),接口艰争,數(shù)組三種坏瞄,為什么叫引用類(lèi)型,因?yàn)槲覀兊囊妙?lèi)型的對(duì)象甩卓,是存在于堆內(nèi)存中的惦积,我們所持有的是棧內(nèi)存中指向相應(yīng)堆內(nèi)存的一個(gè)引用
這和自動(dòng)拆裝箱有什么關(guān)系?請(qǐng)看下邊
持有對(duì)象&包裝類(lèi)
在有些情況下猛频,我們需要持有一系列的對(duì)象,也就是使用我們常用的集合類(lèi)蛛勉,在這里不展開(kāi)說(shuō)鹿寻,然而集合類(lèi)在設(shè)計(jì)的時(shí)候持有的是我們所有類(lèi)型的單根超類(lèi),Object诽凌,在將對(duì)象裝入集合的時(shí)候毡熏,對(duì)象都會(huì)被向上轉(zhuǎn)型為Object類(lèi),然后取出的時(shí)候侣诵,又通過(guò)參數(shù)化類(lèi)型狱窘,也就是我們常用的泛型菱形<>語(yǔ)法财搁,轉(zhuǎn)型為我們裝入的原始類(lèi)型,但是如果我們呢要持有的是基本類(lèi)型呢尖奔?基礎(chǔ)類(lèi)型的并沒(méi)有父類(lèi)搭儒,所以集合類(lèi)并不能持有他提茁,那怎么辦呢?于是Java為每一個(gè)基礎(chǔ)類(lèi)型封裝了相應(yīng)的包裝類(lèi)
基本類(lèi)型 | 包裝類(lèi) |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
于是我們可以這樣操作了
List<Interger> intList = new ArrayList<>();
intList.add(1);
注意后邊一句茴扁,這就是我們后邊要說(shuō)的自動(dòng)裝箱铃岔,因?yàn)閍dd方法需要傳入的是List中所持有的參數(shù)化類(lèi)型,也就是int的包裝類(lèi)型Integer峭火,而我們傳入的是一個(gè)int類(lèi)型的值,這個(gè)int值被編譯器自動(dòng)包裝成了Integer值,這就是本文的主題躲胳,自動(dòng)拆裝箱,后邊我們會(huì)展開(kāi)細(xì)說(shuō)
你應(yīng)該會(huì)想到隆檀,數(shù)組可以來(lái)持有基本類(lèi)型啊粹湃,但是你也知道的是,有些時(shí)候我們要持有的數(shù)量是不確定的裳仆,數(shù)組在初始化的時(shí)候就必須確定長(zhǎng)度孤钦,這使我們使用數(shù)組來(lái)持有基本類(lèi)型,或是對(duì)象都有很大的局限性
集合持有對(duì)象是包裝類(lèi)最常見(jiàn)的應(yīng)用點(diǎn)静袖,當(dāng)然也有其他地方俊扭,我們需要的是Object參數(shù)而又需要的是數(shù)值,或者是其他基本類(lèi)型的時(shí)候捐康,也會(huì)應(yīng)用到包裝類(lèi)解总,包裝類(lèi)使基本類(lèi)型有了對(duì)象的性質(zhì),并且為其添加了屬性和方法倾鲫,豐富了基本類(lèi)型的操作
自動(dòng)拆裝箱
何為自動(dòng)拆裝箱乌昔,請(qǐng)看代碼
Java5以前
//裝箱
Integer integer = new Integer(10);
//拆箱
int i = integer.intValue();
Java5以后
//自動(dòng)裝箱
Integer integer = 10;
//自動(dòng)拆箱
int i = integer;
在Java5之前,你需要一個(gè)Integer類(lèi)型的對(duì)象供屉,你需要像其他對(duì)象一樣溺蕉,把他new出來(lái)(調(diào)用靜態(tài)方法Integer.valueOf(3)來(lái)創(chuàng)建對(duì)象內(nèi)部也是new疯特,別抬杠),拆箱需要調(diào)用intValue()方法來(lái)取出int值录别,而在Java5之后邻吞,你創(chuàng)建Integer類(lèi)型的對(duì)象,可以直接用int類(lèi)型賦值崔列,Integer類(lèi)型的也能賦值給int類(lèi)型的變量
通俗點(diǎn)來(lái)說(shuō)旺遮,就是基本類(lèi)型和他的包裝類(lèi)可以互相賦值了耿眉,在賦值的時(shí)候,編譯器自動(dòng)的進(jìn)行了包裝/拆箱工作,但是不僅僅是賦值的時(shí)候會(huì)發(fā)生自動(dòng)拆裝箱,請(qǐng)看下一個(gè)問(wèn)題
什么時(shí)候會(huì)發(fā)生自動(dòng)拆裝箱
- 賦值
上邊大家已經(jīng)看到了斤寇,不說(shuō)啦 - 方法調(diào)用傳入?yún)?shù)的時(shí)候
public void argAutoBoxing(Integer i) {
}
argAutoBoxing(1);
public void argAutoUnBoxing(int i) {
}
argAutoUnBoxing(new Integer(1));
3.被操作符操作的時(shí)候
Integer integer = new Integer(1);
int i = interger + 1
自動(dòng)拆裝箱是怎么實(shí)現(xiàn)的
一句話娘锁,就是編譯器幫我們自動(dòng)調(diào)用了拆裝箱的方法饺鹃,以Integer/int為例子悔详,自動(dòng)裝箱就是編譯器自動(dòng)調(diào)用了valueOf(int i)方法,自動(dòng)拆箱自動(dòng)調(diào)用了intValue()方法,其他基本類(lèi)型類(lèi)推
有哪些問(wèn)題得注意?
- 性能問(wèn)題
首先在堆內(nèi)存中創(chuàng)建對(duì)象的消耗肯定是要比使用棧內(nèi)存要多的缝驳,同時(shí)在自動(dòng)拆裝箱的時(shí)候归苍,也有一定的性能消耗,如果在數(shù)據(jù)量比較大夏伊,或者是循環(huán)的情況下吻氧,頻繁的拆裝箱并且生成包裝類(lèi)的時(shí)候医男,對(duì)性能的影響就是一星半點(diǎn)了,所以不是特殊的需求刀森,例如上述被集合持有的情況报账,還是使用基本類(lèi)型而不是包裝類(lèi)
在循環(huán)的時(shí)候
Integer sum = 0;
for(int i=1000; i<5000; i++){
sum+=i;
}
上面的代碼sum+=i可以看成sum = sum + i透罢,在sum被+操作符操作的時(shí)候,會(huì)對(duì)sum進(jìn)行自動(dòng)拆箱操作乾胶,進(jìn)行數(shù)值相加操作,最后發(fā)生自動(dòng)裝箱操作轉(zhuǎn)換成Integer對(duì)象斩郎。其內(nèi)部變化如下
sum = sum.intValue() + i;
Integer sum = new Integer(result);
sum為Integer類(lèi)型喻频,在上面的循環(huán)中會(huì)創(chuàng)建4000個(gè)無(wú)用的Integer對(duì)象甥温,在這樣龐大的循環(huán)中,會(huì)降低程序的性能并且加重了垃圾回收的工作量宋梧。因此在我們編程時(shí)史简,需要注意到這一點(diǎn),正確地聲明變量類(lèi)型跺讯,避免因?yàn)樽詣?dòng)裝箱引起的性能問(wèn)題
再舉一個(gè)例子殉农,在Java中的HashMap的性能也受到自動(dòng)拆裝箱的影響
因?yàn)镠ashMap接收的參數(shù)類(lèi)型是HashMap <Object, Object>超凳,所以在增刪改查的時(shí)候,都會(huì)對(duì)Key值進(jìn)行大量的自動(dòng)拆裝箱暂雹,為了解決這個(gè)問(wèn)題创夜,Java提供了SparseArray驰吓,包括SparseBoolMap, SparseIntMap, SparseLongMap, LongSparseMap,所接受的Key值都是基本類(lèi)型的值,例如SparseIntMap就是SparseIntMap<int, Object>,在避免了大量自動(dòng)拆裝箱的同時(shí)姑廉,還降低的內(nèi)存消耗翁涤。這里就點(diǎn)到為止,具體的數(shù)據(jù)結(jié)構(gòu)的問(wèn)題我們就不深入了
- 重載與自動(dòng)裝箱
在Java5之前限书,value(int i)和value(Integer o)是完全不相同的方法倦西,開(kāi)發(fā)者不會(huì)因?yàn)閭魅胧莍nt還是Integer調(diào)用哪個(gè)方法困惑赁严,但是由于自動(dòng)裝箱和拆箱的引入疼约,處理重載方法時(shí)稍微有點(diǎn)復(fù)雜,例如在ArrayList中劝枣,有remove(int index)和remove(Object o)兩個(gè)重載方法织鲸,如果集合持有三個(gè)Integer類(lèi)型值為3,1,2的對(duì)象搂擦,我們調(diào)用remove(3), 是調(diào)用了remove的哪個(gè)重載方法?remove掉的是值為3的對(duì)象扳还,還是remove了index為3橱夭,值為2的那個(gè)對(duì)象呢棘劣?其實(shí)問(wèn)題就是,參數(shù)3是否會(huì)被自動(dòng)打包呢舆驶?答案是:不會(huì)而钞,在這種情況下,編譯器不會(huì)進(jìn)行自動(dòng)拆裝箱撬陵,所以調(diào)用的是remove(int index),index為3值為2的這個(gè)Integer對(duì)象會(huì)被remove
通過(guò)以下例子我們可以驗(yàn)證
public void testAutoBoxing(int i){
System.out.println("primitive argument");
}
public void testAutoBoxing(Integer integer){
System.out.println("wrapper argument");
}
//calling overloaded method
int value = 1;
test(value); //no autoboxing
Integer iValue = value;
test(iValue); //no autoboxing
Output:
primitive argument
wrapper argument
- 緩存值問(wèn)題
這個(gè)問(wèn)題是面試的尘匏埃客了
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
Output:
true
false
這是為什呢草添,讓我們來(lái)翻一翻源碼
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
欸,看來(lái)問(wèn)題就出在IntegerCache類(lèi)中了,我們?cè)賮?lái)翻一下IntegerCache的實(shí)現(xiàn)類(lèi)
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() {}
}
在通過(guò)valueOf方法創(chuàng)建Integer對(duì)象的時(shí)候抄淑,如果數(shù)值在[-128,127]之間肆资,便返回指向IntegerCache.cache中已經(jīng)存在的對(duì)象的引用灶芝;否則創(chuàng)建一個(gè)新的Integer對(duì)象夜涕。
上面的代碼中i1和i2的數(shù)值為100,因此會(huì)直接從cache中取已經(jīng)存在的對(duì)象栖秕,所以i1和i2指向的是同一個(gè)對(duì)象晓避,而i3和i4則是分別指向不同的對(duì)象
我們?cè)賮?lái)看一題
public class Main {
public static void main(String[] args) {
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
Output:
flase
flase
至于為什么俏拱,我們就不深挖下去了,小伙伴可以自己去看源碼事格,這里要說(shuō)的是搞隐,包裝類(lèi)都有相應(yīng)的緩存機(jī)制劣纲,來(lái)降低一般情況下的資源消耗,但是每個(gè)包裝類(lèi)的機(jī)制肯定是不一樣的劫瞳,大家自己去探索
- == 和 equlas()
大家都應(yīng)該清楚明了的了解兩者的區(qū)別,一句話說(shuō)就是 == 比較的是內(nèi)存中地址志于,equlas()對(duì)比的為數(shù)值,因?yàn)榛绢?lèi)型相同的數(shù)值指向的同一塊內(nèi)存养泡,所以可以用==來(lái)比較奈应,而引用類(lèi)型則不可以
下邊我們也是直觀的使用代碼來(lái)說(shuō)明在包裝類(lèi)和自動(dòng)拆裝箱時(shí)使用==和equlas()的情況
public class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d);
System.out.println(e==f);
System.out.println(c==(a+b));
System.out.println(c.equals(a+b));
System.out.println(g==(a+b));
System.out.println(g.equals(a+b));
System.out.println(g.equals(a+h));
}
}
Output:
true
false
true
true
true
false
true
在包裝類(lèi)的使用和自動(dòng)拆裝箱中钥组,使用==運(yùn)算符的時(shí)候程梦,如果兩個(gè)操作數(shù)都是包裝器類(lèi)型的引用橘荠,則是比較指向的是否是同一個(gè)對(duì)象,而如果其中有一個(gè)操作數(shù)是表達(dá)式(即包含算術(shù)運(yùn)算)則比較的是數(shù)值(上邊說(shuō)到的的使用運(yùn)算符觸發(fā)了自動(dòng)拆箱)挺份。另外匀泊,對(duì)于包裝器類(lèi)型朵你,equals()方法并不會(huì)進(jìn)行類(lèi)型轉(zhuǎn)換,和我們常見(jiàn)的對(duì)String類(lèi)型使用一樣躲因,比較的是對(duì)象的值
理解了這個(gè)大脉,大家應(yīng)該就對(duì)結(jié)果清晰明了了水孩,第一句和第二句是因?yàn)樯线呎f(shuō)過(guò)的緩存機(jī)制,重點(diǎn)解釋一下第三句衡怀,a+b包含了算術(shù)運(yùn)算抛杨,因此會(huì)觸發(fā)自動(dòng)拆箱過(guò)程,因此它們比較的是數(shù)值是否相等茁帽。而對(duì)于c.equals(a+b)會(huì)先觸發(fā)自動(dòng)拆箱過(guò)程屈嗤,再觸發(fā)自動(dòng)裝箱過(guò)程饶号,也就是說(shuō)a+b,會(huì)先各自調(diào)用intValue方法琅束,得到了加法運(yùn)算后的數(shù)值之后算谈,便調(diào)用Integer.valueOf方法然眼,再進(jìn)行equals比較
- 警惕NullPointerException
我們?cè)谑褂没绢?lèi)型的時(shí)候,在聲明的時(shí)候即使我們沒(méi)有對(duì)變量進(jìn)行賦值屿岂,編譯器也會(huì)自動(dòng)的為其賦予初始值雁社,比如int值就是0晒骇,boolean就是flase,所以我們?cè)谑褂没绢?lèi)型的時(shí)候徒坡,是不會(huì)出現(xiàn)NullPointerException的喇完,但在使用包裝類(lèi)的時(shí)候剥啤,我們就要注意這個(gè)問(wèn)題了,不能因?yàn)橛凶詣?dòng)拆裝箱這個(gè)語(yǔ)法糖刻诊,就忘記了包裝類(lèi)和基本類(lèi)型的區(qū)別则涯,將其同等對(duì)待了,如果你沒(méi)有在使用包裝類(lèi)的時(shí)候通過(guò)顯式亿昏、或是通過(guò)自動(dòng)裝箱機(jī)制為其賦值角钩,在你取出值呻澜、或是通過(guò)自動(dòng)拆箱使用該值的時(shí)候易迹,就會(huì)發(fā)生NullPointerException平道,這個(gè)是大家要注意的
總結(jié)
在Java中一屋,使用基本類(lèi)型還是最節(jié)省資源的選擇,雖然基礎(chǔ)類(lèi)型影響了Java的"面向?qū)ο笮?闸衫,诽嘉,但是犧牲換來(lái)了性能虫腋,所以在非必要的時(shí)候,所以我們應(yīng)該盡量避免使用包裝類(lèi)趋翻,并且在使用的時(shí)候踏烙,要清楚自動(dòng)拆裝箱機(jī)制,規(guī)避使用的誤區(qū)和風(fēng)險(xiǎn)
某些內(nèi)容和栗子參考于網(wǎng)絡(luò)辟癌,沒(méi)有找到原po,感謝愿待!