轉 Java中集合的擴容策略及實現的對比分析(ArrayList,Vector,HashMap,ArrayMap,SparseArray源碼)

我的理解:ArrayList和Vector用數組存儲,調用私有的grow方法擴容,最終落地到Arrays.copyOf()方法中node

                 HashMap使用鏈地址法解決hash衝突,數組+鏈表儲存,當鏈表容量大到門限值改用紅黑樹存儲進行樹形轉換,門限值=負載因子0.75×桶容量android

 




 

版權聲明:原創不易,轉載前請留言得到做者許可,轉載後標明做者 Troy.Tang 與 原文連接。算法

轉載自: https://blog.csdn.net/Troy_kfrozen/article/details/78889947  數組

侵刪。緩存




 

本文將從源碼角度來分析和對比一下集合擴容相關的知識,涉及到的集合框架有:ArrayList,Vector,HashMap,ArrayMap,SparseArray。下面先從ArrayList開始。安全

  • ArrayList數據結構

    ArrayList是以數組實現的一個集合類,在ArrayList的源碼中能夠看到,全部元素都是被儲存在elementData這個全局的數組變量中,而所謂的擴容也是針對這個數組對象進行操做。具體來講,當一個添加元素的動做,即add或addAll被執行時,都會先調用ensureCapacityInternal(…)方法進行容量預檢,若是當前elementData數組的容量不足以完成本次添加操做便會進行自動擴容。該方法代碼以下:框架

        //這是一個私有方法,ArrayList提供了另外一個public的擴容方法ensureCapacity以知足外界手動擴容的需求
        //其實質也是調用了本方法,在此不作累述
        //minCapacity是指本次添加操做後所須要的數組容量,即elementData.length + newSize
        private void ensureCapacityInternal(int minCapacity) {
                //這個if判斷若是成立,則表明當前ArrayList爲空,而本次添加操做是第一次添加。
                //此時須要將ArrayList的容量擴充至DEFAULT_CAPACITY=10和本次添加操做所要求的minCapacity兩者中的較大者。
                if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
                }
    
                ensureExplicitCapacity(minCapacity);
        }
    
        private void ensureExplicitCapacity(int minCapacity) {
            //操做計數器+1
            modCount++;
    
            // overflow-conscious code
            // 確保須要進行擴容,即當前數組的容量小於本次添加操做所要求的新的總容量
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

     

    在ensureCapacityInternal方法中,一開始會對本次添加操做是否爲第一次添加進行判斷。從源碼:private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 中能夠得知,DEFAULTCAPACITY_EMPTY_ELEMENTDATA變量指代的是一個空的對象數組,這也是經過無參構造函數new出來的ArrayList對象的初始狀態,也即 elementData = {};這種狀況下的第一次擴容,若是所要求的新容量小於10,則會直接擴容至10。函數

    下面繼續看到grow方法,這個方法是ArrayList擴容操做的實現工具

     private void grow(int minCapacity) {
            // overflow-conscious code
            //記錄當前elementData數組的容量
            int oldCapacity = elementData.length;
            //新的容量暫定爲當前容量的1.5倍
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            //若是1.5倍容量仍不知足minCapacity所要求的,則直接將容量定爲minCapacity
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            //若是新的容量超過了Integer.MAX_VALUE - 8,則作最大化處理
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            //將當前數組copy到新的數組中
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    
        private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
        }

     

    最後的擴容操做,Arrays.copyOf方法會新建立一個大小爲newCapacity的數組,而後經過System.arraycopy方法將以前elementData數組中的元素複製到新數組中(起點的index爲0),並將新數組返回給變量elementData完成擴容。

  • Vector

    Vector和ArrayList其實身出同門,都是AbstractList的子類而且都實現了List接口,所提供的功能也基本相同,最大的不一樣在於Vector全部的公有API都是加鎖的,也即Vector是線程安全的。但這也正是它不受歡迎的緣由之一,過重了。。。言歸正傳,咱們來關心一下Vector的擴容操做。其實基本跟ArrayList相同,方法以下:

        private void ensureCapacityHelper(int minCapacity) {
            // overflow-conscious code
            //當前容量不夠,擴之
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

     

    能夠看到,基本只是方法名不同而已,再來看看Vector的grow函數,這裏還真有些不一樣:

    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
    
            //不一樣之處在這裏,MARK
            int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
    
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            elementData = Arrays.copyOf(elementData, newCapacity);
        }

     

    上面代碼塊中MARK出的那一句即是不一樣之處,能夠看出Vector的擴容是先判斷有沒有大於0的capacityIncrement,該變量是經過:

        public Vector(int initialCapacity, int capacityIncrement) {
            super();
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
            this.elementData = new Object[initialCapacity];
            this.capacityIncrement = capacityIncrement;
        }

    這個帶參構造函數傳入的,若是不調用這個構造函數,則capacityIncrement爲0。在capacityIncrement不爲0的狀況下,則每次擴容都暫定擴大capacityIncrement個,反之擴容oldCapacity個,即當前容量的一倍。後續的擴容操做和ArrayList一致,也是經過Arrays.copyOf方法來完成,這裏再也不重複分析。

  • HashMap

    下面來看另外一個熟人–HashMap。不一樣於上面兩個List的實現類,HashMap是一個採用哈希表實現的鍵值對集合,繼承自AbstractMap,實現了Map接口並使用拉鍊法解決Hash衝突,其內部儲存的元素並非在連續內存地址的,而且是無序的。此處咱們只關心其擴容操做的邏輯和實現,先說一下,因爲要從新建立數組,rehash,從新分配元素位置等,HashMap擴容的開銷要比List大不少。下面介紹幾個和擴容相關的成員變量:

    //哈希表中的數組,JDK 1.8以前存放各個鏈表的表頭。1.8中因爲引入了紅黑樹,則也有可能存的是樹的根
        transient Node<K,V>[] table;
    
        //默認初始容量:16,必須是2的整數次方。這樣規定是由於在經過key來肯定元素在table中的index時
        //所用的算法爲:index = (n - 1) & hash,其中n即爲table容量。保證n是2的整數次方就能保證n-1的低位均爲1,
        //這樣便能保留hash(key)獲得的hash值的全部低位,從而保證獲得的index在n範圍內分佈均勻,由於hash算法的結果就是均勻的
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    
        //默認加載因子爲: 0.75,這是在時間、空間兩方面均衡考慮下的結果。
        //這個值過大會致使發生衝突的概率增長,容易造成長鏈表,下降查找效率;過小則會致使頻繁的擴容,下降總體性能。
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
        //閾值,下次須要擴容時的值,等於 容量*加載因子
        int threshold;
    
        //最大容量: 2^30次方
        static final int MAXIMUM_CAPACITY = 1 << 30;
    
        //樹化閾值。JDK 1.8後HashMap對衝突處理作了優化,引入了紅黑樹。
        //當桶中元素個數大於TREEIFY_THRESHOLD時,就須要用紅黑樹代替鏈表,以提升操做效率。此值必須大於2,並建議大於8
        static final int TREEIFY_THRESHOLD = 8;
    
        //非樹化閾值。在進行擴容操做時,桶中的元素可能會減小,這很好理解,由於在JDK1.7中,
        //每個元素的位置須要經過key.hash和新的數組長度取模來從新計算,而1.8中則會直接將其分爲兩部分。
        //而且在1.8中,對於已是樹形的桶,會作一個split操做(具體實現下面會說),在此過程當中,
        //若剩下的樹元素個數少於UNTREEIFY_THRESHOLD,則須要將其非樹化,從新變回鏈表結構。
        //此值應小於TREEIFY_THRESHOLD,且規定最大值爲6
        static final int UNTREEIFY_THRESHOLD = 6;

     

    好了,相關變量介紹完了,接下來開始分析HashMap的擴容函數resize,一個長得很討厭的方法:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //記錄當前數組長度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //記錄當前擴容閾值
        int oldThr = threshold;
        int newCap, newThr = 0;
        //下面一長串if-else是爲了肯定newCap和newThr,即新的容量和擴容閾值
        if (oldCap > 0) {
            //oldCap不爲0,已被初始化過
            if (oldCap >= MAXIMUM_CAPACITY) {
                //當前已是最大容量,不容許再擴容,返回當前哈希表
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //先將oldCap翻倍,若是獲得的值小於最大容量,而且oldCap不小於默認初始值,則將擴容閾值也翻倍,結束
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) 
            // initial capacity was placed in threshold
            // 若構造函數中有傳入initialCapacity,則會暫存在oldThr=threshold變量中。
            // 而後在第一次put操做致使的resize方法中被賦給newCap,這樣作的目的應該是避免污染oldCap從而影響上面那個if的判斷
            // 從這裏也能夠看出HashMap對於所需內存的申請是被延遲到第一次put操做時進行的,而非在構造函數中。
            newCap = oldThr;
        else {               
            // zero initial threshold signifies using defaults
            // Map沒有被初始化,用默認值初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            //若通過計算後,新閾值爲0,則賦值爲新容量和擴容因子的乘積(需考慮邊界條件)
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
    
        /******-----分割線,至此新的容量和擴容因子已肯定------*************/
    
        @SuppressWarnings({"rawtypes","unchecked"})
        //建立一個大小爲newCap的Node<K,V>數組,並賦值給table變量
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            //遍歷擴容前的數組
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    //將原數組中第j個元素賦給e,並將原數組第j位置置空
                    oldTab[j] = null;
                    if (e.next == null)
                        //該元素沒有後續節點,即該位置未發生過hash衝突。則直接將該元素的hash值與新數組的長度取模獲得新位置並放入
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //JDK1.8中,若是該元素是一個樹節點,說明該位置存放的是一顆紅黑樹,則須要對該樹進行分解操做
                        //具體實現後面會討論,這裏split的結果就是分爲兩棵樹(這裏必要時要進行非樹化操做)並分別放在新數組的高段和低段
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //剩下這種狀況就是該位置存放的是一個鏈表,須要說明的是在JDK1.7和1.8中這裏有着不一樣的實現,下面分別討論
    
                        /******--- JDK 1.7版本 starts ---*******/
                        //遍歷鏈表
                        while(null != e) {
                            //將原鏈表中e的下一個元素暫存到next變量中
                            HashMapEntry<K,V> next = e.next;
                            //算出在新數組的index=i,indexFor其實就是e.hash & (newCapacity - 1)
                            int i = indexFor(e.hash, newCapacity);
                            //改變e.next的指向,將新數組該位置上原先的內容(一個鏈表,元素或是null)掛在e的身後,使e成爲這個鏈表的表頭
                            e.next = newTable[i];
                            //將這個e位表頭的新鏈表放回到index爲i的位置中
                            newTable[i] = e;
                            //將以前暫存的原鏈表中的下一個元素賦給e,繼續遍歷原鏈表
                            e = next;
                        }
                        /******--- JDK 1.7版本 ends ---*******/
    
                        /******--- JDK 1.8版本 starts ---*******/
                        //在1.8的實現中,新數組被分紅了高低兩個段,而原鏈表也會被分紅兩個子鏈表,分別放入新數組的高段和低段中
                        //loHead和loTail用於生成將被放入新數組低段的子鏈表
                        Node<K,V> loHead = null, loTail = null;
                        //hiHead和hiTail則用於生成將被放入新數組高段的子鏈表
                        Node<K,V> hiHead = null, hiTail = null;
                        //跟1.7中同樣,next用於暫存原鏈表中e的下一個元素
                        Node<K,V> next;
                        //開始遍歷原鏈表
                        do {
                            next = e.next;
                            //用if中的方法肯定e是該去新數組的高段仍是低段
                            if ((e.hash & oldCap) == 0) {
                                //將e加到將被放入低段的子鏈表的尾部
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                //將e加到將被放入高段的子鏈表的尾部
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
    
                        if (loTail != null) {
                            //將loHead指向的子鏈表放入新數組中index=j的位置
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            ////將hiHead指向的子鏈表放入新數組中index=j+oldCap的位置
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                        /******--- JDK 1.8版本 ends ---*******/
                    }
                }
            }
        }
        return newTab;
    }

     

    能夠看到JDK1.8對resize方法進行了完全的改造,引入紅黑樹結合以前的鏈表勢必會提升在發生hash衝突時的操做效率(紅黑樹能保證在最壞狀況下插入,刪除,查找的時間複雜度都爲O(logN))。

    此外最大的改動即是在擴容的時候對鏈表或樹的處理,在1.7時代,鏈表中的每個元素都會被從新計算在新數組中的index,具體方法仍舊是e.hash對新數組長度作取模操做;而在1.8時代,這個鏈表或樹會被分爲兩部分,咱們暫且稱其爲A和B,若元素的hash值按位與擴容前數組的長度獲得的結果爲0(其實就是判斷hash的某一位是1仍是0,因爲hash值均勻分佈的特性,這個分裂基本能夠認爲是均勻的),則將其接入A,反之接入B。最後保持A的位置不變,即在新數組中仍位於原先的index=j處,而B則去到j+oldCap處。

    其實對於這個改動帶來的好處我理解的不是特別透徹,由於整個過程並無減小計算的次數。目前看到的好處是能夠避免擴容重定向過程當中發生哈希衝突(由於是擴容一倍,因此一個蘿蔔一個坑,不會有衝突),而且不會將鏈表中的元素倒置(考慮極端狀況,就一條鏈表,1.7的方法每次都會將元素插到表頭)。這裏仍是得求教你們,歡迎討論~

    回到resize方法,上面還留了一個尾巴,就是當桶中是樹形結構時的split方法,下面就來看源碼:

     /**
         * Splits nodes in a tree bin into lower and upper tree bins,
         * or untreeifies if now too small. Called only from resize;
         * see above discussion about split bits and indices.
         *
         * @param map the map
         * @param tab the table for recording bin heads
         * @param index the index of the table being split
         * @param bit the bit of hash to split on
         */
        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode節點繼承自LinkedHashMapEntry,在鏈表節點的基礎上擴充了樹節點的功能,譬如left,right,parent
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            // 將樹分爲兩部分這裏的作法和鏈表結構時是類似的
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            //遍歷整棵樹,這裏說明一下,因爲這棵樹是由鏈表treeify生成的,其next指針依舊存在並指向以前鏈表中的後繼節點,
            //所以遍歷時依然能夠按照遍歷鏈表的方式來進行
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                //暫存next
                next = (TreeNode<K,V>)e.next;
                //將e從鏈表中切斷
                e.next = null;
                //與對鏈表的處理相同,若e.hash按位與bit=oldCap結果爲0,則接到低段組的尾部
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                //不然接到高段組的尾部
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }
    
            //若低段不爲空
            if (loHead != null) {
                //若是低段子樹的元素個數小於非樹化閾值,則將該樹進行非樹化,還原爲鏈表
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    //不然的話將低段子樹按照本來在舊數組中的index放入新數組中
                    tab[index] = loHead;
                    //對低段子樹進行樹化調整。這裏有一個優化,若是發現高段子樹爲空,則說明以前樹中的全部元素都被放到了低段子樹中,
                    //也即這已是一棵完整的,調整好了的紅黑樹,不須要再進行樹化調整
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                //與低段子樹一樣的邏輯,放入新數組的位置爲舊數組的index+oldCap
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    //這裏一樣的,若是低段子樹爲空,說明高段這棵樹已是一棵完整的紅黑樹,無需調整
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

    注:因爲篇幅所限,紅黑樹的treeify樹化操做和untreeify非樹化操做將在另外一篇關於紅黑樹的文章中單獨進行說明,在此你們只需理解treeify作的事情是將一個鏈表中的TreeNode節點,按照二叉查找樹的結構鏈接其left,right和parent指針,並根據紅黑樹的規則進行調整,同時也能夠對一個非紅黑樹的樹結構進行調整;而untreeify反之,是將一個紅黑樹的TreeNode節點還原爲HashMap.Node節點,並將其首尾相連還原爲一個鏈表結構。

    至此,HashMap的擴容邏輯和實現就分析完成了,能夠看到1.7中的邏輯和作法還比較簡單粗暴,而到了1.8中因爲紅黑樹的引入,總體變得精巧了許多,總體HashMap的操做性能也有了大的提高。但即使如此,HashMap的擴容依舊是一個很貴的操做,這就要求咱們在初始化HashMap的時候根據本身的業務場景設置儘量合適的初始容量,以下降擴容發生的概率。例如我須要一個容納96個元素的map,那麼只要我把capacity初始值設置爲128,那麼就不會經歷16到32到64再到128的三次擴容,這樣來講是節省內存和運算成本的。固然若是須要容納97個元素的話,由於超過了capacity值的3/4,因此就須要設置爲256了,不然也會經歷一次擴容。

    • ArrayMap

      ArrayMap是一個(key,value)映射的數據結構,它設計上更多的是考慮內存的優化,內部是使用兩個數組進行數據存儲,一個數組記錄key的hash值,另一個數組記錄Value值。它會對key使用二分法進行從小到大排序,在添加、刪除、查找數據的時候都是先使用二分查找法獲得相應的index,而後經過index來進行添加、查找、刪除等操做。相比於HashMap,它更適合在數據量不是很大的狀況下(千級)使用,有點用時間換空間的意思,若是在數據量比較大的狀況下,那麼它的性能將退化至少50%。

      下面仍是先來看看跟ArrayMap擴容相關的成員變量:

       //最小擴容容量,即一次擴容最少擴大4個
              private static final int BASE_SIZE = 4;
      
              //第一個數組,存放全部key的hash值,這個數組的容量就是ArrayMap當前的容量
              int[] mHashes;
      
              //第二個數組,容量是mHashes的兩倍,元素的key和value被相鄰存放,即一個元素佔兩格
              Object[] mArray;
      
              //ArrayMap中當前元素的個數,也即mHashes數組中元素的個數
              int mSize;
      
              //小號緩存數組,數組長度爲8,其中index=0位置存放的是指向下一個緩存數組index=0位置的指針,
              //即該位置存儲的實際上是一個鏈表,鏈表元素爲一個一個的mBaseCache數組,其經過index=0位置相連
              //index=1位置存放的是一個長度爲4的數組,供mHashes使用
              static Object[] mBaseCache;
      
              //小號緩存數組中index=0的位置存放的鏈表的長度
              static int mBaseCacheSize;
      
              //大號緩存數組,數組長度爲16,其中index=0位置存放的是指向下一個緩存數組index=0位置的指針,
              //即該位置存儲的實際上是一個鏈表,鏈表元素爲一個一個的mTwiceBaseCache數組,其經過index=0位置相連
              //index=1位置存放的是一個長度爲8的數組,供mHashes使用
              static Object[] mTwiceBaseCache;
      
              //大號緩存數組中index=0的位置存放的鏈表的長度
              static int mTwiceBaseCacheSize;
      
              //緩存數組中的鏈表的最大長度,不能超過10
              private static final int CACHE_SIZE = 10;

       

      相關成員變量就是這些,先提一句,ArrayMap不一樣於HashMap,若是你在構造函數中指定了capacity,則構造函數會直接爲mHashes和mArray兩個數組申請內存,並不會延遲到第一次put。若是不指定capacity,則兩個數組會被分別賦值爲EmptyArray.INT和EmptyArray.OBJECT,即對於類型的空數組。下面咱們來看擴容的實現:

      /**
              * Ensure the array map can hold at least <var>minimumCapacity</var>
              * items.
              */
              //這個方法在putAll中會被調用,傳入的參數是mSize+array.size(),即當前個數與新put進來的集合個數的和。
              //這個方法並無在put中被調用,但核心方法都是allocArrays(capacity),惟一的差異在於這個capacity的計算方法,
              //因此此處咱們先用它來分析,至於計算capacity的區別後面會提到。
              //minimumCapacity這個輸入參數表示新的操做須要這個容量來支撐,也即新的最小容量
              public void ensureCapacity(int minimumCapacity) {
                  //暫存擴容前的元素個數至osize
                  final int osize = mSize;
                  //若當前ArrayMap的容量小於所需的最小容量,則進行擴容
                  if (mHashes.length < minimumCapacity) {
                      //暫存擴容前的mHashes數組至ohashes
                      final int[] ohashes = mHashes;
                      //暫存擴容前的mArray數組至oarray
                      final Object[] oarray = mArray;
                      //申請內存,核心操做,後面單獨分析
                      allocArrays(minimumCapacity);
                      //若ArrayMap中有元素
                      if (mSize > 0) {
                          //這裏的mHashes是通過擴容後的空數組,這一句是將擴容前暫存的ohashes中的內容copy到新的mHashes中
                          System.arraycopy(ohashes, 0, mHashes, 0, osize);
                          //同理,這裏的mArray是通過擴容後的空數組,這裏將擴容前暫存的oarray中的內容copy到新的mArray中
                          System.arraycopy(oarray, 0, mArray, 0, osize<<1);
                      }
                      //善後工做,該緩存的緩存,該置空的置空
                      freeArrays(ohashes, oarray, osize);
                  }
                  if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize != osize) {
                      throw new ConcurrentModificationException();
                  }
              }

       

      下面繼續看真正幹活兒的allocArrays方法,上面說到的putAll方法會經過ensureCapacity來調用到這個方法,傳入的size爲mSize+array.size()。除此以外,構造函數中根據指定的capacity初始化數組的操做也是經過這個函數完成的,size天然就是指定的capacity。可是最經常使用到的仍是put方法,即添加單個元素,put中調用allocArrays(size)時傳入的size是這樣算出來的:

      final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                      : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

       

      翻譯一下就是先判斷mSize值是否大於等於8,若是是則n=mSize*1.5,若是否,則判斷是否大於等於4,是則n=8個,不然n=4個。

       private void allocArrays(final int size) {
              if (mHashes == EMPTY_IMMUTABLE_INTS) {
                  throw new UnsupportedOperationException("ArrayMap is immutable");
              }
              if (size == (BASE_SIZE*2)) {
                  //線程安全,而且上的是類鎖
                  synchronized (ArrayMap.class) {
                      //若要求的size是8,且大號緩存數組不爲空,則從大號緩存數組中取緩存
                      if (mTwiceBaseCache != null) {
                          //將當前大號緩存數組賦給array,其容量爲16,符合mArray的要求
                          final Object[] array = mTwiceBaseCache;
                          mArray = array;
                          //index=0的位置存放的是指向下一個大號緩存數組的指針,取出賦給mTwiceBaseCache變量,即第一層緩存被mArray取走了
                          mTwiceBaseCache = (Object[])array[0];
                          //第一層緩存的index=1的位置存放的是長度爲8的數組,賦給mHashes
                          mHashes = (int[])array[1];
                          //將已經取出的第一層緩存數組的前兩位都清空,這樣一來便獲得了長度分別爲16和8的空的mArray和mHashes數組,好精巧的設計!
                          array[0] = array[1] = null;
                          //大號緩存被取走了一層,鏈表長度減一
                          mTwiceBaseCacheSize--;
                          if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                                  + " now have " + mTwiceBaseCacheSize + " entries");
                          return;
                      }
                  }
              } else if (size == BASE_SIZE) {
                  //這邊size=4,與上同理,只是操做的緩存對象變成了小號緩存數組
                  synchronized (ArrayMap.class) {
                      if (mBaseCache != null) {
                          final Object[] array = mBaseCache;
                          mArray = array;
                          mBaseCache = (Object[])array[0];
                          mHashes = (int[])array[1];
                          array[0] = array[1] = null;
                          mBaseCacheSize--;
                          if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                                  + " now have " + mBaseCacheSize + " entries");
                          return;
                      }
                  }
              }
      
              //若是要求的size不是4或8,則不知足使用緩存的條件,直接按照要求的size新建立兩個數組
              mHashes = new int[size];
              mArray = new Object[size<<1];
          }

       

      上面緩存的邏輯有點繞,還有一個緩存相關的地方須要分析一下,就是freeArrays這個方法,不要被它的名字迷惑了,它不光是作了回收內存,上面兩個緩存數組的添加就是在這裏完成的,下面來看一下:

      //調用時是這個樣子的,三個參數分別爲擴容前暫存的mHashes,mArrays和mSize
          freeArrays(ohashes, oarray, osize);
      
          private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
              if (hashes.length == (BASE_SIZE*2)) {
                  synchronized (ArrayMap.class) {
                      //若須要回收的容量爲8,而且大號緩存數組的鏈表長度還沒到10
                      if (mTwiceBaseCacheSize < CACHE_SIZE) {
                          //直接將傳入的array數組(顯然,此處長度爲16)做爲新一層的緩存,index=0的位置指向現有的緩存數組
                          array[0] = mTwiceBaseCache;
                          //index=1的位置指向本次傳入的hashes數組,也即將這個hashes緩存在此
                          array[1] = hashes;
                          //將後面14個位置都置空,由於已經無用,避免內存泄漏
                          for (int i=(size<<1)-1; i>=2; i--) {
                              array[i] = null; //for gc
                          }
                          //將這層新作好的緩存數組賦值給mTwiceBaseCache變量,下次allocArray中就能經過mTwiceBaseCache變量取到這一層緩存。這裏緩存層也是後進先出,相似於棧。
                          mTwiceBaseCache = array;
                          //大號緩存數組的鏈表長度加1
                          mTwiceBaseCacheSize++;
                          if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
                              + " now have " + mTwiceBaseCacheSize + " entries");
                      }
                  }
              } else if (hashes.length == BASE_SIZE) {
                  synchronized (ArrayMap.class) {
                      //這邊size=4,與上同理,只是操做的緩存對象變成了小號緩存數組
                      if (mBaseCacheSize < CACHE_SIZE) {
                          array[0] = mBaseCache;
                          array[1] = hashes;
                          for (int i=(size<<1)-1; i>=2; i--) {
                              array[i] = null;
                          }
                          mBaseCache = array;
                          mBaseCacheSize++;
                          if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
                              + " now have " + mBaseCacheSize + " entries");
                      }
                  }
              }
          }

       

      到這裏ArrayMap的擴容機制就分析完畢了,能夠看到ArrayMap內部對於內存的使用進行很大的優化,不只將每次擴容的容量經過4和8分爲三個檔位,更是對於4和8的數組採起了緩存的機制,以免重複建立對象。尤爲是緩存結構的設計不可謂不精妙,看完以後只能感嘆路還很長,本身什麼時候能設計出如此精妙的結構!

      言歸正傳,其實ArrayMap是在Android SDK中存在,專門供Android使用以在數據量不大的場景下替換HashMap的。經過上面對二者擴容機制的分析不難看出其在這方面的區別:HashMap每次擴容是直接申請雙倍於當前容量的內存,而ArrayMap則是根據所需size大小,若是size長度大於8時申請size*1.5個長度,大於4小於8時申請8個,小於4時申請4個。這樣一來同等狀況下,粒度更細的ArrayMap天然會申請更少的內存空間,同時致使的問題就是擴容頻率會大於HashMap。另外因爲ArrayMap在查找元素使用的是二分法,當數據量過大時性能會遠不如用hash值定位的HashMap。但衆所周知內存對於移動設備來講有多麼珍貴,所以ArrayMap這種用時間換空間的作法,在數據量較小的時候是很是適合於Android應用的。

    • SparseArray

      最後再來看一下SparseArray,與ArrayMap相似,這也是Android對於HashMap的一種優化和替代方案,不一樣的是它只能將int做爲key的類型(注意是int不是Integer),也就是說它不會對key進行自動裝箱,這個是當key爲int時SparseArray優於ArrayMap的地方,因此對於數據量不大,且肯定key爲int類型的場景,可使用SparseArray代替HashMap或ArrayMap,由於它避免了自動裝箱的過程。

      SparseArray內部也是經過兩個數組來存儲數據,一個存key,一個存value。因爲key只能是int,因此比較時只須要看是否相等便可,因此結構很是簡單。它的擴容是經過GrowingArrayUtils.growSize(int currentsize)方法來完成的,這個工具類位於android.support.v7.content.res包中,其中輸入參數是指擴容前當前的數組容量,這個方法很是簡單:

        public static int growSize(int currentSize) {
              //當前容量不大於4,就擴容到8,不然就擴容一倍
              return currentSize <= 4 ? 8 : currentSize * 2;
          }

       

    • 最後

      至此幾個經常使用的集合類的擴容機制就都分析完畢了,下面給出一個簡單的表格用於總結:

      容器 單次擴容容量 緩存策略
      ArrayList 默認擴容當前容量的一半,若不知足需求,則擴容至需求容量(此處需考慮邊界狀況,若所需求容量超過了Integer.MAX_VALUE - 8,則作最大化處理)
      Vector 可在構造函數中指定,默認擴容一倍
      HashMap 擴容一倍,並進行rehash
      ArrayMap 所需size長度大於8時申請size*1.5個長度,大於4小於8時申請8個,小於4時申請4個 對長度爲4和8的數組進行緩存
      SparseArray 當前容量不大於4,就擴容到8,不然就擴容一倍

      後續會繼續對這些集合類的查找,添加,刪除操做,線程安全性以及HashMap中的紅黑樹等方面作源碼分析,感謝閱讀!

    • 感謝

      http://blog.csdn.net/u011240877/article/details/53351188

      http://blog.csdn.net/u011240877/article/details/53358305

      http://blog.csdn.net/vansbelove/article/details/52422087

相關文章
相關標籤/搜索