面試必備:HashMap源碼解析(JDK8)


一、前言

在分析jdk1.8后的HashMap源碼時,發現網上好多分析都是基於之前的jdk,而Java8的HashMap對之前做了較大的優化,其中最重要的一個優化就是桶中的元素不再唯一按照鏈表組合,也可以使用紅黑樹進行存儲,總之,目標只有一個,那就是在安全和功能性完備的情況下讓其速度更快,提升性能。好~下面就開始分析源碼。

二、HashMap數據結構

 

說明:上圖很形象的展示了HashMap的數據結構(數組+鏈表+紅黑樹),桶中的結構可能是鏈表,也可能是紅黑樹,紅黑樹的引入是為了提高效率。所以可見,在分析源碼的時候我們不知不覺就溫習了數據結構的知識點,一舉兩得。

三、HashMap源碼分析

3.1 類的繼承關系

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

可以看到HashMap繼承自父類(AbstractMap),實現了Map、Cloneable、Serializable接口。其中,Map接口定義了一組通用的操作;Cloneable接口則表示可以進行拷貝,在HashMap中,實現的是淺層次拷貝,即對拷貝對象的改變會影響被拷貝的對象;Serializable接口表示HashMap實現了序列化,即可以將HashMap對象保存至本地,之后可以恢復狀態。

3.2 類的屬性

 

說明:類的數據成員很重要,以上也解釋得很詳細了,其中有一個參數MINTREEIFYCAPACITY,筆者暫時還不是太清楚,有讀者知道的話歡迎指導。

3.3 類的構造函數

1. HashMap(int, float)型構造函數

說明:tableSizeFor(initialCapacity)返回大於等於initialCapacity的最小的二次冪數值。

 

2. HashMap(int)型構造函數。

 

3. HashMap()型構造函數。

 

4. HashMap(Map)型構造函數。

 

說明:putMapEntries(Map m, boolean evict)函數將m的所有元素存入本HashMap實例中。

 

 

3.4 重要函數分析

1. 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;

   // table未初始化或者長度為0,進行擴容

   if ((tab = table) == null || (n = tab.length) == 0)

       n = (tab = resize()).length;

   // (n - 1) & hash 確定元素存放在哪個桶中,桶為空,新生成結點放入桶中(此時,這個結點是放在數組中)

   if ((p = tab[i = (n - 1) & hash]) == null)

       tab[i] = newNode(hash, key, value, null);

   // 桶中已經存在元素

   else {

       Node<K,V> e; K k;

       // 比較桶中第一個元素(數組中的結點)的hash值相等,key相等

       if (p.hash == hash &&

           ((k = p.key) == key || (key != null && key.equals(k))))

               // 將第一個元素賦值給e,用e來記錄

               e = p;

       // hash值不相等,即key不相等;為紅黑樹結點

       else if (p instanceof TreeNode)

           // 放入樹中

           e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

       // 為鏈表結點

       else {

           // 在鏈表最末插入結點

           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;

               }

               // 判斷鏈表中結點的key值與插入的元素的key值是否相等

               if (e.hash == hash &&

                   ((k = e.key) == key || (key != null && key.equals(k))))

                   // 相等,跳出循環

                   break;

               // 用於遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表

               p = e;

           }

       }

       // 表示在桶中找到key值、hash值與插入元素相等的結點

       if (e != null) {

           // 記錄e的value

           V oldValue = e.value;

           // onlyIfAbsent為false或者舊值為null

           if (!onlyIfAbsent || oldValue == null)

               //用新值替換舊值

               e.value = value;

           // 訪問后回調

           afterNodeAccess(e);

           // 返回舊值

           return oldValue;

       }

   }

   // 結構性修改

   ++modCount;

   // 實際大小大於閾值則擴容

   if (++size > threshold)

       resize();

   // 插入后回調

   afterNodeInsertion(evict);

   return null;

}

 

 

 

 

說明:HashMap並沒有直接提供putVal接口給用戶調用,而是提供的put函數,而put函數就是通過putVal來插入元素的。

2. getNode函數

 

final Node<K,V> getNode(int hash, Object key) {

   Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

   // table已經初始化,長度大於0,根據hash尋找table中的項也不為空

   if ((tab = table) != null && (n = tab.length) > 0 &&

       (first = tab[(n - 1) & hash]) != null) {

       // 桶中第一項(數組元素)相等

       if (first.hash == hash && // always check first node

           ((k = first.key) == key || (key != null && key.equals(k))))

           return first;

       // 桶中不止一個結點

       if ((e = first.next) != null) {

           // 為紅黑樹結點

           if (first instanceof TreeNode)

               // 在紅黑樹中查找

               return ((TreeNode<K,V>)first).getTreeNode(hash, key);

           // 否則,在鏈表中查找

           do {

               if (e.hash == hash &&

                   ((k = e.key) == key || (key != null && key.equals(k))))

                   return e;

           } while ((e = e.next) != null);

       }

   }

   return null;

}

 

 

 

說明:HashMap並沒有直接提供getNode接口給用戶調用,而是提供的get函數,而get函數就是通過getNode來取得元素的。

3. resize函數

 

 
         

 

final Node<K,V>[] resize() {

 

 

   // 當前table保存

 

 

   Node<K,V>[] oldTab = table;

 

 

   // 保存table大小

 

 

   int oldCap = (oldTab == null) ? 0 : oldTab.length;

 

 

   // 保存當前閾值

 

 

   int oldThr = threshold;

 

 

   int newCap, newThr = 0;

 

 

   // 之前table大小大於0

 

 

   if (oldCap > 0) {

 

 

       // 之前table大於最大容量

 

 

       if (oldCap >= MAXIMUM_CAPACITY) {

 

 

           // 閾值為最大整形

 

 

           threshold = Integer.MAX_VALUE;

 

 

           return oldTab;

 

 

       }

 

 

       // 容量翻倍,使用左移,效率更高

 

 

       else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

 

 

           oldCap >= DEFAULT_INITIAL_CAPACITY)

 

 

           // 閾值翻倍

 

 

           newThr = oldThr << 1; // double threshold

 

 

   }

 

 

   // 之前閾值大於0

 

 

   else if (oldThr > 0)

 

 

       newCap = oldThr;

 

 

   // oldCap = 0並且oldThr = 0,使用缺省值(如使用HashMap()構造函數,之后再插入一個元素會調用resize函數,會進入這一步)

 

 

   else {          

 

 

       newCap = DEFAULT_INITIAL_CAPACITY;

 

 

       newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

 

 

   }

 

 

   // 新閾值為0

 

 

   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"})

 

 

   // 初始化table

 

 

   Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

 

 

   table = newTab;

 

 

   // 之前的table已經初始化過

 

 

   if (oldTab != null) {

 

 

       // 復制元素,重新進行hash

 

 

       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;

 

 

                   // 將同一桶中的元素根據(e.hash & oldCap)是否為0進行分割,分成兩個不同的鏈表,完成rehash

 

 

                   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;

 

 

}

 

 

 

 

 

說明:進行擴容,會伴隨着一次重新hash分配,並且會遍歷hash表中所有的元素,是非常耗時的。在編寫程序中,要盡量避免resize。

在resize前和resize后的元素布局如下

 

 說明:上圖只是針對了數組下標為2的桶中的各個元素在擴容后的分配布局,其他各個桶中的元素布局可以以此類推。

四、針對HashMap的思考

4.1. 關於擴容的思考

從putVal源代碼中我們可以知道,當插入一個元素的時候size就加1,若size大於threshold的時候,就會進行擴容。假設我們的capacity大小為32,loadFator為0.75,則threshold為24 = 32 * 0.75,此時,插入了25個元素,並且插入的這25個元素都在同一個桶中,桶中的數據結構為紅黑樹,則還有31個桶是空的,也會進行擴容處理,其實,此時,還有31個桶是空的,好像似乎不需要進行擴容處理,但是是需要擴容處理的,因為此時我們的capacity大小可能不適當。我們前面知道,擴容處理會遍歷所有的元素,時間復雜度很高;前面我們還知道,經過一次擴容處理后,元素會更加均勻的分布在各個桶中,會提升訪問效率。所以,說盡量避免進行擴容處理,也就意味着,遍歷元素所帶來的壞處大於元素在桶中均勻分布所帶來的好處。如果有讀者有不同意見,也歡迎討論~

五、總結

至此,HashMap的源碼就分析到這里了,其中理解了其中的核心函數和數據結構,那么理解HashMap的源碼就不困難了。當然,此次分析中還有一些知識點沒有涉及到,如紅黑樹、序列化、拷貝等,以后有機會會進行詳細的說明和講解,謝謝各位園友的觀看~

出處:https://dwz.cn/QnLsm3RQ


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM