1. 程式人生 > >jdk原始碼閱讀筆記-LinkedHashMap

jdk原始碼閱讀筆記-LinkedHashMap

  Map是Java collection framework 中重要的組成部分,特別是HashMap是在我們在日常的開發的過程中使用的最多的一個集合。但是遺憾的是,存放在HashMap中元素都是無序的,原因是我們在put或get資料的時候都是根據key的hash值來確定元素的位置。在具體的業務場景中,我們更多的希望對於HashMap集合能夠進行順序訪問,好在 jdk 中已經給我們提供了一種解決方案,那就是LinkedHashMap。該類繼承與HashMap,因此HashMap擁有的特性它都有,同時還具備其他的特性,比如實現了插入順序排序和訪問順序排序,預設以插入順序排序。同時也能夠利用LinkedHashMap實現LRU演算法。LinkedHashMap api很少,基本都是呼叫HashMap的方法,所以建議熟悉HashMap原始碼之後再來看這篇文章,我之前也寫過【

HashMap原始碼分析筆記】,大家可以點進去參考一下。

  LRU演算法: LRU是Least Recently Used的縮寫,即最近最少使用,也就是說將熱點資料放到最前面,冷門資料放到最後,當達到一定條件後會刪除冷門資料,在一個快取系統中經常會用到該演算法。

  

  一、LinkedHashMap與HashMap資料結構對比

  從上圖可以看到HashMap的資料結構位陣列+單向連結串列,資料存放在連結串列的node節點上,每個node節點上都有一個指標指向下一個節點,每個陣列index上的連結串列跟其他的index上面的連結串列是部相互連結的。LinkedHashMap在部破壞HashMap的結構基礎之上,每個node節點都額外增加了兩個指標,分別指向了前一個節點和下一個節點,所以在HashMap上所有的node節點形成了一條雙向連結串列,每次新增往LinkedHashMap put資料的時候都將節點放在雙向連結串列的最後位置,從而實現了插入順序排序。在LinkedHashMap中,節點的定義如下:

/**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

 

   節點繼承與HashMap的 Node內部類,但是又額外添加了兩個屬性,before和after,分別指向前一個節點和後一個節點,形成一個雙向連結串列。

 

  二、LinkedHashMap類結構

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

   LinkedHashMap繼承了HashMap,所以擁有HashMap的所有特性。

 

  三、成員變數

    /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;

    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;

  LinkedHashMap在HashMap的基礎之上添加了head、tail和accessOrder屬性:

  head:雙向連結串列的表頭

  tail: 雙向連結串列的表尾

  accessOrder:排序的標誌。預設為false,按插入順序排序。可以通過構造方法設定為true,按訪問順序排序。

 

  四、構造方法

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * 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 LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the specified initial capacity and a default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity
     * @throws IllegalArgumentException if the initial capacity is negative
     */
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the default initial capacity (16) and load factor (0.75).
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    /**
     * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
     * the same mappings as the specified map.  The <tt>LinkedHashMap</tt>
     * instance is created with a default load factor (0.75) and an initial
     * capacity sufficient to hold the mappings in the specified map.
     *
     * @param  m the map whose mappings are to be placed in this map
     * @throws NullPointerException if the specified map is null
     */
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

    /**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

  5個構造方法都是直接呼叫父類HashMap的構造方法。

  

  五、新增資料put(Object key,V value)

  LinkedHashMap中並沒有對HashMap進行復寫,也就是說新增資料的時候其實就是呼叫的HashMap的put方法,那麼它是怎麼進行排序的呢?下面我們一起來看看HashMap中是怎麼新增資料的吧。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /**
         * 通過位與的方式來確定下標位置,判斷當前下標位置是否為空,如果為空直接放入到該位置上
         * 不為空則通過equals方法來尋找當前位置上面的元素,如果有相同的key,則將覆蓋掉,如果沒有則將node放置在對應
         * 位置上面
         */
        if ((p = tab[i = (n - 1) & hash]) == null)//直接放到陣列中
            tab[i] = newNode(hash, key, value, null);//建立新節點
        else {//當前位置不為空
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//已存在相同的key的資料,將其覆蓋
                e = p;
            else if (p instanceof TreeNode)//當前位置是紅黑樹,將Node節點放到紅黑樹中
                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;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//覆蓋相同key的node
                        break;
                    p = e;
                }
            }
        //map中已經存在了相同的key,將原來的資料替換掉
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e);//替換資料被視為更新資料,所以呼叫訪問排序方法。 return oldValue; } } ++modCount;//快速失敗機制 if (++size > threshold)//每次插入資料都要判斷一下當前儲存的資料是否需要擴容 resize(); afterNodeInsertion(evict);//插入資料後進行插入排序 return null; }    

   HashMap 插入資料的核心方法為 putVal方法,每次插入資料都呼叫newNode方法,這個方法LinkedHashMap 中已經重寫了:

   Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

  第一:因為已經重寫了父類的newNode方法,所以在插入資料時建立新節點實際是呼叫了LinkedHashMap的newNode方法,該方法中每次建立新節點都想LinkedHashMap自身維護的雙向連結串列的尾部新增一個當前新創節點,我們繼續看看linkNodeLast方法:

 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

  這個方法很簡單,如果連結串列為空則將新節點設定為頭部和尾部,否則將新節點放到連結串列的最後,將新節點的前指標指向原尾部的節點,原尾部節點的後指標指向新節點。如果不明白具體的插入流程,可參考我之前的【ArrayList原始碼閱讀筆記】,裡面有詳細插入各個位置的流程。

  第二:建立新節點完成後,將新節點放入物件的連結串列或樹中,如果新節點的key在HashMap中已經存在,那麼就會原來的value覆蓋掉,此時被視為修改了節點,呼叫afterNodeAccess(e)方法,LinkedHashMap對這個方法進行了重寫,我們看一下原始碼:

    /**
     * 每次訪問節點後將該節點放在最後
     * @param e
     */
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

  該方法中,如果 accessOrder  為true並且訪問節點不為空,那麼就會將訪問過的節點移動到最後。這也就是實現了LRU演算法,具體移動路程如下:

 

   第三:插入資料全部完成之後,執行afterNodeInsertion(evict)方法,evict為true,LinkedHashMap重寫的該方法:

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

  afterNodeInsertion方法是用來刪除最舊最少使用的資料,上面提到過,每次訪問、修改map中的資料的時候都會將該節點放在連結串列的最後面,因此約靠前的資料使用的頻率的越低,我們稱之為冷資料,該方法就是將最冷門的資料刪除掉。removeEldestEntry方法預設返回false,所以LinkedHashMap本身並不提供LRU演算法,需要自己手動實現LinkedHashMap,然後重寫removeEldestEntry方法,根據自己具體的業務決定何時刪除冷資料。

  總結:這裡只總結LinkedHashMap實現部分。在put資料的時候,LinkedHashMap不僅將資料放在HashMap中,同時也將該資料放在自己維護的雙向連結串列的最後,以實現順序排序。如果put進去的資料的key已經存在與Map中,則將該資料移動到連結串列的最後位置。插入資料完成後,根據子類具體實現情況是否將第一個資料刪除。

 

  六、獲取資料get(key)

  

 /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     */
    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

   這個方法看起來也比較簡單,如果accessOrder為true,即按訪問順序排序,那個每次都將該資料放到連結串列的最後面。

 

  七、其他方法

  LinkedHashMap本身的方法比較少,而且大部分都是呼叫父類的方法,所以在這裡就不說了,可以看看HashMap的原始碼。

 

  八、總結

  LinkedHashMap繼承與HashMap,因此它有HashMap一樣的特性。同時也彌補了HashMap無法順序遍歷的缺點。LinkedHashMap可以實現插入順序排序(預設排序),也可以根據訪問順序排序,也就是訪問的元素次數越多,該元素就越靠前。實現順序遍歷的底層原理是,LinkedHashMap自身維護了一張雙向連結串列,為此插入、訪問或修改資料的時候都將該節點放在連結串列最後面。按預設排序方式的話,在遍歷的時候就從表頭開始遍歷,按訪問順序排序就從連結串列表尾開始遍歷。另外,LinkedHashMap也可以用來搭建一個快取系統底層儲存結構,後面如果我有空的話,可能也會手寫一個簡單的快取demo。最後,如果文章有什麼寫的不對的地方,歡迎大家提出來,我的qq:1170971295。