1. 程式人生 > 其它 >LinkedHashMap原始碼解析(jdk1.7之前)

LinkedHashMap原始碼解析(jdk1.7之前)

目錄

1 LinkedHashMap(jdk1.7之前)

我們知道Map其底層資料儲存是一個hash表(陣列+單向連結串列)。接下來我們看一下另一個LinkedHashMap,它是HashMap的一個子類,他在HashMap的基礎上維持了一個雙向連結串列(hash表+雙向連結串列),在遍歷的時候可以使用插入順序(先進先出,類似於FIFO),或者是最近最少使用(LRU)的順序。
來具體看下LinkedHashMap的實現。

1.1 定義

 public class LinkedHashMap<K,V>
     extends HashMap<K,V>
     implements Map<K,V>

從定義可以看到LinkedHashMap繼承於HashMap,且實現了Map介面。這也就意味著HashMap的一些優秀因素可以被繼承下來,比如hash定址,使用連結串列解決hash衝突等實現的快速查詢,對於HashMap中一些效率較低的內容,比如容器擴容過程,遍歷方式,LinkedHashMap是否做了一些優化呢。繼續看程式碼吧。

1.2 底層儲存

LinkedHashMap是基於HashMap

,並在其基礎上維持了一個雙向連結串列,也就是說LinkedHashMap是一個hash表(陣列+單向連結串列) +雙向連結串列的實現,到底實現方式是怎麼樣的,來看一下:

      /**       * The head of the doubly linked list.
       */ 
            private transient Entry<K,V> header ;
  
      /**       * The iteration ordering method for this linked hash map: <tt>true</tt>
       * for access -order, <tt> false</tt> for insertion -order.
       *
      * @serial11      */   
      private final boolean accessOrder;

看到了一個無比熟悉的屬性header,它在LinkedList中出現過,英文註釋很明確,是雙向連結串列的頭結點對不對。
再看accessOrder這個屬性,true表示最近較少使用順序,false表示插入順序。當然你說怎麼沒看到陣列呢,別忘了LinkedHashMap繼承於HashMap

再來看下Entry這個節點類和HashMap中的有什麼不同。

      /**       * LinkedHashMap entry.
       */ 
            private static class Entry<K,V> extends HashMap.Entry<K,V> {
          // These fields comprise the doubly linked list used for iteration.
          // 雙向連結串列的上一個節點before和下一個節點after 
                   Entry<K,V> before, after ;
  
         // 構造方法直接呼叫父類HashMap的構造方法(super)
                 Entry( int hash, K key, V value, HashMap.Entry<K,V> next) {
             super(hash, key, value, next);
         }
 
         /**          * 從連結串列中刪除當前節點的方法
          */
                   private void remove() {
             // 改變當前節點前後兩個節點的引用關係,當前節點沒有被引用後,gc可以回收
             // 將上一個節點的after指向下一個節點
                          before.after = after;
            // 將下一個節點的before指向前一個節點
                         after.before = before;
         }
 
         /**          * 在指定的節點前加入一個節點到連結串列中(也就是加入到連結串列尾部)
          */
                   private void addBefore(Entry<K,V> existingEntry) {
             // 下面改變自己對前後的指向
             // 將當前節點的after指向給定的節點(加入到existingEntry前面嘛)
                          after  = existingEntry;
             // 將當前節點的before指向給定節點的上一個節點33             before = existingEntry.before ;
 
             // 下面改變前後最自己的指向
             // 上一個節點的after指向自己
                          before.after = this;
             // 下一個幾點的before指向自己
                          after.before = this;
         }
 
         // 當向Map中獲取查詢元素或修改元素(put相同key)的時候呼叫這個方法
                  void recordAccess(HashMap<K,V> m) {
             LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
             // 如果accessOrder為true,也就是使用最近較少使用順序
                          if (lm.accessOrder ) {
                 lm. modCount++;
                 // 先刪除,再新增,也就相當於移動了
                 // 刪除當前元素
                                 remove();
                 // 將當前元素加入到header前(也就是連結串列尾部)
                                  addBefore(lm. header);
             }
         }
 
         // 當從Map中刪除元素的時候調動這個方法
                  void recordRemoval(HashMap<K,V> m) {
             remove();
         }
}

可以看到Entry繼承了HashMap中的Entry,但是LinkedHashMap中的Entry多了兩個屬性指向上一個節點的before和指向下一個節點的after,也正是這兩個屬性組成了一個雙向連結串列,Entry還有一個繼承下來的next屬性,這個next是單向連結串列中用來指向下一個節點的,怎麼回事嘛,怎麼又是單向連結串列又是雙向連結串列呢,其實想的沒錯,這裡的節點即是Hash表中的單向連結串列中的一個節點,它又是LinkedHashMap維護的雙向連結串列中的一個節點,是不是瞬間覺得高大上了

注:黑色箭頭指向表示單向連結串列的next指向,紅色箭頭指向表示雙向連結串列的before指向,藍色箭頭指向表示雙向連結串列的after指向。另外LinkedHashMap種還有一個header節點是不儲存資料的,這裡沒有畫出來。

從上圖可以看出LinkedHashMap仍然是一個Hash表,底層由一個數組組成,而陣列的每一項都是個單向連結串列,由next指向下一個節點。但是LinkedHashMap所不同的是,在節點中多了兩個屬性beforeafter,由這兩個屬性組成了一個雙向迴圈連結串列,而由這個雙向連結串列維持著Map容器中元素的順序。看下Entry中的recordRemoval方法,該方法將在節點被刪除時候呼叫,Hash表中連結串列節點被正常刪除後,呼叫該方法修正由於節點被刪除後雙向連結串列的前後指向關係,從這一點來看,LinkedHashMap比HashMap的add、remove、set等操作要慢一些(因為要維護雙向連結串列 )。

1.3 構造方法

  /**       * 構造一個指定初始容量和載入因子的LinkedHashMap,預設accessOrder為false
       */ 
            public LinkedHashMap( int initialCapacity, float loadFactor) {
          super(initialCapacity, loadFactor);
          accessOrder = false;
      }
  
      /**      * 構造一個指定初始容量的LinkedHashMap,預設accessOrder為false
      */
           public LinkedHashMap( int initialCapacity) {
         super(initialCapacity);
         accessOrder = false;
     }
 
     /**      * 構造一個使用預設初始容量(16)和預設載入因子(0.75)的LinkedHashMap,預設accessOrder為false
      */
           public LinkedHashMap() {
         super();
        accessOrder = false;
     }
 
     /**      * 構造一個指定map的LinkedHashMap,所建立LinkedHashMap使用預設載入因子(0.75)和足以容納指定map的初始容量,預設accessOrder為false 。
      */
           public LinkedHashMap(Map<? extends K, ? extends V> m) {
         super(m);
         accessOrder = false;
     }
 
     /**      * 構造一個指定初始容量、載入因子和accessOrder的LinkedHashMap
      */
           public LinkedHashMap( int initialCapacity,
                       float loadFactor,
                          boolean accessOrder) {
         super(initialCapacity, loadFactor);
40         this.accessOrder = accessOrder;
41 }

構造方法很簡單基本都是呼叫父類HashMap的構造方法(super),只有一個區別就是對於accessOrder的設定,上面的構造引數中多數都是將accessOrder預設設定為false,只有一個構造方法留了一個出口可以設定accessOrder引數。看完了構造方法,發現一個問題,頭部節點header的初始化跑哪裡去了
回憶一下,看看HashMap的構造方法:

  /**       * Constructs an empty <tt>HashMap</tt> with the specified initial
       * capacity and load factor.
       *
       * @param  initialCapacity the initial capacity
       * @param  loadFactor      the load factor
       * @throws IllegalArgumentException if the initial capacity is negative
       *         or the load factor is nonpositive
       */
            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);
 
         // Find a power of 2 >= initialCapacity
                  int capacity = 1;
         while (capacity < initialCapacity)
             capacity <<= 1;
 
         this.loadFactor = loadFactor;
         threshold = (int)(capacity * loadFactor);
         table = new Entry[capacity];
         init();
     }
 
     /**      * Initialization hook for subclasses. This method is called
      * in all constructors and pseudo -constructors (clone, readObject)
      * after HashMap has been initialized but before any entries have
      * been inserted.  (In the absence of this method, readObject would
      * require explicit knowledge of subclasses.)
      */
           void init() {

init()HashMap中是一個空方法,也就是給子類留的一個回撥函式,我們來看下LinkedHashMapinit()方法的實現

      /**       * Called by superclass constructors and pseudoconstructors (clone,
       * readObject) before any entries are inserted into the map.  Initializes
      * the chain.
       */ 
            void init() {
          // 初始化話header,將hash設定為-1,key、value、next設定為null 
                   header = new Entry<K,V>(-1, null, null, null);
          // header的before和after都指向header自身
                   header.before = header. after = header ;
     

1.4 增加

LinkedHashMap沒有重寫put方法,只是重寫了HashMap中被put方法呼叫的addEntry

      /**       * This override alters behavior of superclass put method. It causes newly
       * allocated entry to get inserted at the end of the linked list and
       * removes the eldest entry if appropriate.
       */ 
            void addEntry( int hash, K key, V value, int bucketIndex) {
          // 呼叫createEntry方法建立一個新的節點 
                   createEntry(hash, key, value, bucketIndex);
  
         // Remove eldest entry if instructed, else grow capacity if appropriate
         // 取出header後的第一個節點(因為header不儲存資料,所以取header後的第一個節點)
                  Entry<K,V> eldest = header.after ;
         // 判斷是容量不夠了是要刪除第一個節點還是需要擴容
                  if (removeEldestEntry(eldest)) {
             // 刪除第一個節點(可實現FIFO、LRU策略的Cache)
                          removeEntryForKey(eldest. key);
         } else {
             // 和HashMap一樣進行擴容
                          if (size >= threshold)
                 resize(2 * table.length );
         }
     }
 
     /**      * This override differs from addEntry in that it doesn't resize the
      * table or remove the eldest entry.
      */
           void createEntry( int hash, K key, V value, int bucketIndex) {
         // 下面三行程式碼的邏輯是,建立一個新節點放到單向連結串列的頭部
         // 取出陣列bucketIndex位置的舊節點
                 HashMap.Entry<K,V> old = table[bucketIndex];
         // 建立一個新的節點,並將next指向舊節點
                 Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
        // 將新建立的節點放到陣列的bucketIndex位置
                 table[bucketIndex] = e;
 
         // 維護雙向連結串列,將新節點新增在雙向連結串列header前面(連結串列尾部)
                  e.addBefore( header);
         // 計數器size加1
                  size++;
     }
 
     /**      * 預設返回false,也就是不會進行元素刪除了。如果想實現cache功能,只需重寫該方法
      */
           protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
         return false;
}

可以看到,在新增方法上,比HashMap中多了兩個邏輯,一個是當Map容量不足後判斷是刪除第一個元素,還是進行擴容,另一個是維護雙向連結串列。而在判斷是否刪除元素的時候,我們發現removeEldestEntry這個方法竟然是永遠返回false,原來想要實現Cache功能,需要自己繼承LinkedHashMap然後重寫removeEldestEntry方法,這裡預設提供的是容器的功能。

1.5 刪除

LinkedHashMap沒有重寫remove方法,只是在實現了Entry類的recordRemoval方法,該方法是HashMap的提供的一個回撥方法,在HashMapremove方法進行回撥,而LinkedHashMaprecordRemoval的主要當然是要維護雙向連結串列了

1.6 查詢

LinkedHashMap重寫了get方法,但是確複用了HashMap中的getEntry方法,LinkedHashMap是在get方法中指加入了呼叫recoreAccess方法的邏輯,recoreAccess方法的目的當然也是維護雙向連結串列了,具體邏輯返回上面去看下Entry類的recoreAccess方法吧

 public V get(Object key) {
         Entry<K,V> e = (Entry<K,V>)getEntry(key);
         if (e == null)
             return null;
         e.recordAccess( this);
         return e.value ;
 }

1.7 是否包含

  /**       * Returns <tt>true</tt> if this map maps one or more keys to the
       * specified value.
       *
       * @param value value whose presence in this map is to be tested
       * @return <tt> true</tt> if this map maps one or more keys to the
       *         specified value
       */      public boolean containsValue(Object value) {
         // Overridden to take advantage of faster iterator
         // 遍歷雙向連結串列,查詢指定的value
                  if (value==null) { 
             for (Entry e = header .after; e != header; e = e.after )
                 if (e.value ==null)
                     return true;
         } else {
             for (Entry e = header .after; e != header; e = e.after )
                 if (value.equals(e.value ))
                     return true;
         }
         return false;
  }

LinkedHashMapcontainsValue進行了重寫,HashMapcontainsValue需要遍歷整個hash表,這樣是十分低效的。而LinkedHashMap中重寫後,不再遍歷hash表,而是遍歷其維護的雙向連結串列,這樣在效率上難道就有所改善嗎?我們分析下:hash表是由陣列+單向連結串列組成,而由於使用hash演算法,可能會導致雜湊不均勻,甚至陣列的有些項是沒有元素的(沒有hash出對應的雜湊值),而LinkedHashMap的雙向連結串列呢,是不存在空項的,所以LinkedHashMapcontainsValueHashMapcontainsValue效率要好一些。

1.8 cache功能

在最後,讓我們簡單基於LInkedHashMap實現一個Cache功能

  import java.util.LinkedHashMap;
  import java.util.Map;
  
  public class MyLocalCache extends LinkedHashMap<String, Object> {
  
          private static final long serialVersionUID = 7182816356402068265L;
  
          private static final int DEFAULT_MAX_CAPACITY = 1024;
  
         private static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
         private int maxCapacity;
 
         public enum Policy {
                FIFO, LRU
        }
 
         public MyLocalCache(Policy policy) {
                super(DEFAULT_MAX_CAPACITY, DEFAULT_LOAD_FACTOR, Policy.LRU .equals(policy));
                this.maxCapacity = DEFAULT_MAX_CAPACITY;
        }
 
         public MyLocalCache(int capacity, Policy policy) {
                super(capacity, DEFAULT_LOAD_FACTOR, Policy. LRU.equals(policy));
                this.maxCapacity = capacity;
        }
 
         @Override
         protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
                return this.size() > maxCapacity;
        }
       
         public static void main(String[] args) {
               MyLocalCache cache = new MyLocalCache(5, Policy.LRU);
               cache.put( "k1", "v1" );
               cache.put( "k2", "v2" );
               cache.put( "k3", "v3" );
               cache.put( "k4", "v4" );
               cache.put( "k5", "v5" );
               cache.put( "k6", "v6" );
               
               System. out.println("size=" + cache.size());
               
               System. out.println("----------------------" );
                for (Map.Entry<String, Object> entry : cache.entrySet()) {   
                 System. out.println(entry.getValue());   
             }
               
               System. out.println("----------------------" );
               
               System. out.println("k3=" + cache.get("k3"));
               
               System. out.println("----------------------" );
                for (Map.Entry<String, Object> entry : cache.entrySet()) {   
                 System. out.println(entry.getValue());   
             }
        }
 
}