個人博客網:https://wushaopei.github.io/ (你想要這里多有)
Java 中提供了很多的集合類,包括,collection的子接口list、set,以及map等。由於它們的底層構成不同,以及數據的構造為單列、多列、可重復、不可重復,導致其擴容機制也不盡相同。
一、List
獲取ArrayList 容量大小的方法:
1、ArrayList
ArrayList以數組的形式存儲。ArrayList中與容量有關的構造器有兩個,代碼如下:
由以上代碼可知,ArrayList 的容量擴容包含有 ArrayList(int initialCapacity) ,另一個則是 ArrayList() ;
第一個構造器在初始創建時以形參將容量大小進行定義,由外部自定義;第二個構造器沒有傳入參數,使用默認初始化的容量大小;詳細解析如下:
第一個有參構造器:自定長度在后面,這里先看無參有默認容量長度的構造器怎么實現的擴容?
第二個無參構造器:
代碼測試:
在上述單元測試中,創建了一個無參構造的ArrayList 集合,並對其進行元素的添加,以滿足對元素添加過程中容量大小的擴張規則。
上述是單元測試結果,在這里做一番分析:
在以上的代碼中,默認使用ArrayList的初始化容量長度,在ArrayList中,默認初始化容量是 10 ,
從測試結果可以看到,當元素的長度為3時,其容量為默認的值 10 ; 當添加的元素超過初始容量后,即 12 > 10 時,集合進行了自動擴容,添加每一個元素都會調用 ensureCapacityInternal(int minCapacity) 方法,在該方法中比較長度是否達到當前容量的邊緣,將比較的結果 覆蓋原來的 minCapacity 參數,該值用於保存當前的容器元素長度;如單元測試中,當元素個數超過默認的 值 10 時,會調用
方法,並接着調用 ensureExplicitCapacity(minCapacity);方法,該方法會對當前集合長度與最大容量值進行比較,並調用 grow(minCapacity) 方法進行位移擴容:
實現ArrayList 集合擴容的代碼如下:
在集合長度超過初始值或當前最大值時,在 grow(int minCapacity) 方法中,進行了擴容,該擴容如代碼所示, 聲明當前集合的最大容量為 oldCapacity , 新增的長度為 oldCapacity 右位移一位數,當容量為 10 時,二進制為 “8020” >> 1 右移后為 “0401” ,十進制值為 5 ,此時將 oldCapacity + 位移后的值 即 10 + 5 = 15 。
此時完成擴容,newCapacity = 15 就是 ArrayList 擴容后的最新容量大小。
根據擴容規則可知,容量的遞增可預計為 0 、10、15、22、33、49,以此類推。
注意: 這里對新的容量 newCapacity 和 當前元素長度進行比較,如果元素添加的比較多,超過當次位移擴容的容量大小,則會 直接使用 hugeCapacity 方法進行擴容,
第一個有參構造器:
單元測試:
① 當聲明的容量小於默認初始值10 時,會在超過初始容量10 后進行擴容,
測試結果:
②超過初始化容量的元素增加:
超過初試容量后,第一次擴容會直接使用當前集合長度作為 oldCapacity 進行擴容
2、LinkedList
鏈表結構,且是是雙向鏈表, 不涉及擴容的問題。
3、Vector
Vector與ArrayList一樣,是存儲在數組結構中。 與擴容機制相關的構造器有三個,分別是
- Vector(int initialCapacity, int capacityIncrement),
- Vector(int initialCapacity)
- Vector()
其實這后兩個最終都是調用了第一個構造器,不過是使用了不同的默認參數,initialCapacity是初始容量,默認是10,capacityIncrement 是每次擴容時候的遞增數量,如果該數值不等於0則每次擴容的時候是原來的二倍,如果該數值大於0,則每次增長的數量等於該數值
比如:
Vector<String> v = new Vector<String>(); // 10 20 ,40 80 ,160,320 的速度遞增
Vector<String> v2 = new Vector<>(5,3);// 5 8 11 14 17 ,20,23 的速度遞增。
他的加載因子也是10
4、Stack
繼承於Vector,所以他的擴容機制等同於Vector的默認情況,也就是2倍速度擴容,加載因子為1
二、Map
獲取Map集合容量大小的方法:
思路:利用反射獲取hashmap里的threshold(擴容上限)除以 負載因子 就得到容器大小了。
1、HashMap 的擴容機制:
在HashMap 中,與容量有關的構造器有三個:
1.1 這里先對 public HashMap( )做單元測試,並進行分析
單元測試結果:
在這里先對單元測試結果進行分析:
首先我們先看一下,單元測試中,創建了一個默認無參的HashMap,此時第一次打印 集合實例的 容量值,得到的結果是 "已占用容量 0",這里對代碼進行分析:
這里是通過反射獲取 HashMap 字節碼對象從而得到運行時的構造器或方法實例及相應的屬性的值,
以上的一條代碼便是獲取 “threshold ” 私有屬性的方法,這里看一看該變量在 HashMap 中的含義:
由注釋我們可以知道,當前字段在 HashMap 用於存儲 key 字段元素的長度,當初始化一個無參構造時,在沒有對其進行分配元素的情況下,該字段是不會被分配值的,默認值表示值為 0 或 為初始化長度 DEFAULT_INITIAL_CAPACITY 。
以上是HashMap中的源碼部分對 默認初始容量的定義: 默認長度為 16 。
以上是為了說明一個問題,在默認定義HashMap 集合容量且沒有添加元素到容器中的情況下, 其容量值 可能是0 ,也可能是初始容量16.
第一條打印結果分析完畢。
以下對HashMap 的 擴容機制進行一番分析,這里會基於單元測試后面的三條打印結果進行分析:
從打印的結果,我們可以看到,第一次打印結果中,集合的實際容量為 16 ,而根據 HashMap 的加載因子來計算,當添加的元素達到長度 0.75 時會進行一次擴容。這里從源碼方面進行分析:
1.2 此處要搞清楚為什么加載因子是0.75?
在以上源碼中,空參構造中,不會對容量大小進行自定義聲明,而是使用默認值 16 ,方法中的成員變量 this.loadFactor 代表當前集合的加載因子,即滿足一定條件進行容量擴張。其中的
這里解決了加載因子為什么是 0.75 的問題,不過,在HashMap提供的構造器中,加載因子也是可以自定義的,就在這里給分析一下:
有上述源碼可以知道, map的容量與加載因子可以同時被自定義聲明,在構造函數中會對容量的合法性進行校驗:
1.3 在這里我們提出一個問題:HashMap中容量的最大值是什么?
我們看看源碼中的這一句,
當容量大於HashMap 定義的私有變量 MAXIMUM_CAPACITY 時,
在HashMap 中對 MAXIMUM_CAPACITY 的值定義的大小是:
從這里可以知道,hashmap的最大容量是 2的30次方。
1.4 Hashmap 的 擴容機制
在 HashMap 在擴容的過程中有幾個問題要注意:
- key 值不可重復與 value 值不可重復的問題?
- 容量長度的擴容機制?
在這里就不對第一個問題進行過多贅述,可以參考 筆者的另一篇博文,
從源碼看: hashset如何保證值不會被重復的 在該博文中對key值和value值不可重復做了詳盡的分析。
添加元素到HashMap 集合中,
進入 put ()方法中,進行新元素的添加:
在上述源碼中,會對傳入的key 進行值重復的校驗,具體使用 key 進行hash運算獲取在 當前 集合中的 hash 值,如下:
在 put 方法中,對元素 key-value 添加到集合容器的操作是在 putVal() 方法中實現的,並且在該方法中調用 resize () 方法對集合容量長度進行擴容。
使用DEBUG分析查看 putVal方法中的 參數,當前 map 集合的長度為 8 ,有八個key ,每個 key 對應一個value
根據源碼我們可以知道,hashmap的底層是由數組和鏈表構成的,如上圖中, Node<K,V> [ ] tab 代表當前map集合的底層結構,該結構由一個Node (鏈表)類型的數組構成。每個新添加的 key-value 會以一個 Node<K,V> 的鏈表節點的形式被添加到 集合 tab 中 。
在添加元素的時候會調用 resize()方法,並在resize() 方法中將新的集合數據存儲到 table 私有變量中:
以下是resize() 的代碼:
在以上源碼中,關於擴容部分的代碼是:
HashMap 中對當前的容量值使用左位移的方式, oldCap << 1 ,將集合容量擴張為當前的2 的N次冪。此時,我們可以知道HashMap 的擴容是新容量為當前的 2的N次方;
1.5 HashMap 集合進行擴容的時機是?
前提: 當前集合集合的鏈表數組中必須有值的情況下,才會觸發擴容機制。
當前的集合元素長度達到了 當前給定集合的容量的0.75倍時,該鏈表數組進行自增左位移,默認為2的N次冪。