LinkedHashMap源碼詳解

LinkedHashMap源碼詳解

LinkedHashMap是基於HashMap實現的,如果對HashMap不瞭解,請先學習HashMap:http://blog.csdn.net/luanmousheng/article/details/75195809

HashMap的無序性和LinkedHashMap的有序性

前面介紹的HashMap查找效率很高,但是也有一個缺點,即遍歷HashMap是無序的。LinkedHashMap顧名思義,是鏈表和哈希表的結合,鏈表具有天然的有序性,這裏的有序不是按照節點大小排序,而是按照節點的插入順序排序或者節點的訪問順序排序。

爲了比較HashMap和LinkedHashMap的有序性,首先觀察HashMap的遍歷結果:

  Map<String, String> map2 = new HashMap();
  map2.put("name", "jack");
  map2.put("age", "23");
  map2.put("job", "student");
  map2.put("home", "china");
  Iterator<Map.Entry<String, String>> it2 = map2.entrySet().iterator();
  while(it2.hasNext()) {
    System.out.println(it2.next().getKey());
  }

這段代碼段輸出:

home
age
name
job

可以看到,遍歷HashMap時的輸出和輸入時的順序沒有關係。

再觀察LinkedHashMap的遍歷結果:

  LinkedHashMap<String, String> map = new LinkedHashMap();
  map.put("name", "jack");
  map.put("home", "china");
  map.put("age", "23");
  map.put("job", "student");
  Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
  while(it.hasNext()) {
    System.out.println(it.next().getKey());
  }

這段代碼輸出:

name
home
age
job

可以看到,默認情況下,遍歷LinkedHashMap時的輸出和輸入的順序是一致的。因此我們說LinkedHashMap是可以保證有序性的。

我們說默認情況下,遍歷LinkedHashMap時的輸出和輸入的順序是一致的,LinkedHashMap還可以根據節點的訪問順序進行排序,即最新訪問的節點放在最前面。LinkedHashMap提供了accessOrder字段,這個字段可以指示LinkedHashMap是否按照訪問時間進行排序,通過LinkedHashMap另一個帶參數的構造函數可以創建一個按照訪問時間排序的哈希表,看下面的例子:

  LinkedHashMap<String, String> map = new LinkedHashMap(10, 0.75F, true);
  map.put("name", "jack");
  map.put("home", "china");
  map.put("age", "23");
  map.put("job", "student");
  //訪問了鍵"home"的節點
  map.get("home");
  Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
  while(it.hasNext()) {
    System.out.println(it.next().getKey());
  }

將4個鍵值對添加到LinkedHashMap後,訪問了鍵”home”的節點,這段代碼的輸出爲:

name
age
job
home

可以看到,鍵”home”被我們訪問後,放到了最後一個位置(最新的位置),這就是LinkedHashMap按照訪問先後順序的有序性。

好了,對於LinkedHashMap有了基本的認識後,下面將基於源碼,詳細介紹LinkedHashMap的原理。

LinkedHashMap原理

LinkedHashMap的存儲還是通過HashMap實現的,但是它和HashMap最大的區別在於LinkedHashMap維護了一個雙向鏈表,這個雙向鏈表按照節點的插入順序保存節點、或者按照節點的訪問先後順序保存節點。

先看下LinkedHashMap的聲明:

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

  //……
  private static class Entry<K,V> extends HashMap.Entry<K,V> {
        Entry<K,V> before, after;

        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }
        //……
  }
}

LinkedHashMap繼承了HashMap,內部靜態類Entry繼承了HashMap的Entry,但是它和HashMap.Entry不同的是,LinkedHashMap.Entry多了兩個字段:beforeafter,before表示在本節點之前添加到LinkedHashMap的那個節點,after表示在本節點之後添加到LinkedHashMap的那個節點,這裏的之前和之後指時間上的先後順序

有了對LinkedHashMap.Entry的瞭解,通過示意圖學習LinkedHashMap的工作原理。

這裏寫圖片描述

圖1 LinkedHashMap初始狀態

LinkedHashMap的初始狀態包括一個HashMap和一個只有頭節點的雙向鏈表。

接着插入鍵K1:

這裏寫圖片描述

圖2 插入鍵K1的狀態

接着插入鍵K2:

這裏寫圖片描述

圖3 插入鍵K2的狀態

通過以上三個示意圖,基本上可以理解LinkedHashMap的工作原理,示意圖的左邊部分是HashMap,右邊部分是雙向鏈表,這個雙向鏈表記錄了鍵的添加順序。注意,這裏我們將HashMap中的鍵和鏈表中的鍵分開表示,其實它們是同一個節點,分開後利於觀察,否則很多指針糾纏在一起,示意圖會顯得很混亂。

以上三個示意圖都是基於插入順序排序的,假設當前LinkedHashMap的狀態如圖3,並且我們創建該哈希表時候指定了按訪問時間排序,當我們在圖3的基礎上分別添加K3、訪問K2後的狀態爲:

這裏寫圖片描述

圖4 分別添加K3、訪問K2後的狀態

在圖3的基礎上,添加K3、訪問K2後將K2移到了鏈表的末尾。

將LinkedHashMap的accessOrder字段設置爲true後,每次訪問哈希表中的節點都將該節點移到鏈表的末尾,表示該節點是最新訪問的節點。

好了,我們通過幾個示意圖已經瞭解了LinkedHashMap的工作原理,接着學習LinkedHashMap的源碼。

LinkedHashMap源碼解析

我們使用哈希表是爲了往哈希表中添加鍵值對,那我們就從最基本的方法put說起。

put方法

這裏寫圖片描述

圖5 LinkedHashMap中搜索put方法

在idea中搜索LinkedHashMap的put方法,驚奇的發現,LinkedHashMap中並沒有定義put方法,相反,idea向我們推薦了很多put的實現,顯然我們應該去看HashMap的put實現,也就是說,LinkedHashMap的方法即是HashMap的put方法,當我們調用LinkedHashMap的put方法實際上調用的是父類HashMap的put方法。

看下HashMap的put實現:

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
      inflateTable(threshold);
    }
    if (key == null)
      return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
      Object k;
      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        //1:HashMap中是個空實現,子類LinkedHashMap的Entry實現了該方法
        e.recordAccess(this);
        return oldValue;
      }
    }

    modCount++;
    //2:子類LinkedHashMap重寫了該方法
    addEntry(hash, key, value, i);
    return null;
}

上面代碼1處和2處正是LinkedHashMap的不同之處,LinkedHashMap重寫了HashMap.Entry的recordAccess方法和HashMap的addEntry方法。其中recordAccess方法在父類HashMap的Entry是個空實現,子類LinkedHashMap.Entry重寫該方法是爲了記錄節點訪問的先後順序。

是時候介紹LinkedHashMap的Entry類了:

private static class Entry<K,V> extends HashMap.Entry<K,V> {
    //before節點在當前節點之前插入
    //after節點在當前節點之後插入
    Entry<K,V> before, after;

    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
      //直接調用了父類的構造函數
      super(hash, key, value, next);
    }

    //將當前節點從該雙向鏈表中刪除
    private void remove() {
      before.after = after;
      after.before = before;
    }

    //將當前節點插入指定節點的前面
    //其實就是改變鏈表的指針指向
    private void addBefore(Entry<K,V> existingEntry) {
      after  = existingEntry;
      before = existingEntry.before;
      before.after = this;
      after.before = this;
    }

    //這是LinkedHashMap與HashMap重要的不同之處
    void recordAccess(HashMap<K,V> m) {
      LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
      if (lm.accessOrder) {
        //若是節點訪問先後順序的規則
        lm.modCount++;
        //先把當前節點刪除,然後把該節點添加到頭節點的前面,也就是鏈表的末尾,參考示意圖2和圖3
        remove();
        addBefore(lm.header);
      }
    }

    void recordRemoval(HashMap<K,V> m) {
      remove();
    }
}

這段代碼最重要之處在於對accessOrder的判斷,若是基於訪問順序,每次訪問節點後需要將該節點移到鏈表的末尾處,否則recordAccess什麼也不做。其他的代碼就是對雙向鏈表指針指向的改變,參考圖2、3、4。

accessOrder的意義在於,每次訪問一個節點都將該節點移到鏈表的末尾,表示這個節點是最新訪問的節點,這在很多場景下都很有用處,比如LRU算法,排在鏈表末尾部分的節點都是最近使用過的節點,那麼排在前面部分的節點就可能長期都沒有被訪問過,此時系統可以將這些節點刪除以增加可用內存。

好了,現在繼續看put方法調用的addEntry方法。子類LinkedHashMap重寫了HashMap的addEntry方法,看下LinkedHashMap的addEntry方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
    //調用了父類HashMap的addEntry方法
    super.addEntry(hash, key, value, bucketIndex);

    // 如果需要的話刪除在鏈表中最久的節點
    Entry<K,V> eldest = header.after;
    //removeEldestEntry是個protected方法。
    //LinkedHashMap中該方法返回false,也就是不會刪除在鏈表中最久的節點。
    if (removeEldestEntry(eldest)) {
      removeEntryForKey(eldest.key);
    }
}

LinkedHashMap的addEntry方法首先調用了父類的addEntry方法,注意,子類可以重寫removeEldestEntry方法並返回true,刪除在鏈表中最久的節點。

到此爲止,似乎並沒有找到令我們多麼興奮的事,畢竟LinkedHashMap的removeEldestEntry返回了false,也就是LinkedHashMap只是調用了父類HashMap的addEntry方法,並沒有做其他的事。繼續深究HashMap的addEntry方法,能發現一些不同的地方:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
      resize(2 * table.length);
      hash = (null != key) ? hash(key) : 0;
      bucketIndex = indexFor(hash, table.length);
    }
    //LinkedHashMap重寫了該方法
    createEntry(hash, key, value, bucketIndex);
}

LinkedHashMap重寫了父類的createEntry方法,讓我們繼續看LinkedHashMap的createEntry方法:

void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMap.Entry<K,V> old = table[bucketIndex];
    Entry<K,V> e = new Entry<>(hash, key, value, old);
    table[bucketIndex] = e;
    //1:將節點e加入到header的前面,也就是鏈表的末尾
    e.addBefore(header);
    size++;
}

比較HashMap和LinkedHashMap,我們發現LinkedHashMap在1處有不同,其他地方都完全相同。

LinkedHashMap創建結點並將該節點添加到HashMap後,該節點會被鏈接到頭結點header鏈表的末尾,在這裏實現了LinkedHashMap插入的有序性。

構造函數

到這裏解釋完了put方法,接下來看下LinkedHashMap的構造方法,LinkedHashMap有很多重載的構造方法,原理大致大同,這裏介紹其中一個構造方法來講解:

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

這個帶3個參數的構造函數其中initialCapacity和loadFactor決定了哈希表的初始容量和加載因子,accessOrder決定了LinkedHashMap的排序規則,如果accessOrder=false,則按插入順序排序,否則按訪問順序排序。

該構造函數調用了父類的構造函數,看下HashMap中的構造函數:

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;
    threshold = initialCapacity;
    //HashMap中該方法是個空方法,子類LinkedHashMap重寫了該方法
    init();
}

父類HashMap中的init方法是個空實現,子類LinkedHashMap重寫了該方法,看下LinkedHashMap中init方法的實現:

void init() {
    header = new Entry<>(-1, null, null, null);
    header.before = header.after = header;
}

該方法創建了一個只帶頭節點的鏈表,參考圖1。

LinkedHashMap的所有構造函數都會調用父類HashMap的構造函數,HashMap的構造函數都會調用init方法,即我們創建LinkedHashMap時總會調用init方法創建一個只帶頭節點的鏈表。

get方法

public V get(Object key) {
    Entry<K,V> e = (Entry<K,V>)getEntry(key);
    if (e == null)
      return null;
    //看這裏,LinkedHashMap的不同之處
    e.recordAccess(this);
    return e.value;
}

這裏需要注意e.recordAccess的調用,這個調用之前我們已經分析過,每次訪問一個節點的時候都要根據accessOrder是否爲true,決定是否將該節點移到鏈表末尾,表示該節點是最近訪問的節點,實現按照訪問順序的有序性。

containsValue方法

public boolean containsValue(Object value) {
    if (value==null) {
      //在鏈表中查找值爲null的節點
      for (Entry e = header.after; e != header; e = e.after)
        if (e.value==null)
          return true;
    } else {
      //在鏈表中查找值爲value的節點
      for (Entry e = header.after; e != header; e = e.after)
        if (value.equals(e.value))
          return true;
    }
    return false;
}

LinkedHashMap的containsValue方法利用了該哈希表的鏈表特性,在鏈表中查找是否存在對應的值。

transfer方法

void transfer(HashMap.Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //利用LinkedHashMap的鏈表特性將節點放到新的哈希表
    for (Entry<K,V> e = header.after; e != header; e = e.after) {
      if (rehash)
        e.hash = (e.key == null) ? 0 : hash(e.key);
      int index = indexFor(e.hash, newCapacity);
      e.next = newTable[index];
      newTable[index] = e;
    }
}

該方法將原來哈希表中的Entry轉移到新的哈希表,通過遍歷鏈表將節點放到新的哈希表中。

迭代器

LinkedHashMap的迭代器是通過它的內部類實現的,其中最主要的類是LinkedHashIterator,該類是個抽象類,提供了基本的迭代方法。Entry迭代器、Key迭代器和Value迭代器都是通過繼承該抽象類實現的。首先看下LinkedHashIterator:

private abstract class LinkedHashIterator<T> implements Iterator<T> {
    //從鏈表頭結點的後一個節點開始遍歷
    Entry<K,V> nextEntry    = header.after;
    //保存了最近訪問到的節點
    Entry<K,V> lastReturned = null;

    //該字段用於快速失敗機制,當迭代器發現這兩個值不相等,說明有其他線程改變了
    //該哈希表,拋出ConcurrentModificationException
    int expectedModCount = modCount;

    //如果下個節點是頭節點,說明遍歷結束
    public boolean hasNext() {
      return nextEntry != header;
    }

    public void remove() {
      //最近返回的節點爲null,不能刪除
      if (lastReturned == null)
        throw new IllegalStateException();
      //快速失敗機制,拋出併發修改異常
      if (modCount != expectedModCount)
        throw new ConcurrentModificationException();

      LinkedHashMap.this.remove(lastReturned.key);
      //每迭代一次只能刪除一次,不能迭代一次刪除多次
      lastReturned = null;
      expectedModCount = modCount;
    }

    Entry<K,V> nextEntry() {
      //快速失敗機制
      if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
      //不存在下個節點
      if (nextEntry == header)
        throw new NoSuchElementException();
      //保存最近訪問的節點
      Entry<K,V> e = lastReturned = nextEntry;
      nextEntry = e.after;
      return e;
    }
}

LinkedHashIterator還是比較好理解的,和HashMap的迭代器類似,直接看源碼註釋好了。

EntryIterator、KeyIterator和ValueIterator都是通過繼承LinkedHashIterator實現的:

private class KeyIterator extends LinkedHashIterator<K> {
    //鍵的迭代器
    public K next() { return nextEntry().getKey(); }
}

private class ValueIterator extends LinkedHashIterator<V> {
    //值的迭代器
    public V next() { return nextEntry().value; }
}

private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
    //Entry的迭代器
    public Map.Entry<K,V> next() { return nextEntry(); }
}

總結

好了,LinkedHashMap的源碼就解釋完了,做下總結:

  1. LinkedHashMap是雙向鏈表和HashMap的完美結合。
  2. LinkedHashMap是有序的,默認通過插入順序排序,也可以通過構造函數的參數accessOrder指定通過訪問順序排序。
  3. 只要理解了HashMap的工作原理,就很容易理解LinkedHashMap,它只是在HashMap的基礎上將各個Entry節點通過雙鏈錶鏈接起來實現有序性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章