Java類集框架 —— LinkedHashMap原始碼分析

weixin_34253539發表於2017-09-22

前言

我們知道HashMap底層是採用陣列+單向線性連結串列/紅黑樹來實現的,HashMap在擴容或者連結串列與紅黑樹轉換過程時可能會改變元素的位置和順序。如果需要儲存元素存入或訪問的先後順序,那就需要採用LinkedHashMap了。

LinkedHashMap結構

LinkedHashMap繼承自HashMap,它的所有操作和HashMap類似,底層結構也和HashMap一樣,只不過為了維護元素的存入/訪問順序,增加了一個雙向連結串列。

LinkedHashMap結構圖
LinkedHashMap結構圖

LinkedHashMap由陣列、單向線性連結串列、紅黑樹、雙向線性連結串列組成。如上圖:灰色區域為陣列,藍色節點和藍色箭頭為單向連結串列的引用關係,綠色節點和綠色箭頭為紅黑樹的引用關係,節點中的數字依次表示元素的存入/訪問順序,由橙色的雙向箭頭表示雙向連結串列的引用關係。

注:在JDK1.7及之前HashMap中沒有紅黑樹,LinkedHashMap中也不存在紅黑樹。另在JDK1.6及之前,HashMap中的連結串列為單向環形連結串列,LinkedHashMap中中的單向連結串列和雙向連結串列都是環形連結串列。在JDK1.8,LinkedHashMap中可能會存在紅黑樹,同時單向連結串列和雙向連結串列都是線性的。本文是基於JDK1.8來分析的。

LinkedHashMap原始碼分析

基本屬性:

transient LinkedHashMap.Entry<K,V> head;    // 雙向連結串列頭節點
transient LinkedHashMap.Entry<K,V> tail;    // 雙向連結串列尾節點
final boolean accessOrder;                    // 是否按照訪問順序排序複製程式碼

head和tail分別記錄了雙向連結串列的頭節點和尾節點,遍歷時通過headtail就可以按照存入/訪問的順序來取資料。

accessOrder用以表示LinkedHashMap是否按照訪問順序來排序,為true的話表示按照訪問順序排序,為false表示按照存入順序排序,預設為false

建構函式:

// 無參構造
public LinkedHashMap() {
    super();
    accessOrder = false;
}
// 給定初始容量
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}
// 給定初始容量和載入因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}
// 給定初始容量、載入因子、是否按訪問先後排序
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}複製程式碼

建構函式都是呼叫父類HashMap的建構函式。前3個都預設accessOrderfalseLinkedHashMap內部按照存入順序排序。最後一個建構函式可以指定accessOrder的值。

增:

LinkedHashMap新增資料要呼叫了父類的HashMapput方法,在HashMap的原始碼中,put方法存入元素後,呼叫了afterNodeAccessafterNodeInsertion方法,這兩個方法在HashMap中都是空方法,LinkedHashMap實現了這兩個方法:

void afterNodeAccess(Node<K,V> e) { 
    LinkedHashMap.Entry<K,V> last;
    // 如果按照訪問順序排序,並且新增的元素e不是雙向連結串列的尾節點
    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;
    }
}複製程式碼

afterNodeAccess方法的邏輯就是將當前節點e移動到雙向連結串列的尾部。每次LinkedHashMap中有元素被訪問時,就會按照訪問先後來排序,先訪問的在雙向連結串列中靠前,越後訪問的越接近尾部。當然只有當accessOrdertrue時,才會執行這個操作。

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);
    }
}複製程式碼

afterNodeInsertion方法意思是evicttrue時刪除雙向連結串列的頭節點。

通過afterNodeAccessafterNodeInsertion這兩個方法,如果當LinkedHashMap的容量達到一定量時,需要儲存它的size不變,那麼每次新增一個元素到雙向連結串列的尾部,就要刪除一個雙向連結串列頭部的元素,這相當於實現了LruCache的策略。

刪:

刪除元素同樣也是呼叫了HashMapremove方法,在remove方法中,呼叫了afterNodeRemoval方法。

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;
}複製程式碼

afterNodeRemoval方法就是將e節點從雙向連結串列中刪除,更改e前後節點引用關係,使之重新連成完整的雙向連結串列。

改:

LinkedHashMap更改元素的value值,仍是呼叫put方法,涉及到的邏輯可以看上面的afterNodeAccessafterNodeInsertion這兩個方法。

查:

LinkedHashMap自己實現了get方法。

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;
}複製程式碼

邏輯非常簡單,直接呼叫HashMapgetNode方法,如果需要按照訪問先後排序,呼叫afterNodeAccess更新雙向連結串列排序。

總結

LinkedHashMap繼承了HashMap的所有特性,唯一的區別就是LinkedHashMap是一個有序的對映集合,而HashMap則是無序的。LinkedHashMap實現排序的原理就是再內部增加了一個雙向連結串列來記錄元素的存入/訪問順序。LinkedHashMap內部是記錄的是存入還是訪問順序取決於關鍵屬性accessOrder,預設是按存入順序記錄。

相關文章