深入理解Java中的包裝類(lèi)與自動(dòng)拆裝箱

深入理解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)拆裝箱

  1. 賦值
    上邊大家已經(jīng)看到了,不說(shuō)啦
  2. 方法調(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)題得注意?

  1. 性能問(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)題我們就不深入了

  1. 重載與自動(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
  1. 緩存值問(wèn)題

這個(gè)問(wèn)題是面試的??土?/p>

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ī)制肯定是不一樣的,大家自己去探索

  1. == 和 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比較

  1. 警惕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,感謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 自動(dòng)裝箱和拆箱從Java1.5開(kāi)始引入,目的是將原始類(lèi)型值轉(zhuǎn)自動(dòng)地轉(zhuǎn)換成對(duì)應(yīng)的對(duì)象。自動(dòng)裝箱與拆箱的機(jī)制可以讓我們...
    GB_speak閱讀 650評(píng)論 0 4
  • 自動(dòng)裝箱和拆箱從Java 1.5開(kāi)始引入,目的是將原始類(lèi)型值轉(zhuǎn)自動(dòng)地轉(zhuǎn)換成對(duì)應(yīng)的對(duì)象。自動(dòng)裝箱與拆箱的機(jī)制可以讓我...
    codersm閱讀 453評(píng)論 0 0
  • 基本數(shù)據(jù)類(lèi)型的包裝類(lèi) 包裝類(lèi)基本知識(shí) Java是面向?qū)ο蟮恼Z(yǔ)言,但不是“純面向?qū)ο蟆?,基本?shù)據(jù)類(lèi)型就不是對(duì)象。但是...
    全棧JAVA筆記閱讀 608評(píng)論 0 1
  • 幸運(yùn)者:余俊娟 地點(diǎn):湖北省武漢市 時(shí)間:2017.7.20,周四 1,我怎么如此幸運(yùn),早上起來(lái),啟動(dòng)了早晨的程序...
    余俊娟閱讀 317評(píng)論 0 0
  • 發(fā)現(xiàn)了很牛,裝逼的圖片美美機(jī)器 覺(jué)得發(fā)表我的成果 果然顯得高逼格 于是就多發(fā)幾張 太好玩了,我愛(ài)你
    旭小東閱讀 194評(píng)論 0 0

友情鏈接更多精彩內(nèi)容