我的理解: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