Java裝箱和拆箱詳解

Java裝箱和拆箱詳解


題外話

今天早上在學習公司代碼,然後準備學習下MVP框架,於是找了個簡單的MVP框架例子,結果在框架中,發現了一個類叫SparseArray的類,秉持着一種遇到問題就深究下去的精神,我就轉去看SparseArray相關的知識,結果發現了一片新天地,順帶研究了一番Android裏的幾個集合類,主要是SparseArray和ArrayMap,然後我就想,Java裏不是有了很多集合類了嘛,比如HashMap,TreeMap,ArrayList,LinkList等等,爲啥Android還要再弄一個呢,何況Android本身就是基於Java的,於是我又屁顛屁顛去找相關資料,結果,又發現一個小島(這個應該不是新大陸,額嘿嘿!),這個小島就是Java裝箱和拆箱,,爲了弄清楚Android爲啥還要專門弄個自己的集合類,於是就有了這篇文章,關於問題的答案就放在文章結語中吧!!

目錄

  • 小例子引發的思考
  • 源碼欣賞及解析
  • 裝箱拆箱的時機
  • 引發的細節問題
  • 小小結語

正文

小例子引發的思考

    public static void main(String[] args) {
        int i0=10;
        int i1=10;
        int i2=500;
        int i3=500;
        Integer i4=new Integer(10);
        Integer i5=new Integer(10);
        Integer i6=new Integer(500);
        Integer i7=new Integer(500);
        System.out.println("i0==i1?  "+(i0==i1));
        System.out.println("i2==i3?  "+(i2==i3));
        System.out.println("i4==i5?  "+(i4==i5));
        System.out.println("i6==i7?  "+(i6==i7));
    }

這是一個很簡單的例子,我們來看一下它的運行結果

i0==i1? true
i2==i3? true
i4==i5? false
i6==i7? false

怎麼樣,和預想的是一樣的嗎,這裏主要就是一個知識點,Java中,基本類型的==比較的是值,而封裝類型==比較的是對象的地址,所以後面兩個是false。
好了,我們再把這個代碼改一改

    public static void main(String[] args) {
        Integer i8 = 40;
        Integer i9 = 40;
        Integer i10 = 500;
        Integer i11 = 500;
        Double d0 = 40.0;
        Double d1 = 40.0;
        Double d2 = 500.0;
        Double d3 = 500.0;
        System.out.println("i8==i9?  " + (i8 == i9));
        System.out.println("i10==i11?  " + (i10 == i11));
        System.out.println("d0==d1?  " + (d0 == d1));
        System.out.println("d2==d3?  " + (d2 == d3));
    }

讓我們再來看看結果,是不是你預想中的樣子呢

i8==i9? true
i10==i11? false
d0==d1? false
d2==d3? false

嘿嘿,你現在可能就有點迷了,沒關係,接着往下看

    public static void main(String[] args) {
        Integer i12 = new Integer(40);
        Integer i13 = new Integer(40);
        Integer integer0 = new Integer(0);
        System.out.println("i12==i13?  " + (i12 == i13));
        System.out.println("i12==i13+integer0?  " + (i12 == i13 + integer0));
    }

結果可能會是什麼呢….

i12==i13? false
i12==i13+integer0? true

怎麼樣,猜對了嘛,是不是一臉蒙加上&%¥#@*,沒事,下面我們來找下原因,看看到底爲啥答案回事這樣的。

源碼欣賞及解析

在欣賞美(cao)妙(dan)的Java源碼前,我們首先需要知道的是,jdk源碼偷偷在哪裏給我們做了裝箱和拆箱的工作,答案就是valueOf()和xxxValue()這兩個方法,你會發現,不管是Integer還是Double還是Short等,都有着兩個方法,其中在Integer中,xxxValue叫做intValue,其它的類似,好了,我們現在知道了裝箱拆箱的源碼在哪,我們再去源碼一探究竟。
首先是Integer的

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

在Integer中,valueOf有三個重載方法,但是最終都會轉到上面參數爲int的方法,代碼不多,我們簡單看下,首先判斷拿到的i是否在某個範圍內,如果滿足添加條件的話,則返回一個數組對應下標的值,我們暫且先不管這個數組是啥,然後如果不滿足條件的話,則直接用i來new一個新的Integer對象。
大致流程就是上面說的,接下來我們再定位到IntegerCache類裏,這個是Integer類的一個內部類,我們看看裏面是些啥

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;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            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;
        }

        private IntegerCache() {}
    }

代碼也不多,靜下心來一行一行讀,首先它聲明瞭三個靜態常量,一個low賦了初值-128,一個high沒有賦值,默認爲0(這裏也很容易想到high就是127),一個Integer數組(取名爲cache,多多少少都可以猜想到是用來幹嘛的,~~~)。
接下來,是一個靜態代碼塊,保證只會加載一次,在這部分代碼裏面,首先做的就是爲high賦值,因爲聲明的時候只給low賦值了,然後通過sun.misc.VM.getSavedProperty(“java.lang.Integer.IntegerCache.high”)獲取一個值,這個值是幹嘛的呢,這個其實是獲取一個私有配置文件裏的值,這個值可以去配置修改,在JVM初始化的時候,就會加載,得到這個值賦值給i後,再將i和127做比較,取最大的賦值給i,然後再將新得到的i值和Integer的最大值相比較,取較小者,然後賦值給h,最後再賦值給high。
簡單點說,就是high的值是取自於配置文件中的一個值,但是這個值可能很小,也可能很大,畢竟是一個文件,我想配置爲多少就配置爲多少,然後拿到這個值後會做個限定,如果它小於127,那麼就取127,如果它大於MAX_VALUE,那麼就取值MAX_VALUE+low-1。
好,接着往下看,隨後爲數組cache開闢了地址空間,大小爲high-low+1,然後再從0下標開始初始化對應的值從low開始(也就是-128)遞增。至此結束。
在開始接下來的討論前,假定配置文件的值是默認的,也就是最終high取值127,那麼這個cache數組到底起到了什麼作用呢?結合上面的valueOf代碼片段你會發現,如果給定的基本類型int值在-128到127之間的話,就會直接去cache數組裏取,如果不在這個範圍的話,那麼就會創新的對象,到這裏你也就明白了爲啥用40和500這兩個基本類型聲明包裝類型Integer時(裝箱),不同的值居然結果不一樣的原因。
那麼問題來了,這樣做有啥好處呢?你想想這個問題,你平常使用的int類型是不是大都在-128到127之間,也就說這個範圍是一個“熱”範圍,爲了節約內存開銷,在新聲明的值在-128-127之間的Integer包裝類型時,直接從預加載的緩存中去取,這個緩存機制上面就已經解釋了,就是從一個cache數組中取,不在-128-127這個範圍的才需要重新new對象,而new對象是需要內存的,如果沒有這個緩存機制,那麼只要是遇到需要裝箱的情形,那麼全部都要聲明新的Integer對象,而且很多還是重複的,浪費了大量的內存,這當然不是我們想要看到的。(不禁感嘆,jdk源碼設計者真的是煞費苦心啊!)
至此,弄清楚了裝箱的原理,下面我們再去intValue方法看看,拆箱是怎麼做的,源碼如下

    public int intValue() {
        return value;
    }

嘿嘿,只有一行代碼呢,真的簡單,這個value值是一個內部變量,在聲明Integer對象的時候用的,這裏直接返回即可得到拆箱後的結果,不過你要是細心的話,你會發現還有類似的shotValue、longValue等方法,代表也可以直接拆箱成short等類型的。
同理,明白了Integer的裝箱原理之後,相信你也應該猜到了Byte或者Short或者Long的實現原理,我們就不一一去看了,一共八種基本類型,其中byte,short,char,int,long這五種基本類型的裝箱實現原理都和int類似,雖然它們之間有一點點區別,但是大同小異,我們就不一一去看了,然後還剩下boolean和float和double,我們先看Boolean裏的

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

嗯,看着挺清爽的,比Integer的簡單多了,嘿嘿,這個,我就不多解釋了吧。註釋這裏的TRUE是大寫的哦,和小寫的true不一樣,算了還是囉嗦一下吧,不放心啊,這裏的TRUE是一個對象,聲明如下

public static final Boolean TRUE = new Boolean(true);

可以看到是一個靜態常量對象,保證不會有重複的對象,不消耗內存。因爲Boolean是一個比較奇葩的,畢竟只有兩個值,所以也不用像Integer那樣去弄個緩存機制。
接下來就是Float和Double,將這兩個放在一起是因爲這兩個是類似的,相比較於Integer,它們的區別就是,沒有采用緩存機制,也就是沒有設置“熱”範圍,仔細思考即可知道,這個要設置“熱”範圍的話,其實是不好處理的,因爲精度問題可能還得不償失,加上本身Float和Double在開發中相對用的比較少,所以沒有設置緩存策略,我們還是象徵性的看一下Double的valueOf方法意思一下吧

    public static Double valueOf(double d) {
        return new Double(d);
    }

簡單粗暴,不管你是哪個值,統統創建新的包裝類型對象,Float一模一樣。
好了,到現在,再回過頭去看看之前的測試用例,你就明白爲啥同樣是40的值,Integer就是true,而Double就是false,原因就是Integer的裝箱採用了緩存策略,但是Double沒有作處理。
然後是最後一個測試用例的解釋:爲啥我在比較兩個值相等的對象時,爲false,但是我再後者加了一個值爲0的對象時,再比較就是true了呢,真的是不明覺厲呀,於是,就又引出了一個問題,何時會發生裝箱和拆箱呢?

裝箱拆箱的時機

  • 賦值
  • 包裝類型計算
  • ….

其實這個時機是很多的,怎麼解釋呢,明白原理之後,其實你就知道只要是遇到需要將基本類型轉換爲包裝類型時就需要裝箱,將包裝類型轉換爲基本類型時就需要拆箱。秉持着這個原則就不會有啥疑問了。
這時,我們再看剛纔測試用例的最後一個,比較的代碼是i12 == i13 + integer0,首先看等號後面,兩個Integer對象相加時,肯定是沒法直接相加的,所以java編譯器就會進行拆箱的過程,將其拆箱爲基本類型int,然後得到相加後的和,這個和仍然是基本類型int,所以在和前者比較時,也沒法比較,再將前者也拆箱爲基本類型int(也就是int和Integer比較時,會將Integer拆箱),然後==號比較2個int的值,發現相等,於是結果爲true。

引發的細節問題

看到這裏,已經對裝箱和拆箱有了比較全面的瞭解,但是我還有個疑問,這樣裝箱拆箱的,對於實際的開發者來說,有啥影響呢?好像我之前不知道這些東西也沒有關係,仍然在做我的工作,知道不知道對我沒啥影響,那我知道這個東西幹嘛?
嘿嘿,首先需要明白的一個問題就是,這樣做肯定是有道理的。那麼道理何在呢?看個例子

    Integer sum = 0;
    //int sum = 0; 一樣?
    for (int i = 0; i < 100; i++) {
        sum += i;
    }

用包裝類聲明的sum在循環迭代時,由於是包裝類,無法直接與基本類型int類型的i相加,所以需要先拆箱,再加,再裝箱,每次相加,都需要進行這個過程,那麼時間效率和用int來聲明相比,就不是一個檔次的了,所以在開發中,要注意這些細節問題。

小小結語

好了,我們回到文章開篇的那個問題,也就是SparseArray、ArrayMap的誕生原因,平常我們在Java中使用的都是HashMap等來保存數據,但是有一個嚴重的問題就是HashMap裏的key以及value都是泛型,那麼就會不可避免的遇到裝箱和拆箱的過程,而這個過程是很耗時的,所以爲了規避裝箱拆箱提高效率,於是誕生了SparseArray等集合類,但是SparceArray效率高也是有條件的,它適用於數據量比較小的場景,而在Android開發中,大部分場景都是數據量比較小的,在數據很大的情況下,當然還是hashmap效率較優,至於具體SparseArray是怎麼實現的,這個我準備單獨弄一篇文章,有興趣的可以關注下。
好了,至此文章也完結了,再說點題外話,不得不說,Java的設計是很優秀的,畢竟一個團隊的心血,這不,今天就學到了一個看似普通的Integer類中的緩存設計思想,所以,小小結語:沒事就看看源碼,說不定你就發現了一個優秀的設計呢!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章