集合框架——LinkedHashMap實現原理詳解

我們已經講解了HashMap的實現原理,LinkedHashMap是HashMap的子類,在其基礎上實現了元素的排序功能,接下來對LinkedHashMap的實現進行一個瞭解。

目錄

底層數據結構

查詢操作get()

插入操作put()/putAll()

刪除remove()方法


  • 底層數據結構

LinkedHashMap就是實現了LRU(Least Recent Use, 最近最少使用)排序的HashMap,LinkedHashMap就是在HashMap的基礎上,在Entry<key,value>對象中添加了before和after兩個指針,構成一個雙向鏈表(最少使用的在head,最多使用的在tail),用來存儲元素的訪問順序(get和put都算訪問),這個雙向鏈表的重要操作就是,當訪問了一個元素,那麼把他從雙向鏈表中刪除,並把它放進雙向鏈表的表頭。

LinkedHashMap 很多方法直接繼承自 HashMap,僅爲維護雙向鏈表覆寫了部分方法。

LinkedHashMap的節點定義如下:

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

 我了便於理解,我們對LinkedHashMap做如下操作:

插入a——>插入b——>插入c——>訪問a

那麼我們可以得到按照訪問順序來說,排序應該是:b、c、a

此時的LinkedHashMap的結構如下:

 LinkedHashMap定義了排序模式accessOrder,該屬性爲boolean型變量,對於訪問順序,爲true;對於插入順序,則爲false。

LinkedHashMap的處理思路如下:除了像HashMap一樣插入和查詢節點,還需要維護循環雙向鏈表,accessOrder爲true則雙向鏈表維護訪問順序,我們認爲get和put操作都是訪問操作,也就是說這兩個操作需要對雙向鏈表進行操作。那麼是如何對雙向鏈表進行處理的呢?當有相關訪問操作時,在雙向鏈表中刪掉當前節點,並將當前節點添加到雙向鏈表的尾部。

  • 查詢操作get()

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)//getNode直接繼承父類的getNode方法
            return null;
        if (accessOrder)//按訪問順序排序
            afterNodeAccess(e);//對雙向鏈表進行操作,將節點e移到雙向鏈表的尾部
        return e.value;
    }

那麼 afterNodeAccess(Node<K,V> e)是怎麼實現的呢?以下代碼和註釋進行了說明:

 void afterNodeAccess(Node<K,V> e) { // move node to last即把最常用的放到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;//因爲要把p放到最後,因此p.after是null
            if (b == null)//如果p在head(最前面),那麼去掉p以後p.after變成head
                head = a;
            else
                b.after = a;//否則b直接鏈接a
            if (a != null)
                a.before = b;//如果p在鏈表中間,那麼去掉p以後b直接鏈接a
            else
                last = b;//如果p已經在最後,那麼去掉p以後last變成b
//以上代碼是刪掉p,接下來開始把p插到鏈表尾部
            if (last == null)
                head = p;//如果鏈表中只有p一個節點,那麼刪掉p以後則沒有節點,此時last==null,重新插入p節點爲head
            else {//否則將p放在鏈表尾部即可
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }
  • 插入操作put()/putAll()

LinkedHashMap並沒有重寫HashMap的put()方法,那麼他是如何處理雙向鏈表的呢?

通過HashMap的put方法可以看出,其中又兩個方法afterNodeAccess(e)afterNodeInsertion(evict),這兩個方法在HashMap中是空方法,在LinkedHashMap中被重寫(afterNodeAccess方法在上方)。

afterNodeInsertion(evict)方法的作用:如果removeEldestEntry返回true,表示map的容量一定,在插入一個節點的時候,如果map滿了,就需要刪掉最少訪問的一個節點(first),通過重寫removeEldestEntry方法可以實現自定義的 LRU 緩存。

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

afterNodeAccess(e)方法的作用是:調整雙向鏈表的節點順序。

但是有一個問題就是,put方法並沒有重寫,那麼插入的節點是HashMap的節點還是LinkedHashMap的節點呢?可以發現,在HashMap的put()方法中調用newNode()方法來構造節點,而LinkedHashMap重寫了改方法,因此在LinkedHashMap中調用put()方法時構造的是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);//將當前node添加到雙向鏈表的尾部
        return p;
    }
// link at the end of list
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)//如果last==null說明鏈表爲空,那麼直接降p放到head
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }
  • 刪除remove()方法

同樣的,刪除方法也是繼承自HashMap, 只是重寫了afterNodeRemoval(node)方法以便實現雙向鏈表的節點刪除操作

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

參考博客:https://segmentfault.com/a/1190000012964859

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章