源碼|jdk源碼之LinkedHashMap分析

HashMap做爲一種經典的數據結構,其根據key定位元素能達到平均O(1)的時間複雜度。 可是,存儲於HashMap中的元素顯然是無序的,遍歷HashMap的順序得看臉。。。
那如何使得HashMap裏的元素變得有序呢?一種思路是,將存放HashMap元素的節點,使用指針將他們串起來。換言之,就像在HashMap裏面「嵌入」了一個鏈表同樣。
實際上,jdk的LinkedHashMap就是使用這種思路實現的。java

<!-- more -->node

繼承HashMap

LinkedHashMap中的代碼不算多,這是由於,jdk的設計使用繼承複用了代碼,在jdk的設計中,LinkedHashMap是HashMap的擴展:算法

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

對節點進行擴展

父類HashMap中的節點

回想一下HashMap的實現方式中,將key和value打包成的節點有兩種:
第一種,傳統分離鏈表法的鏈表節點。數據結構

static class Node<K,V> implements Map.Entry<K,V> {
    /* ... */
}

第二種,HashMap爲進行優化,必定狀況下會將鏈表重構爲紅黑樹。第二種節點是紅黑樹節點:函數

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    /* ... */
}

忽然發現,HashMap的TreeNode是繼承至LinkedHashMap的Entry的。。。
我的觀點是jdk這種作法不是很優雅,自己LinkedHashMap繼承HashMap就使得二者之間的邏輯混在了一塊兒,而這裏的內部實現又反過來繼承,邏輯搞得很混亂。優化

擴展節點

LinkedListHashMap須要將節點串成一個「嵌入式」雙向鏈表,所以須要給這兩種節點增長兩個字段:spa

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,增長雙向鏈表字段。
因爲TreeNode是繼承自LinkedListMap.Entry的,因此它也有這兩個字段。設計

屬性

再來看下LinkedHashMap中的屬性,不多:指針

/**
 * 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;

記錄雙向鏈表的表頭和表尾。從註釋中能夠看出,head節點是最老的,tail節點是最新的,也即鏈表按照由老到新的順序串起來。code

最後,因爲LinkedHashMap是能夠設置它組織元素的順序。一種是鏈表中的元素是按插入時候的順序排序,另一種是按照訪問的順序排序。

// 指定順序是按照訪問順序來,仍是插入順序來
final boolean accessOrder;

這個accessOrder指定是否按插入順序來。

重寫建立節點的函數

因爲對Map中的節點進行了擴展,所以,在建立節點時不能使用原來的節點了,而應該使用從新建立後的。
HashMap將建立節點的操做抽取出來放到了單獨的函數中,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;
    }

    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
        LinkedHashMap.Entry<K,V> t =
            new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }

    // HashMap的TreeNode是繼承自LinkedHashMap.Entry的,所以可以參與組織雙向鏈表
    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
        TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
        linkNodeLast(p);
        return p;
    }

    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
        TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }

在獲取、插入、刪除元素時維護雙向鏈表

接下來,則須要在LinkedHashMap的操做時維護雙向鏈表。

刪除

回顧下HashMap的源代碼,咱們知道,HashMap在刪除節點後,會調用afterNodeRemoval函數。
這個函數在HashMap中是空的,實際上jdk是將它設計爲一個hook,果真,在LinkedHashMap中,就重寫了該函數,在其中維護雙向鏈表:

// 當有節點被刪除(即有元素被移除),那麼也要將它從雙向鏈表中移除
void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

插入

按照相似的思路,HashMap中在插入元素後會調用afterNodeInsertion,那是否是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);
    }
}

然而,實際上在LinkedHashMap中該函數彷佛沒有什麼用。由於:

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

removeEldestEntry始終返回false,afterNodeInsertion至關於什麼也沒作。這個邏輯設計目的是什麼,還不能很清楚。也許也是爲了讓誰去繼承?之後再探究。

那插入元素後的在哪裏維護了雙向鏈表呢?回到以前的newNodenewTreeNode

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;
}

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    linkNodeLast(p);
    return p;
}

// 尾插雙向鏈表節點
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;
    }
}

因爲HashMap中調用newNode時候都是爲了裝新插入的元素,因此在這裏維護雙向鏈表。
感受耦合是否是太緊了。。。若是HashMap因爲某個操做須要臨時搞個newNode借用下,豈不是會出問題?

下面是replacementNodereplacementTreeNode
replacementNode在HashMap中的做用是,該K V以前是被TreeNode包裝的,如今須要拿Node包裝它。這也勢必會影響雙向鏈表的結構,因此這裏也須要額外維護下。

獲取

獲取的時候,一樣,是重寫了`afterNodeAccess`鉤子,這樣在HashMap的獲取邏輯結束後,這裏的邏輯會被執行,維護雙向鏈表。
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;
    }
}

LinkedHashMap中的順序有訪問序和插入序,只有訪問序才須要在訪問的時候更新雙向鏈表結構。也即accessOrder爲true纔會執行這段邏輯。

最後,注意到:

++modCount;
}

通常來講,只有修改了Map結構的操做,才須要修改modCount以讓正在迭代的迭代器感知到了變化。
可是這裏,因爲迭代器是使用這裏的「嵌入式」雙向鏈表進行迭代,而在這裏會改變雙向鏈表的結構,若迭代器繼續迭代會形成不可預測的結果。
因此,這裏須要改變modCount,阻止迭代器繼續迭代。

典型應用場景

LinkedHashMap的一個典型應用場景是LRU算法。

因爲如今夜已深,如今不敢熬夜身體吃不消,想睡覺了。因此這個坑之後再填

最後

LinkedHashMap還有其它的一些實現細節,如:

  1. clear的時候也要同時維護雙向鏈表;
  2. 根據雙向鏈表實現迭代器。

最後,總結下jdk中對LinkedHashMap中的實現思路:

  1. 擴展HashMap實現。
  2. 擴展HashMap的節點(包括Node和TreeNode),加入兩個域組織額外的雙向鏈表保存順序。
  3. 在產生插入、刪除、訪問的地方維護雙向鏈表,經過重寫某些方法實現。
  4. 實現迭代器相關邏輯,由於迭代器是根據雙向鏈表順序迭代的。
相關文章
相關標籤/搜索