LinkedHashMap源碼解析 給jdk寫註釋系列之jdk1.6容器(5)

前面分析了HashMap的實現,我們知道其底層數據存儲是一個hash表(數組+單向鏈表)。接下來我們看一下另一個LinkedHashMap,它是HashMap的一個子類,他在HashMap的基礎上維持了一個雙向鏈表(hash表+雙向鏈表),在遍歷的時候可以使用插入順序(先進先出,類似於FIFO),或者是最近最少使用(LRU)的順序。
     來具體看下LinkedHashMap的實現。
     
1.定義 
1 public class LinkedHashMap<K,V>
2     extends HashMap<K,V>
3     implements Map<K,V>
  從定義可以看到LinkedHashMap繼承於HashMap,且實現了Map接口。這也就意味着HashMap的一些優秀因素可以被繼承下來,比如hash尋址,使用鏈表解決hash衝突等實現的快速查找,對於HashMap中一些效率較低的內容,比如容器擴容過程,遍歷方式,LinkedHashMap是否做了一些優化呢。繼續看代碼吧。
 
2.底層存儲
 
     開篇我們說了LinkedHashMap是基於HashMap,並在其基礎上維持了一個雙向鏈表,也就是說LinkedHashMap是一個hash表(數組+單向鏈表) +雙向鏈表的實現,到底實現方式是怎麼樣的,來看一下:
複製代碼
 1     /**
 2      * The head of the doubly linked list.
 3      */
 4     private transient Entry<K,V> header ;
 5 
 6     /**
 7      * The iteration ordering method for this linked hash map: <tt>true</tt>
 8      * for access -order, <tt> false</tt> for insertion -order.
 9      *
10      * @serial
11      */
12     private final boolean accessOrder;
複製代碼
  看到了一個無比熟悉的屬性header,它在LinkedList中出現過,英文註釋很明確,是雙向鏈表的頭結點對不對。再看accessOrder這個屬性,true表示最近較少使用順序,false表示插入順序。當然你說怎麼沒看到數組呢,別忘了LinkedHashMap繼承於HashMap,Entry[]這個東東就不用寫了吧。。。
 
     再來看下Entry這個節點類和HashMap中的有什麼不同。
複製代碼
 1     /**
 2      * LinkedHashMap entry.
 3      */
 4     private static class Entry<K,V> extends HashMap.Entry<K,V> {
 5         // These fields comprise the doubly linked list used for iteration.
 6         // 雙向鏈表的上一個節點before和下一個節點after
 7         Entry<K,V> before, after ;
 8 
 9        // 構造方法直接調用父類HashMap的構造方法(super)
10        Entry( int hash, K key, V value, HashMap.Entry<K,V> next) {
11             super(hash, key, value, next);
12         }
13 
14         /**
15          * 從鏈表中刪除當前節點的方法
16          */
17         private void remove() {
18             // 改變當前節點前後兩個節點的引用關係,當前節點沒有被引用後,gc可以回收
19             // 將上一個節點的after指向下一個節點
20             before.after = after;
21             // 將下一個節點的before指向前一個節點
22             after.before = before;
23         }
24 
25         /**
26          * 在指定的節點前加入一個節點到鏈表中(也就是加入到鏈表尾部)
27          */
28         private void addBefore(Entry<K,V> existingEntry) {
29             // 下面改變自己對前後的指向
30             // 將當前節點的after指向給定的節點(加入到existingEntry前面嘛)
31             after  = existingEntry;
32             // 將當前節點的before指向給定節點的上一個節點
33             before = existingEntry.before ;
34 
35             // 下面改變前後最自己的指向
36             // 上一個節點的after指向自己
37             before.after = this;
38             // 下一個幾點的before指向自己
39             after.before = this;
40         }
41 
42         // 當向Map中獲取查詢元素或修改元素(put相同key)的時候調用這個方法
43         void recordAccess(HashMap<K,V> m) {
44             LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
45             // 如果accessOrder爲true,也就是使用最近較少使用順序
46             if (lm.accessOrder ) {
47                 lm. modCount++;
48                 // 先刪除,再添加,也就相當於移動了
49                 // 刪除當前元素
50                 remove();
51                 // 將當前元素加入到header前(也就是鏈表尾部)
52                 addBefore(lm. header);
53             }
54         }
55 
56         // 當從Map中刪除元素的時候調動這個方法
57         void recordRemoval(HashMap<K,V> m) {
58             remove();
59         }
60 }
複製代碼

  可以看到Entry繼承了HashMap中的Entry,但是LinkedHashMap中的Entry多了兩個屬性指向上一個節點的before和指向下一個節點的after,也正是這兩個屬性組成了一個雙向鏈表。等等。。。Entry還有一個繼承下來的next屬性,這個next是單向鏈表中用來指向下一個節點的,怎麼回事嘛,怎麼又是單向鏈表又是雙向鏈表呢,都要暈了對不對,其實想的沒錯,這裏的節點即是Hash表中的單向鏈表中的一個節點,它又是LinkedHashMap維護的雙向鏈表中的一個節點,是不是瞬間覺得高大上了。圖解一下吧(不要告訴我圖好亂,我看不懂。。。)

  注:黑色箭頭指向表示單向鏈表的next指向,紅色箭頭指向表示雙向鏈表的before指向,藍色箭頭指向表示雙向鏈表的after指向。另外LinkedHashMap種還有一個header節點是不保存數據的,這裏沒有畫出來。
     
     從上圖可以看出LinkedHashMap仍然是一個Hash表,底層由一個數組組成,而數組的每一項都是個單向鏈表,由next指向下一個節點。但是LinkedHashMap所不同的是,在節點中多了兩個屬性before和after,由這兩個屬性組成了一個雙向循環鏈表(你怎麼知道是循環,下面在說嘍),而由這個雙向鏈表維持着Map容器中元素的順序。看下Entry中的recordRemoval方法,該方法將在節點被刪除時候調用,Hash表中鏈表節點被正常刪除後,調用該方法修正由於節點被刪除後雙向鏈表的前後指向關係,從這一點來看,LinkedHashMap比HashMap的add、remove、set等操作要慢一些(因爲要維護雙向鏈表 )
     
     明白了LinkedHashMap的底層存儲結構後,我們來看一下它的構造方法以及怎麼樣對鏈表進行初始化的。
 
3.構造方法
複製代碼
 1 /**
 2      * 構造一個指定初始容量和加載因子的LinkedHashMap,默認accessOrder爲false
 3      */
 4     public LinkedHashMap( int initialCapacity, float loadFactor) {
 5         super(initialCapacity, loadFactor);
 6         accessOrder = false;
 7     }
 8 
 9     /**
10      * 構造一個指定初始容量的LinkedHashMap,默認accessOrder爲false
11      */
12     public LinkedHashMap( int initialCapacity) {
13         super(initialCapacity);
14         accessOrder = false;
15     }
16 
17     /**
18      * 構造一個使用默認初始容量(16)和默認加載因子(0.75)的LinkedHashMap,默認accessOrder爲false
19      */
20     public LinkedHashMap() {
21         super();
22         accessOrder = false;
23     }
24 
25     /**
26      * 構造一個指定map的LinkedHashMap,所創建LinkedHashMap使用默認加載因子(0.75)和足以容納指定map的初始容量,默認accessOrder爲false 。
27      */
28     public LinkedHashMap(Map<? extends K, ? extends V> m) {
29         super(m);
30         accessOrder = false;
31     }
32 
33     /**
34      * 構造一個指定初始容量、加載因子和accessOrder的LinkedHashMap
35      */
36     public LinkedHashMap( int initialCapacity,
37                       float loadFactor,
38                          boolean accessOrder) {
39         super(initialCapacity, loadFactor);
40         this.accessOrder = accessOrder;
41 }
複製代碼
  構造方法很簡單基本都是調用父類HashMap的構造方法(super),只有一個區別就是對於accessOrder的設定,上面的構造參數中多數都是將accessOrder默認設置爲false,只有一個構造方法留了一個出口可以設置accessOrder參數。看完了構造方法,發現一個問題,咦?頭部節點header的初始化跑哪裏去了?
     回憶一下,看看HashMap的構造方法:
複製代碼
 1 /**
 2      * Constructs an empty <tt>HashMap</tt> with the specified initial
 3      * capacity and load factor.
 4      *
 5      * @param  initialCapacity the initial capacity
 6      * @param  loadFactor      the load factor
 7      * @throws IllegalArgumentException if the initial capacity is negative
 8      *         or the load factor is nonpositive
 9      */
10     public HashMap( int initialCapacity, float loadFactor) {
11         if (initialCapacity < 0)
12             throw new IllegalArgumentException( "Illegal initial capacity: " +
13                                                initialCapacity);
14         if (initialCapacity > MAXIMUM_CAPACITY)
15             initialCapacity = MAXIMUM_CAPACITY;
16         if (loadFactor <= 0 || Float.isNaN(loadFactor))
17             throw new IllegalArgumentException( "Illegal load factor: " +
18                                                loadFactor);
19 
20         // Find a power of 2 >= initialCapacity
21         int capacity = 1;
22         while (capacity < initialCapacity)
23             capacity <<= 1;
24 
25         this.loadFactor = loadFactor;
26         threshold = (int)(capacity * loadFactor);
27         table = new Entry[capacity];
28         init();
29     }
30 
31     /**
32      * Initialization hook for subclasses. This method is called
33      * in all constructors and pseudo -constructors (clone, readObject)
34      * after HashMap has been initialized but before any entries have
35      * been inserted.  (In the absence of this method, readObject would
36      * require explicit knowledge of subclasses.)
37      */
38     void init() {
39     
複製代碼

  哦,明白了,init()在HashMap中是一個空方法,也就是給子類留的一個回調函數,ok,我們來看下LinkedHashMap對init()方法的實現吧。

複製代碼
 1     /**
 2      * Called by superclass constructors and pseudoconstructors (clone,
 3      * readObject) before any entries are inserted into the map.  Initializes
 4      * the chain.
 5      */
 6     void init() {
 7         // 初始化話header,將hash設置爲-1,key、value、next設置爲null
 8         header = new Entry<K,V>(-1, null, null, null);
 9         // header的before和after都指向header自身
10         header.before = header. after = header ;
11     
複製代碼
   init()方法看完了,看出點什麼嘛?LinkedHashMap中維護的是個雙向循環鏈表對不對?(什麼?還不明白,去好好看看給jdk寫註釋系列之jdk1.6容器(2)-LinkedList源碼解析
 
4.增加
 
     LinkedHashMap沒有重寫put方法,只是重寫了HashMap中被put方法調用的addEntry。
複製代碼
 1     /**
 2      * This override alters behavior of superclass put method. It causes newly
 3      * allocated entry to get inserted at the end of the linked list and
 4      * removes the eldest entry if appropriate.
 5      */
 6     void addEntry( int hash, K key, V value, int bucketIndex) {
 7         // 調用createEntry方法創建一個新的節點
 8         createEntry(hash, key, value, bucketIndex);
 9 
10         // Remove eldest entry if instructed, else grow capacity if appropriate
11         // 取出header後的第一個節點(因爲header不保存數據,所以取header後的第一個節點)
12         Entry<K,V> eldest = header.after ;
13         // 判斷是容量不夠了是要刪除第一個節點還是需要擴容
14         if (removeEldestEntry(eldest)) {
15             // 刪除第一個節點(可實現FIFO、LRU策略的Cache)
16             removeEntryForKey(eldest. key);
17         } else {
18             // 和HashMap一樣進行擴容
19             if (size >= threshold)
20                 resize(2 * table.length );
21         }
22     }
23 
24     /**
25      * This override differs from addEntry in that it doesn't resize the
26      * table or remove the eldest entry.
27      */
28     void createEntry( int hash, K key, V value, int bucketIndex) {
29         // 下面三行代碼的邏輯是,創建一個新節點放到單向鏈表的頭部
30         // 取出數組bucketIndex位置的舊節點 
31         HashMap.Entry<K,V> old = table[bucketIndex];
32         // 創建一個新的節點,並將next指向舊節點
33        Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
34         // 將新創建的節點放到數組的bucketIndex位置
35         table[bucketIndex] = e;
36 
37         // 維護雙向鏈表,將新節點添加在雙向鏈表header前面(鏈表尾部)
38         e.addBefore( header);
39         // 計數器size加1
40         size++;
41     }
42 
43     /**
44      * 默認返回false,也就是不會進行元素刪除了。如果想實現cache功能,只需重寫該方法
45      */
46     protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
47         return false;
48 }
複製代碼
  可以看到,在添加方法上,比HashMap中多了兩個邏輯,一個是當Map容量不足後判斷是刪除第一個元素,還是進行擴容,另一個是維護雙向鏈表。而在判斷是否刪除元素的時候,我們發現removeEldestEntry這個方法竟然是永遠返回false,這什麼鬼。。。哦,想了下,原來想要實現Cache功能,需要自己繼承LinkedHashMap然後重寫removeEldestEntry方法,這裏默認提供的是容器的功能。
     
5.刪除
 
     LinkedHashMap沒有重寫remove方法,只是在實現了Entry類的recordRemoval方法,該方法是HashMap的提供的一個回調方法,在HashMap的remove方法進行回調,而LinkedHashMap中recordRemoval的主要當然是要維護雙向鏈表了,返回上面去看下Entry類的recordRemoval方法吧。
 
6.查找
 
     LinkedHashMap重寫了get方法,但是確複用了HashMap中的getEntry方法,LinkedHashMap是在get方法中指加入了調用recoreAccess方法的邏輯,recoreAccess方法的目的當然也是維護雙向鏈表了,具體邏輯返回上面去看下Entry類的recoreAccess方法吧。
複製代碼
1 public V get(Object key) {
2         Entry<K,V> e = (Entry<K,V>)getEntry(key);
3         if (e == null)
4             return null;
5         e.recordAccess( this);
6         return e.value ;
7 }
複製代碼

 

7.是否包含
複製代碼
 1 /**
 2      * Returns <tt>true</tt> if this map maps one or more keys to the
 3      * specified value.
 4      *
 5      * @param value value whose presence in this map is to be tested
 6      * @return <tt> true</tt> if this map maps one or more keys to the
 7      *         specified value
 8      */
 9     public boolean containsValue(Object value) {
10         // Overridden to take advantage of faster iterator
11         // 遍歷雙向鏈表,查找指定的value
12         if (value==null) { 
13             for (Entry e = header .after; e != header; e = e.after )
14                 if (e.value ==null)
15                     return true;
16         } else {
17             for (Entry e = header .after; e != header; e = e.after )
18                 if (value.equals(e.value ))
19                     return true;
20         }
21         return false;
22  }
複製代碼
  LinkedHashMap對containsValue進行了重寫,我們在HashMap中說過,HashMap的containsValue需要遍歷整個hash表,這樣是十分低效的。而LinkedHashMap中重寫後,不再遍歷hash表,而是遍歷其維護的雙向鏈表,這樣在效率上難道就有所改善嗎?我們分析下:hash表是由數組+單向鏈表組成,而由於使用hash算法,可能會導致散列不均勻,甚至數組的有些項是沒有元素的(沒有hash出對應的散列值),而LinkedHashMap的雙向鏈表呢,是不存在空項的,所以LinkedHashMap的containsValue比HashMap的containsValue效率要好一些。
 
8.遍歷
 
 
     LinkedHashMap分析的也就差不多了,要理解LinkedHashMap一定要先對HashMap的實現有所理解,因爲它是HashMap的一個子類。我們明白了,LinkedHashMap的兩個主要作用,一個是可以實現FIFO、LUR策略的Cache功能,另一個就是提高了HashMap遍歷的效率(其他功能的效率有所降低的。。。),當然LinkedHashMap的遍歷會在Set中統一講解
 
     在最後,讓我們簡單基於LInkedHashMap實現一個Cache功能吧,go!
複製代碼
 1 import java.util.LinkedHashMap;
 2 import java.util.Map;
 3 
 4 public class MyLocalCache extends LinkedHashMap<String, Object> {
 5 
 6         private static final long serialVersionUID = 7182816356402068265L;
 7 
 8         private static final int DEFAULT_MAX_CAPACITY = 1024;
 9 
10         private static final float DEFAULT_LOAD_FACTOR = 0.75f;
11 
12         private int maxCapacity;
13 
14         public enum Policy {
15                FIFO, LRU
16        }
17 
18         public MyLocalCache(Policy policy) {
19                super(DEFAULT_MAX_CAPACITY, DEFAULT_LOAD_FACTOR, Policy.LRU .equals(policy));
20                this.maxCapacity = DEFAULT_MAX_CAPACITY;
21        }
22 
23         public MyLocalCache(int capacity, Policy policy) {
24                super(capacity, DEFAULT_LOAD_FACTOR, Policy. LRU.equals(policy));
25                this.maxCapacity = capacity;
26        }
27 
28         @Override
29         protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
30                return this.size() > maxCapacity;
31        }
32        
33         public static void main(String[] args) {
34               MyLocalCache cache = new MyLocalCache(5, Policy.LRU);
35               cache.put( "k1", "v1" );
36               cache.put( "k2", "v2" );
37               cache.put( "k3", "v3" );
38               cache.put( "k4", "v4" );
39               cache.put( "k5", "v5" );
40               cache.put( "k6", "v6" );
41               
42               System. out.println("size=" + cache.size());
43               
44               System. out.println("----------------------" );
45                for (Map.Entry<String, Object> entry : cache.entrySet()) {   
46                 System. out.println(entry.getValue());   
47             }
48               
49               System. out.println("----------------------" );
50               
51               System. out.println("k3=" + cache.get("k3"));
52               
53               System. out.println("----------------------" );
54                for (Map.Entry<String, Object> entry : cache.entrySet()) {   
55                 System. out.println(entry.getValue());   
56             }
57        }
58 
59 }
複製代碼
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章