文章目錄
前言
今天咱們來學習Java中較爲經常使用的集合類 HashMap。
另外說明一下,本文的 HashMap 源碼是基於Jdk1.8版本的,若是沒有特別說明的話,以後的集合類源碼解析都是1.8的版本。html
HashMap的數據結構
打開HashMap源碼文件,能夠看到它是繼承自 AbstractMap,並實現了Java集合的根接口Map,以及Cloneable和Serializable接口,因此HashMap能夠被序列化。
HashMap的底層結構是哈希表的具體實現,經過相應的哈希運算就能夠很快查詢到目標元素在表中的位置,擁有很快的查詢速度,所以,HashMap被普遍應用於平常的開發中。理想的狀況就是一個元素對應一個Hash值,這樣的查詢效果是最優的。web
但實際這是不可能的,由於哈希表存在「hash (哈希) 衝突「 的問題。當發生hash衝突時,HashMap採用 「拉鍊法「 進行解決,也就是數組加鏈表的結構。在HashMap的代碼註釋中,數組中的元素用 「bucket」 (中文讀做 桶) 來稱呼,而哈希函數的做用就是將key尋址到buckets中的一個位置,若是一個 bucket 有多個元素,那麼就以鏈表的形式存儲(jdk1.8以前單純是這樣)。面試
這是HashMap的存儲結構圖:
segmentfault
關於 「拉鍊法」 和 「hash衝突」 的知識點有疑問的讀者能夠看下我以前的文章 數據結構:哈希表以及哈希衝突的解決方案 。
爲了方便,下文的 「bucket「 都用 「桶「 替代。數組
深刻源碼
兩個參數
在具體學習源碼以前,咱們須要先了解兩個HashMap中的兩個重要參數,「初識容量」 和 「加載因子」,安全
初識容量是指數組的數量。加載因子則決定了 HashMap 中的元素在達到多少比例後能夠擴容 (rehash),當HashMap的元素數量超過了加載因子與當前容量的乘積後,就須要對哈希表作擴容操做。數據結構
在HashMap中,加載因子默認是0.75,這是結合時間、空間成本均衡考慮後的折中方案,由於 加載因子太大的話發生衝突的可能性會變大,查找的效率反而低;過小的話頻繁rehash,下降性能。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小 rehash 操做次數。若是初始容量大於最大條目數除以加載因子,則不會發生 rehash 操做。app
成員變量
好了,前面說了那麼多,如今開始深刻源碼學習吧,先了解一下HashMap的主要的成員變量:svg
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; static final int MAXIMUM_CAPACITY = 1 << 30; static final float DEFAULT_LOAD_FACTOR = 0.75F; static final int TREEIFY_THRESHOLD = 8; static final int MIN_TREEIFY_CAPACITY = 64; transient HashMap.Node<K, V>[] table; transient Set<Entry<K, V>> entrySet; transient int size; int threshold; final float loadFactor;
能夠看出,HashMap主要的成員變量比較多,有些變量還初始化了值,下面一個個來作解釋。函數
DEFAULT_INITIAL_CAPACITY:默認初識容量 1 << 4 ,也就是16,必須是2的整數次方。
DEFAULT_LOAD_FACTOR:默認加載因子,大小爲0.75。
MAXIMUM_CAPACITY:最大容量, 2^ 30 次方。
TREEIFY_THRESHOLD :樹形閾值,大於這個數就要樹形化,也就是轉成紅黑樹。
MIN_TREEIFY_CAPACITY:樹形最小容量。
table:哈希表的連接數組,對應桶的下標。
entrySet:鍵值對集合。
size:鍵值對的數量,也就是HashMap的大小。
threshold:閾值,下次須要擴容時的值,等於 容量*加載因子。
loadFactor:加載因子。
介紹玩變量,下面介紹HashMap的構造方法。
四個構造方法
HashMap共有四個構造方法,代碼以下:
//加載默認大小的加載因子 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } //加載默認大小的加載因子,並建立一個內容爲參數 m 的內容的哈希表 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; //添加整個集合 putMapEntries(m, false); } //指定容量和加載因子 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; //根據指定容量設置閾值 this.threshold = tableSizeFor(initialCapacity); } //指定容量,加載因子默認大小 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
不難發現,上面第三個構造函數能夠自定義加載因子和容量,首先判斷傳入的加載因子是否符合要求,而後根據制定的容量執行 tableSizeFor() 方法,它會根據容量來指定閾值,爲什麼要多這一步呢?
由於buckets數組的大小約束對於整個HashMap都相當重要,爲了防止傳入一個不是2次冪的整數,必需要有所防範。tableSizeFor()
函數會嘗試修正一個整數,並轉換爲離該整數最近的2次冪。
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; }
好比傳入一個整數244,通過位移,或運算後會返回最近的2次冪 256
插入數據的方法:put()
在集合中最經常使用的操做是存儲數據,也就是插入元素的過程,在HashMap中,插入數據用的是 put() 方法。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
put方法沒有作多餘的操做,只是傳入 key 和 value 還有 hash 值 進入到 putVal方法中並返回對應的值,點擊進入方法,一步步跟進源碼:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //哈希表若是爲空,就作擴容操做 resize() if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //要插入位置沒有元素,直接新建一個包含key的節點 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //若是要插入的桶已經有元素,替換 else { Node<K,V> e; K k; //key要插入的位置發生碰撞,讓e指向p if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //沒碰撞,可是p是屬於紅黑樹的節點,執行putTreeVal()方法 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //p是鏈表節點,遍歷鏈表,查找並替換 else { //遍歷數組,若是鏈表長度達到8,轉換成紅黑樹 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 找到目標節點,退出循環,e指向p if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 節點已存在,替換value,並返回舊value if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //若是超出閾值,就得擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
代碼看上去有點複雜,參數有點亂,但理清邏輯後容易理解多了,源碼大概的邏輯以下:
- 先調用 hash() 方法計算哈希值
- 而後調用 putVal() 方法中根據哈希值進行相關操做
- 若是當前 哈希表內容爲空,作擴容
- 若是要插入的桶中沒有元素,新建個節點並放進去
- 不然從要插入的桶中第一個元素開始查找(這裏爲何是第一個元素,下面會講到)
- 若是沒有碰撞,賦值給e,結束查找
- 有碰撞,並且當前採用的仍是 紅黑樹的節點,調用 putTreeVal() 進行插入
- 鏈表節點的話從傳統的鏈表數組中查找、找到賦值給e,結束
- 若是鏈表長度達到8,轉換成紅黑樹
- 最後檢查是否須要擴容
put方法的代碼中有幾個關鍵的方法,分別是:
- hash():哈希函數,計算key對應的位置
- resize():擴容
- putTreeVal():插入紅黑樹的節點
- treeifyBin():樹形化容器
前面兩個是HashMap的桶鏈表操做的核心方法,後面的方法是Jdk1.8以後有關紅黑樹的操做,後面會講到,先來看前兩個方法。
哈希函數:hash()
hash() 方法是HashMap 中的核心函數,在存儲數據時,將key傳入中進行運算,得出key的哈希值,經過這個哈希值運算才能獲取key應該放置在 「桶」 的哪一個位置,下面是方法的源碼:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
從源碼中能夠看出,傳入key以後,hash() 會獲取key的hashCode進行無符號右移 16 位,而後進行按位異或,並把運算後的值返回,這個值就是key的哈希值。這樣運算是爲了減小碰撞衝突,由於大部分元素的hashCode在低位是相同的,不作處理的話很容易形成衝突。
以後還須要把 hash() 的返回值與table.length - 1
作與運算,獲得的結果便是數組的下標(爲何這麼算,下面會說),在上面的 putVal() 方法中就能夠看到有這樣的代碼操做,舉個例子圖:
table.length - 1
就像是一個低位掩碼(這個設計也優化了擴容操做的性能),它和hash()
作與操做時必然會將高位屏蔽(由於一個HashMap不可能有特別大的buckets數組,至少在不斷自動擴容以前是不可能的,因此table.length - 1
的大部分高位都爲0),只保留低位,這樣一來就老是隻有最低的幾位是有效的,就算你的hashCode()
實現得再好也難以免發生碰撞。這時,hash()
函數的價值就體現出來了,它對hash code的低位添加了隨機性而且混合了高位的部分特徵,顯著減小了碰撞衝突的發生。
另外,在putVal方法的源碼中,咱們能夠看到有這樣一段代碼
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
上面的註釋也說明了,這是檢測要插入位置是否有元素,沒有的話直接新建一個包含key的節點,那麼這裏爲何要用 i = (n - 1) & hash
做爲索引運算呢?
下面這段解釋摘自http://www.importnew.com/29724.html
這實際上是一種優化手段,因爲數組的大小永遠是一個2次冪,在擴容以後,一個元素的新索引要麼是在原位置,要麼就是在原位置加上擴容前的容量。這個方法的巧妙之處全在於&運算,以前提到過&運算只會關注n
– 1(n =
數組長度)的有效位,當擴容以後,n的有效位相比以前會多增長一位(n會變成以前的二倍,因此確保數組長度永遠是2次冪很重要),而後只須要判斷hash在新增的有效位的位置是0仍是1就能夠算出新的索引位置,若是是0,那麼索引沒有發生變化,若是是1,索引就爲原索引加上擴容前的容量。
效果圖以下:
這樣在每次擴容時都不用從新計算hash,省去了很多時間,並且新增有效位是0仍是1是帶有隨機性的,以前兩個碰撞的Entry又有可能在擴容時再次均勻地散佈開,真可謂是很是精妙的設計。
動態擴容:resize()
在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; //‘桶’數組的大小超過0,作擴容 if (oldCap > 0) { //超過最大值不會擴容,把閾值設置爲int的最大數 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //向左移動1位擴大爲原來2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //舊數組大小爲0,舊閾值>0,說明以前建立了哈希表但沒有添加元素,初始化容量等於閾值 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults //舊容量、舊閾值都是0,說明還沒建立哈希表,容量爲默認容量,閾值爲 容量*加載因子 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //新閾值尚未值,從新根據新的容量newCap計算大小 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //不爲空,表明是擴容操做 if (oldTab != null) { //遍歷舊數組的每個‘桶’,移動到新數組newTab for (int j = 0; j < oldCap; ++j) { Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order //普通鏈表節點,遍歷鏈表,並將鏈表節點按原順序進行分組 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { 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; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
上面的源碼有點長,但整體邏輯就三步:
- 計算新桶數組的容量大小 newCap 和新閾值 newThr
- 根據計算出的 newCap 建立新的桶數組,並初始化桶的數組table
- 將鍵值對節點從新映射到新的桶數組裏。若是節點是 TreeNode 類型,則須要拆分成黑樹 (調用**split()**方法 )。若是是普通節點,則節點按原順序進行分組。
前面兩步的邏輯比較簡單,這裏很少敘述。重點是第三點,涉及到了紅黑樹的拆分,這是由於擴容後,桶數組變多了,原有的數組上元素較多的紅黑樹就須要從新拆分,映射成鏈表,防止單個桶的元素過多。
紅黑樹的拆分是調用TreeNode.split() 來實現的,這裏不單獨講。放到後面的紅黑樹一塊兒分析。
節點樹化、紅黑樹的拆分
紅黑樹的引進是HashMap 在 Jdk1.8以後最大的變化,在1.8之前,HashMap的數據結構就是數組加鏈表,某個桶的鏈表有可能由於數據過多而致使鏈表過長,遍歷的效率低下,1.8以後,HashMap對鏈表的長度作了處理,當鏈表長度超過8時,自動轉換爲紅黑樹,有效的提高了HashMap的性能。
但紅黑樹的引進也使得代碼的複雜度提升了很多,添加了有關紅黑樹的操做方法。本文只針對這些方法來作解析,不針對紅黑樹自己作展開,想了解紅黑樹的讀者能夠看我以前的文章
數據結構:紅黑樹的結構以及方法剖析 (上) 以及 數據結構:紅黑樹的結構以及方法剖析 (下)
節點樹化
HashMap中的樹節點的代碼用 TreeNode 表示:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); }
能夠看到就是個紅黑樹節點,有父親、左右孩子、前一個元素的節點,還有個顏色值。知道節點的結構後,咱們來看有關紅黑樹的一些操做方法。
先來分析下樹化的代碼:
//將普通的鏈表轉化爲樹形節點鏈表 final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 桶數組容量小於 MIN_TREEIFY_CAPACITY,優先進行擴容而不是樹化 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { //把節點轉換爲樹形節點 TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) //把轉化後的頭節點賦給hd hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) // 樹形節點不爲空,轉換爲紅黑樹 hd.treeify(tab); } } TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); }
上面的代碼並不太複雜,大體邏輯是根據hash表的元素個數判斷是須要擴容仍是樹形化,而後依次調用不一樣的代碼執行。
值得注意的是,在判斷容器是否須要樹形化的標準是鏈表長度須要大於或等於 MIN_TREEIFY_CAPACITY
,前面也說了,它是HashMap的成員變量,初始值是64,那麼爲何要知足這個條件纔會樹化呢?
下面這段摘自http://www.javashuo.com/article/p-hbiddcap-mp.html
當桶數組容量比較小時,鍵值對節點 hash
的碰撞率可能會比較高,進而致使鏈表長度較長。這個時候應該優先擴容,而不是立馬樹化。畢竟高碰撞率是由於桶數組容量較小引發的,這個是主因。容量小時,優先擴容能夠避免一些列的沒必要要的樹化過程。同時,桶容量較小時,擴容會比較頻繁,擴容時須要拆分成黑樹並從新映射。因此在桶容量比較小的狀況下,將長鏈表轉成紅黑樹是一件吃力不討好的事。
因此,HashMap的樹化過程也是儘可能的考慮了容器性能,再看回上面的代碼,鏈表樹化以前是先把節點轉爲樹形節點,而後再調用 treeify() 轉換爲紅黑樹,而且樹形節點TreeNode 繼承自 Node 類,因此 TreeNode 仍然包含 next 引用,原鏈表的節點順序最終經過 next 引用被保存下來。
下面看下轉換紅黑樹的過程:
final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (root == null) { //第一次進入循環,肯定頭節點,而且是黑色 x.parent = null; x.red = false; root = x; } else { //後面進入循環走的邏輯,x 指向樹中的某個節點 K k = x.key; int h = x.hash; Class<?> kc = null; //從根節點開始,遍歷全部節點跟當前節點 x 比較,調整位置, for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) //當比較節點的哈希值比 x 大時, dir 爲 -1 dir = -1; else if (ph < h) //哈希值比 x 小時 dir 爲 1 dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) // 比較節點和x的key dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; //把 當前節點變成 x 的父親 //若是當前比較節點的哈希值比 x 大,x 就是左孩子,不然 x 是右孩子 if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); }
能夠看到,代碼的整體邏輯就是拿樹中的節點與當前節點作比較,進而肯定節點在樹中的位置,具體實現的細節仍是比較複雜的,這裏不一一展開了。
紅黑樹拆分
介紹了節點的樹化後,咱們來學習下紅黑樹的拆分過程,HashMap擴容後,普通的節點須要從新映射,紅黑樹節點也不例外。
在將普通鏈表轉成紅黑樹時,HashMap 經過兩個額外的引用 next 和 prev 保留了原鏈表的節點順序。這樣再對紅黑樹進行從新映射時,徹底能夠按照映射鏈表的方式進行。這樣就避免了將紅黑樹轉成鏈表後再進行映射,無形中提升了效率。
下面看一下拆分的方法源碼:
//map 容器自己 //tab 表示保存桶頭結點的哈希表 //index 表示從哪一個位置開始修剪 //bit 要修剪的位數(哈希值) final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order // 修剪後的兩個鏈表,下面用lo樹和hi樹來替代 TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0; for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; //若是當前節點哈希值的最後一位等於要修剪的 bit 值,用於區分位於哪一個桶 if ((e.hash & bit) == 0) { //把節點放到lo樹的結尾 if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } //把當前節點放到hi樹 else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } if (loHead != null) { // 若是 loHead 不爲空,且鏈表長度小於等於 6,則將紅黑樹轉成鏈表 if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { tab[index] = loHead; /* * hiHead != null 時,代表擴容後, * 有些節點不在原位置上了,須要從新樹化 */ if (hiHead != null) // (else is already treeified) loHead.treeify(tab); } } //與上面相似 if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } }
源碼的邏輯大概是這樣:拆分後,將紅黑樹拆分紅兩條由 TreeNode 組成的鏈表(hi樹和lo樹)。若是鏈表長度小於 UNTREEIFY_THRESHOLD,則將鏈表轉換成普通鏈表。不然根據條件從新將 TreeNode 鏈表樹化。這裏用兩張圖來展現一下拆分先後的變化
紅黑樹拆分前:
拆分後:
至此,有關紅黑樹的一些轉換操做就介紹完畢了,除此以外,hashMap還提供了不少操做紅黑樹的方法,原理都差很少,讀者們能夠本身去研究。
總結
HashMap的源碼解析就告一段落了,最後,總結一下HashMap的一些特性:
一、HashMap 容許 key, value 爲 null;
二、HashMap源碼裏沒有作同步操做,多個線程操做可能會出現線程安全的問題,建議用Collections.synchronizedMap
來包裝,變成線程安全的Map,例如:
Map map = Collections.synchronizedMap(new HashMap<String,String>());
三、Jdk1.7之前,當HashMap中某個桶的結構爲鏈表時,遍歷的時間複雜度爲O(n),1.8以後,桶中過多元素的話會轉換成了紅黑樹,這時候的遍歷時間複雜度就是O(logn)。
心得
最後,說下心得,老實說,在寫這篇文章以前,我對HashMap只是的瞭解僅僅停留在用過的層面,沒有對源碼作深刻的瞭解,直到心血來潮想學習下Java的集合類纔去看HashMap的源碼,看完源碼後,我被深深的震撼了,說實話,我沒想過平時最多見的工具類的源碼是這麼複雜,一個HashMap就涉及到了如此衆多的技術知識,好比紅黑樹,鏈表轉換,hash運算等,經過簡單的代碼就整合了這些知識點,並且還保證了HashMap的高效性能。說實話,我對設計者是很是佩服的,估計今生我都寫不出如此牛逼的代碼。我也終於能理解爲何那麼多公司面試時很喜歡問集合類的底層實現了,由於集合中涉及的技術知識是很是高深的,若能吃透集合類的源碼,那人能不NB嗎?
最後,感謝這幾位大神的技術文章
https://blog.csdn.net/u011240877/article/details/53351188
http://www.javashuo.com/article/p-hbiddcap-mp.html
http://www.importnew.com/29724.html
本文分享 CSDN - 鄙人薛某。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。