LinkedHashMap常用方法源碼

類介紹(註釋)

  1. add、contains、remove 方法,時間複雜度是O(1)。
  2. LinkedHashMap的遍歷耗時,與_capacity無關,與map的size(元素多少)呈線性。_
  3. HashMap的遍歷,可能比_LinkedHashMap更耗時,其和_capacity呈線性關係。
  4. LinkedHashMap是非線程安全的,併發出錯時,會快速失敗,拋出ConcurrentModificationException。可以使用_Collections.synchronizedMap(new LinkedHashMap(…));_
  5. LinkedHashMap非常適合於構建LRU緩存least-recently Used)。
  6. 並不是所有的adds、delete操作都會造成LinkedHashMap結構的變更。

_insertion-ordered 模式下,_修改一個已經存在的key,對應的值,並不會造成結構的變更
access-ordered 模式下,get將造成結構的變更

LinkedHashMap的一些概念


```java // 頭結點,同時也是最早插入的節點。 transient LinkedHashMap.Entry

// 尾結點,同時也是最後插入的節點。
transient LinkedHashMap.Entry<K,V> tail;

// 繼承 Node,爲數組的每個元素增加了 before 和 after 屬性。
// LinkedHashMap 的數據結構很像是把 LinkedList 的每個元素換成了 HashMap 的 Node,像是兩者的結合體.
// 也正是因爲增加了這些結構,從而能把 Map 的元素都串聯起來,形成一個鏈表,而鏈表就可以保證順序了,就可以維護元素插入進來的順序。
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);
}
}

// 控制兩種訪問模式的字段,默認 false
// true 按照訪問順序,會把經常訪問的 key 放到隊尾
// false 按照插入順序提供訪問
final boolean accessOrder;

<a name="rquVh"></a>
# 常用方法分析
<a name="yKIWn"></a>
## LinkedHashMap() (無參構造方法)
LinkedHashMap 在無參構造方法中,將accessOrder設置爲false,即按照插入順序訪問。它的_capacity、與HashMap的默認的一致,爲16、0.75。
```java
public LinkedHashMap() {
    super();
    accessOrder = false;
}

LinkedHashMap按順序插入節點(put)

首先,put是直接調用HashMap#put方法。從LinkedHashMap 的重要概念中,可以看到,存在一個繼承了HashMap.Node的Entry。
當前put時,即會創建LinkedHashMap#Entry對象。並且,LinkedHashMap中重寫了HashMap#put中調用的newNodeafterNodeInsertionafterNodeAccess方法。
put的具體流程,可以參照HashMap源碼。這裏來看LinkedHashMap重寫的newNode方法。

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

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    // 備份尾節點
    LinkedHashMap.Entry<K,V> last = tail;
    // 將新插入的節點,作爲尾節點
    tail = p;
    
    // 如果原先尾節點爲null(原先鏈表爲空,現在有一個元素)
    if (last == null)
        // 將p也作爲頭結點(只有一個元素的鏈表,head=tail=唯一的元素)
        head = p;
    else {
        // 鏈表有數據,則建立 前後 指向
        p.before = last;
        last.after = p;
    }
}

LinkedHashMap 通過新尾節點,給每個節點增加 before、after 屬性,每次新增時,都把節點追加到尾節點等手段,在新增的時候,就已經維護了按照插入順序的鏈表結構了。

遍歷

LinkedHashMap可以通過以下,實現的Map接口中的方法,進行遍歷。
image.png
當然,也可以通過迭代器來進行遍歷。如map.entrySet().iterator();得到迭代器,再通過hasNext方法判斷是否有下一個節點後,最後通過next來訪問下一個節點。
其中的實現都依賴LinkedHashIterator。

final class LinkedEntryIterator extends LinkedHashIterator implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { 
        return nextNode(); 
    }
}

// 下個節點
LinkedHashMap.Entry<K,V> next;
// 當前節點
LinkedHashMap.Entry<K,V> current;
// 期望的結構版本號
int expectedModCount;

// 構造方法
LinkedHashIterator() {
    // 初始化時,頭結點即爲 next
    next = head;
    expectedModCount = modCount;
    current = null;
}

// 是否還有下個節點
public final boolean hasNext() {
    return next != null;
}

// 獲取下個節點
final LinkedHashMap.Entry<K,V> nextNode() {
    LinkedHashMap.Entry<K,V> e = next;
    
    // 判斷結構版本號 是否有變更。有變更的話拋出錯誤
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    
    // 沒有下個節點
    if (e == null)
        throw new NoSuchElementException();
    
    // 有下個節點,通過e.after,取得下個節點,並賦值給next
    current = e;
    next = e.after;
    
    // 返回當前節點
    return e;
}

LRU 最近最少使用

使用示例

這裏,是採用了LinkedHashMap的三個參數的構造方法,其源碼如下。其中最主要的,是指定了accessOrdertrue (默認爲false)。 true 按照訪問順序,會把經常訪問的 key 放到隊尾,get時會造成結構的變更; false 按照插入順序提供訪問。

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

LRU大概的意思就是經常訪問的元素會被追加到隊尾,這樣不經常訪問的數據自然就靠近隊頭,然後我們可以通過設置刪除策略,比如當 Map 元素個數大於3時。

public static void main(String[] args) {
    // 初始化時,主要指定了accessOrder爲true
    LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>(16,0.75f,true) {

        @Override
        protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
            return this.size() > 3;
        }
    };

    // 放入元素
    map.put("1",1);
    map.put("2",2);
    map.put("3",3);
    map.put("4",4);

    // 輸出,並測試LRU策略
    System.out.println(map);
    map.get("2");
    System.out.println(map);
    map.get("3");
    System.out.println(map);
    map.get("4");
    System.out.println(map);
}
// {2=2, 3=3, 4=4}
// {3=3, 4=4, 2=2}
// {4=4, 2=2, 3=3}
// {2=2, 3=3, 4=4}

可以看到:我們往map中放置四個元素,但輸出結果只有三個元素。1 不見了,這個是因爲在每次put時,會調用afterNodeInsertion方法。該方法在允許刪除節點時,並在removeEldestEntry方法返回true的情況下,(我們在用例中重寫了removeEldestEntry方法)會移除頭節點。源碼如下:

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

當我們調用get方法後,發現輸出的元素順序發生改變。被訪問元素移動到鏈表尾部,這個體現了最經常被訪問的節點會被移動到鏈表尾部。在調用get後,頭節點即爲最早插入-最少被訪問的節點。

get方法源碼

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    
    // accessOrder爲true時,纔會去將訪問的元素,放到鏈表尾部
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章