Java基礎之LinkedHashMap原理分析,瞭解一下

我們平時用LinkedHashMap的時候,都會寫下面這段

LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("student", "333");
map.put("goods", "222");
map.put("product", "222");

然後我們通常都會去看 put 方法,但是我們點到LinkedHashMap內部後,發現沒有put方法,這是爲什麼呢?

其實這個不難,因爲LinkedHashMap繼承子HashMap

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

這就好理解了。因爲put是集成自HashMap,那麼LinkedHashMap的數據也是 數組+鏈表 的形式存儲的嗎?我們慢慢往下看

在HashMap中,put一個數據的時候,會調用一個newNode方法來創建節點,而LinkedHashMap重寫了該方法

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

在每次創建節點的時候,都調用了一次linkNodeLast方法,來拼接鏈表。
tail代表鏈表尾巴,head代表鏈表腦袋
entry.before代表前驅
entry.after代表後置

private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    LinkedHashMapEntry<K,V> last = tail;
    tail = p;
    //判斷尾部是否是空的,爲空就認爲鏈表沒創建,拼接在頭上
    if (last == null)
        head = p;
    else {
        //在最後一個節點的before上放前一個節點
        p.before = last;
        //在after上放置當前節點
        last.after = p;
    }
}

通過這個方法,我們就對LinkedHashMap有了一個初步的瞭解了。before和after分別指向前驅和後置,這是典型的雙向鏈表的結構,稍等,我去畫個賦予靈魂的配圖。

有了這個圖就好理解多了~
當我們 put 數據的時候,除了創建節點之外,還有一個操作,就是HashMap會回調一個 afterNodeInsertion方法,我們看一下LinkedHashMap的實現

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<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;
}

其實這個方法是移出最老的節點,但這段代碼在jdk1.8裏就不在被執行了,除非你自己集成LinkedHashMap重寫removeEldestEntry方法。因爲removeEldestEntry=false,OK,當我們在put數據的時候,整個雙鏈表就建立起來了,接下來我們看下get有什麼操作吧

final boolean accessOrder;
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;
}

LinkedHashMap的get操作首先會從 HashMap 維護的數據中通過hash獲取Node,然後判斷accessOrder屬性,如果等於true就調用afterNodeAccess方法

那麼accessOrder是個什麼呢?有什麼用呢?
其實accessOrder是個標記位,用來標記數據是否按照訪問順序處理,如果設置爲true,那麼我們每次訪問數據,這個數據都會被移動到鏈表尾部,就會導致鏈表尾部的訪問頻次是最高的(年老的變量),鏈表頭部是訪問頻次最低的(年輕的變量),這個特性正好適合做LRU緩存。如果設置爲false,也就是默認的模式,那麼就是按照存儲順序存儲數據,訪問也不會觸發置尾操作。我們接下來看一下它是怎麼做到的置尾吧。

首先通過這個構造方法,把accessOrder初始化成true,默認是false

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

然後我們試一下效果

Map<String, String> map = new LinkedHashMap<>(
        1 << 4, 0.75f, true
);
map.put("node1", "node1");
map.put("node2", "node1");
map.put("node3", "node1");
map.put("node4", "node1");
map.get("node1");
System.out.println(map);

{node2=node1, node3=node1, node4=node1, node1=node1}

和預期一樣,訪問了一次node1,它就把node1放在鏈表的尾巴上了,這個操作主要是在afterNodeAccess內,我們接下來看下是怎麼實現的吧

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;
        //如果我們get的數據是表頭的數據,那麼表頭就需要更新爲表頭的後置
        //比如node1->node2->node3,我們獲取node1的時候,node1要跑到隊尾
        //所以node2就是老大
        if (b == null)
            head = a;
        else
            //否則的話,把get的數據的前置節點和get的數據的後置節點連接
            //比如,get node2,node2的before正是node1
            //因爲node2要去隊尾了,所以node1就不能在綁定after爲node2了,要改成node3
            b.after = a;
        //a等於空說明p是隊尾。因爲只有隊尾的後置節點是空的
        if (a != null)
            //把操作數據的後置節點連接上操作數據的前置節點
            //比如,get node2,node的after便是node3
            //node3的before在沒改變的時候是node2,結果node2要去隊尾,所以要連接都node1去
            a.before = b;
        else
            //a等於空說明什麼?說明p的後置節點是空的。說明p可能是隊尾
            last = b;
        //假設last等於b的時候。結果b是空的,按照規則,before爲空就要成爲頭
        if (last == null)
            head = p;
        else {
            //把操作數據的前置節點設置成隊尾,準備去隊尾了。。。
            p.before = last;
            //把剛纔隊尾的後置節點,設置成剛剛操作的node2,實錘了,真的都隊尾了
            last.after = p;
        }
        //執行隊尾賦值
        tail = p;
        ++modCount;
    }
}

這個方法我在囉嗦總結一下吧
1.先把操作數據的前置和後置找處理
2.然後把它前置和它後置做鏈接
3.把它的前置鏈接到之前的隊尾上,再把之前的隊尾的後置鏈接到它身上
4.最後把隊尾改成操作的數據即可
最後再讓我這個靈魂畫手配張圖吧~

最後聊一下resize吧。既然是集成自HashMap,那麼肯定也是到達了擴容閥值就要擴容的

我們去找LinkedhashMap內部,發現沒有重寫resize,那就說明它的擴容是由父類HashMap完成的。

來源:https://www.cnblogs.com/kezhuang/p/13682064.html

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