1. 程式人生 > >Android版資料結構與演算法(五):LinkedHashMap核心原始碼徹底分析

Android版資料結構與演算法(五):LinkedHashMap核心原始碼徹底分析

上一篇基於雜湊表實現HashMap核心原始碼徹底分析 分析了HashMap的原始碼,主要分析了擴容機制,如果感興趣的可以去看看,擴容機制那幾行最難懂的程式碼真是花費了我很大的精力。

 好了本篇我們分析一下HashMap的兒子LinkedHashMap的核心原始碼,提到LinkedHashMap做安卓的同學肯定會想到Lru(Least Recently Used)演算法,Lru演算法就是基於LinkedHashMap來實現的,明白了LinkedHashMap中基於訪問排序邏輯Lru演算法自然就明白了。

 進入正題,原始碼基於android-23。

一、LinkedHashMap中成員變數

 1
/** 2 * A dummy entry in the circular linked list of entries in the map. 3 * The first real entry is header.nxt, and the last is header.prv. 4 * If the map is empty, header.nxt == header && header.prv == header. 5 */ 6 transient LinkedEntry<K, V> header;
7 8 /** 9 * True if access ordered, false if insertion ordered. 10 */ 11 private final boolean accessOrder;

很簡單,就兩個成員變數。不過這裡要明白LinkedHashMap是繼承HashMap也就是HashMap中一些成員變數,方法LinkedHashMap中都是有的,父類的玩意就不提了,這裡只說一下子類自己的。

header雙向迴圈連結串列的頭結點,看註釋The first real entry is header.nxt, and the last is header.prv.翻譯過來就是第一個加入連結串列的結點是header.nxt,最後被加入連結串列的是header.prv。

accessOrder控制連結串列的排序方式,如果是true那麼連結串列節點是基於訪問排序的,什麼是訪問排序?就是我們訪問連結串列中某一節點的時候會將這個結點從連結串列中刪除然後在放入連結串列的尾部,表示使用者最近使用了這個結點,最近被“寵幸”了一次,那好,我把你放入連結串列尾部,連結串列刪除是從頭部刪除的,插入資料是從尾部插入的,如果遇到一些情況要刪除連結串列中節點資料,那麼優先刪除的是連結串列頭部不經常使用的節點資料。如果為false則表示連結串列節點是基於插入排序的,理解起來很簡單,就是平常的插入順序了,先插入的在頭部優先被刪除。

二、LinkedHashMap構造方法

接下來看下構造方法,如下:

   public LinkedHashMap() {
         init();
         accessOrder = false;
     }
 
     public LinkedHashMap(int initialCapacity) {
         this(initialCapacity, DEFAULT_LOAD_FACTOR);
     }
 
      public LinkedHashMap(int initialCapacity, float loadFactor) {
         this(initialCapacity, loadFactor, false);
     }
   
     public LinkedHashMap(          int initialCapacity, float loadFactor, boolean accessOrder) {       super(initialCapacity, loadFactor);        init();        this.accessOrder = accessOrder;    }
    @Override     void init() {        header = new LinkedEntry<K, V>();     }

 以上就是初始化的主要方法,大體上和HashMap差不多,不在細說,主要一點是預設的accessOrder值為false,也就是連結串列節點按照插入排序來排序的,當然我們也可以在初始化的時候指定accessOrder值,比如LruCache中LinkedHashMap初始化的時候accessOrder就指定為true。

三、LinkedHashMap中資料項LinkedEntry

LinkedHashMap中每個資料節點型別為LinkedEntry,LinkedEntry為HashMapEntry子類,我們直接看其原始碼:

 1     static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
 2         LinkedEntry<K, V> nxt;
 3         LinkedEntry<K, V> prv;
 4 
 5         /** Create the header entry */
 6         LinkedEntry() {
 7             super(null, null, 0, null);
 8             nxt = prv = this;
 9         }
10 
11         /** Create a normal entry */
12         LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
13                     LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
14             super(key, value, hash, next);
15             this.nxt = nxt;
16             this.prv = prv;
17         }
18     }

LinkedEntry多了兩個成員變數nxt與prv,分別只向後一個節點與前一個節點,這裡暫且稱呼為前向指標與後向指標,方便理解。

每個資料節點結構類似如下圖所示:

並且稍有經驗就知道了LinkedHashMap中連結串列為雙向迴圈連結串列,其資料結構如下圖所示:

四、LinkedHashMap中put方法

我們會發現LinkedHashMap中並沒有重寫put方法,只是重寫了addNewEntry方法,很好理解,HashMap與LinkedHashMap二者資料結構都不一樣,肯定無法共用同一個put方法,這裡LinkedHashMap重寫了addNewEntry方法根據自己需要放入資料即可,至於hash值,index等父類已經幫我算好了,直接繼承傳遞過來用就可以了,接下來我們分析LinkedHashMap中addNewEntry方法,原始碼如下:

 1  @Override 
 2  void addNewEntry(K key, V value, int hash, int index) {
 3         LinkedEntry<K, V> header = this.header;
 4 
 5         // Remove eldest entry if instructed to do so.
 6         LinkedEntry<K, V> eldest = header.nxt;
 7         if (eldest != header && removeEldestEntry(eldest)) {
 8             remove(eldest.key);
 9         }
10 
11         // Create new entry, link it on to list, and put it into table
12         LinkedEntry<K, V> oldTail = header.prv;
13         LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
14                 key, value, hash, table[index], header, oldTail);
15         table[index] = oldTail.nxt = header.prv = newTail;
16  }

3行,獲取連結串列的頭結點header。

6行,獲取連結串列中最先被加入的資料節點eldest,也就是最老的資料節點,位於隊頭。

7-9行,判斷最老的資料節點eldest與header是否相等以及removeEldestEntry(eldest)方法是否返回true,如果二者均為true則刪除最老的資料節點。

什麼情況下eldest與header是否相等?很簡單就是連結串列剛剛建立的時候啊,只有一個header節點,nxt與prv指標均指向自己。

removeEldestEntry(eldest)方法原始碼如下:

1     protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
2         return false;
3     }

看到了吧,超級簡單,直接返回false,也就是預設是不會刪除最老的節點的。

12行,獲取oldTail,這裡oldTail是連結串列中最後被插入的資料節點,也就是最新的資料,位於連結串列最尾部。

13行,建立一個新的資料節點newTail,看這名字就知道了位於連結串列尾部,翻譯過來就是:新的尾巴。

 新節點的nxt指標指向header,prv指標指向oldTail,也就是加入之前連結串列的尾部,13,14行執行完連結串列圖示如下:紅色部分為即將加入連結串列的節點。

15行,oldTail的nxt指標指向newTail,連結串列的頭結點header的prv指標指向newTail節點,此時連結串列結構如圖所示:

此時,新的資料節點(紅色部分)就已經插入連結串列了,最新插入的資料位於連結串列尾部。

以上就是LinkedHashMap放入資料的核心邏輯,其實很簡單,就是操作雙向連結串列而已。

接下來,我們分析get方法,看看怎麼實現訪問排序的。

五、LinkedHashMap中get方法

再講get方法之前我們稍微回顧一下addNewEntry方法的13-15行,這幾行中有個table[index]沒有提到,其實上面我只是將雙向迴圈連結串列提取出來講放入資料的邏輯,這樣理解起來比較簡單,而LinkedHashMap中隱藏了HashMap中的單向連結串列,全部展示出其資料結構如圖所示:

是不是看著亂了很多,如果一開始我就丟擲此圖,估計很多就蒙圈了,除去紅色的線其就是一個雙向迴圈連結串列,為什麼這時候要丟擲這個圖呢?大家想一下我們如果要get一個數據沒有單向連結串列的話很自然從header節點開始挨個遍歷整個連結串列就完了,和LinkedList演算法就很像了,顯然效率低下,這裡有單向連結串列,我們只需要算出將要獲取的資料在table陣列的哪一行,只需要遍歷那一行單向連結串列就完了,效率自然提升很多,這也是LinkedHashMap中存在單向連結串列的意義所在。

接下來我們分析get原始碼:

 1  @Override 
 2  public V get(Object key) {
 3         /*
 4          * This method is overridden to eliminate the need for a polymorphic
 5          * invocation in superclass at the expense of code duplication.
 6          */
 7         if (key == null) {
 8             HashMapEntry<K, V> e = entryForNullKey;
 9             if (e == null)
10                 return null;
11             if (accessOrder)
12                 makeTail((LinkedEntry<K, V>) e);
13             return e.value;
14         }
15 
16         int hash = Collections.secondaryHash(key);
17         HashMapEntry<K, V>[] tab = table;
18         for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
19                 e != null; e = e.next) {
20             K eKey = e.key;
21             if (eKey == key || (e.hash == hash && key.equals(eKey))) {
22                 if (accessOrder)
23                     makeTail((LinkedEntry<K, V>) e);
24                 return e.value;
25             }
26         }
27         return null;
28     }

7-14行我們不做詳細分析,就是獲取key的null的情況,比較簡單,自己看看就行了。

16行,計算key的二次hash值,上一篇分析HashMap的時候已經分析過,不在分析。

17行,獲取table陣列。

18-26行,就是遍歷key所在行的單向連結串列,21行如果連結串列中有此資料則執行24行邏輯返回對應資料,如果迴圈整個所在行單向連結串列都沒有那麼執行27行邏輯返回null,表明連結串列中沒有我們要獲取的資料。

22-23行,核心所在,我們說LinkedHashMap可以控制資料是插入排序還是訪問排序,這裡get方法顯示就是對資料的訪問,如果我們設accessOrder為true,表明我們想讓LinkedHashMap資料基於訪問排序,則執行makeTail方法。

接下來我們看下makeTail都做了什麼。

六、LinkedHashMap中訪問排序的實現

直接分析makeTail方法原始碼:

 1  private void makeTail(LinkedEntry<K, V> e) {
 2         // Unlink e
 3         e.prv.nxt = e.nxt;
 4         e.nxt.prv = e.prv;
 5 
 6         // Relink e as tail
 7         LinkedEntry<K, V> header = this.header;
 8         LinkedEntry<K, V> oldTail = header.prv;
 9         e.nxt = header;
10         e.prv = oldTail;
11         oldTail.nxt = header.prv = e;
12         modCount++;
13     }

 又是對連結串列的操作,而且還是雙向連結串列,很多同學估計一看就發愁了,靜下心來,其實沒那麼難。

假設原連結串列如圖所示:

 

 此時訪問資料①,我們看下makeTail是如何處理資料①的。

3行,將e所在節點的prv指標指向的節點的nxt指標指向e所在節點的nxt指標指向的節點,真是拗口。

4行,同理。

其實3,4行邏輯就是將e所在節點從連結串列中斷開,執行完3,4行邏輯,圖示如下:  

 主要資訊圖中已經體現,不在過多解釋。

7,8行分別獲取header與oldTail節點。

9行,將e所在節點的nxt指標指向header節點。

10行,將e所在節點的prv指標指向oldTail節點。

9,10行執行完,資料結構圖示如下:

11行,oldTail所在節點的nxt指標指向e,header所在節點的prv指標指向e,11行完圖示如下:

這樣e所在節點就插入了連結串列的尾部,成為最新的資料。

makeTail方法就是將我們訪問的資料通過調整指標的指向來將訪問的節點調整到佇列的尾部,成為最新的資料。是不是很簡單?

七、總結

到此我想講的就都完了,本篇希望你掌握LinkedHashMap的資料結構,記住有個單向連結串列啊,不僅僅是雙向連結串列,否則get方法的邏輯你是看不懂的。

此外,掌握訪問排序到底怎麼實現的,其實很簡單,就是對雙向連結串列的操作。

好了,本篇到此結束,希望對你有用,真好!!!!!!,咱們下篇見。