扩容是元素还是数组_Map扩容源码
首先我們運行一段代碼:
此時運行,程序正常,接下來我們將注釋放開:
此時運行發現,OOM了:
為什么new出來HashMap的時候并沒有報OOM,而是在第一次進行put操作的時候才報的OOM?我們來看下map的源碼。
設置數組長度、擴容閾值
創建Map時可以指定元素個數,也可以不指定。先來看下不指定元素個數的構造方法:
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // 設置加載因子為0.75}static final float DEFAULT_LOAD_FACTOR = 0.75f;當進行put操作時,會進行resize擴容操作:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) //初始table為空 n = (tab = resize()).length; //觸發resize操作 //省略... } //省略...}transient Node[] table;看下resize擴容的核心代碼:
final Node[] resize() { Node[] oldTab = table; //初始table為空 int oldCap = (oldTab == null) ? 0 : oldTab.length; //所以oldCap為0 int oldThr = threshold; //初始threshold為0 所以oldThr為0 int newCap, newThr = 0; if (oldCap > 0) { //省略... } else if (oldThr > 0) //省略... else { newCap = DEFAULT_INITIAL_CAPACITY; //設置新數組長度為16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //設置新map可容納元素個數為16*0.75 } if (newThr == 0) { //省略... } threshold = newThr; //為threshold賦值為newThr 代表擴容后的map的可容納元素個數上限 當map中元素個數超過threshold會觸發擴容操作 @SuppressWarnings({"rawtypes","unchecked"}) Node[] newTab = (Node[])new Node[newCap]; table = newTab; //設置table為擴容后的新數組 //省略... return newTab;}static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;如果指定元素個數,例如:
Map<String,String> map = new HashMap<String,String>(17);public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); //加載因子0.75}public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //加載因子0.75 this.threshold = tableSizeFor(initialCapacity); //設置threshold為最接近initialCapacity的2次冪的值}static final float DEFAULT_LOAD_FACTOR = 0.75f;調用tableSizeFor方法為threshold賦值,看下tableSizeFor的代碼:
static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}這個函數是用來對你申請的容量進行處理讓他變成最接近你申請的容量的2次冪的大小,這里注意:假如你申請的容量為0,最后處理的結果會變成1,代表著你最小的容量為1。
我們自己測試一下,tableSizeFor(17)=32:
public static void main(String[] args) { int number = 17; System.out.println(tableSizeFor(number)); //32}public static int tableSizeFor(int number) { int n = number - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}補充:
算術右移(>>)
右移是按最左邊(高位)來補的(即如果是1就補1,如果是0就補0,不改變該位的值)。另一種說法,正數右移高位補0,負數右移高位補1。
邏輯右移(>>>)
不管最左邊一位是0還是1,都補0。另一種說法,無論是正數還是負數,高位通通補0。
同樣的,當put元素時會觸發map的擴容操作。
接下來看下指定元素個數時,Map擴容的核心源碼。
final Node[] resize() { Node[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; //初始table為空 oldCap=0 int oldThr = threshold; //此時threshold已經有值(2的n次冪) int newCap, newThr = 0; if (oldCap > 0) { //省略... } else if (oldThr > 0) newCap = oldThr; //擴容后map的數組長度 else { 省略... } if (newThr == 0) { float ft = (float)newCap * loadFactor; //設置擴容后map的threshold newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node[] newTab = (Node[])new Node[newCap]; table = newTab; //省略... return newTab;}總結一下,如果沒有指定元素個數,則擴容后的數組長度默認為16,如果指定元素個數,則擴容后的數組長度為與指定的元素個數最接近的2次冪的值,比如指定元素個數為17,則數組長度為32。threshold的取值都是固定數組長度*加載因子(0.75,0.75并不是一個絕對的,只是在時間和空間上可能map的效率最高)。
再回到一開始的問題,new的時候沒有OOM但是在put的時候卻OOM了,因為在new的時候只是設置threshold值,而且設置的值還是比最大整數大的2次冪,在擴容的時候,需要分配數組內存,所以OOM了。
數據遷移
上面其實只是分析了resize操作關于設置數組長度、擴容閾值的代碼,真正擴容后數據遷移都省略了,接下來看下數據遷移部分:
final Node[] resize() { //省略... if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { //遍歷老數組 Node e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //如果老數組的某一索引位置沒有鏈表,則將計算該索引位置的元素在新數組的索引位置 else if (e instanceof TreeNode) ((TreeNode)e).split(this, newTab, j, oldCap); else { //如果索引位置有鏈表 Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { //將索引位置的鏈表拆分成loHead->loTail和hiHead->hiTail兩個鏈表,順序保持不變 if (loTail == null) loHead = e; else loTail.next = e; //尾插法 loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; //新數組索引位置放入loHead鏈表 } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; //新數組索引位置+原始數組長度位置放入hiHead鏈表 } } } } } return newTab;}舉例說明下e.hash&oldCap==0來區分該放到loHead還是hiHead鏈表,下面是我的理解:
0 1 0 0 0 8 //假設原數組長度8 即oldCap=80 0 0 1 1 3 j=0 //假設原數組0索引位置存在3 6 12 三個數組成的鏈表0 0 1 1 0 6 j=0 3->6 //經e.hash&oldCap計算 3和6結果均為0 所以組成loHead:3-60 1 1 0 0 12 j=8 [0+8] //而12與8與操作結果不等于0 所以組成hiHead:12在擴容時?將3->6放入新數組的0索引位置,將12放入新數組的8索引位置這樣的好處是不用計算鏈表的每一個元素在新數組對應的索引位置了,同時也保持了元素在鏈表中的順序。
總結
以上是生活随笔為你收集整理的扩容是元素还是数组_Map扩容源码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: EDI电子数据交换
- 下一篇: dbms_xplan之display_c