Java容器之LinkedHashMap源碼分析(看看確定不點進來?進來真不點?)

  前面幾篇博客Java容器之Hashtable源碼分析Java容器之HashMap源碼分析分別分析了HashMapHashtable的源碼,此篇博客我們分析一下LinkedHashMap容器,看看它又有什麼花樣。

註明:以下源碼分析都是基於jdk 1.8.0_221
在這裏插入圖片描述

一、LinkedHashMap容器概述(一圖以蔽之

  LinkedHashMap類的申明如下:

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

  可以看出LinkedHashMapHashMap子類(建議先看看HashMap再來看捏 → Java容器之HashMap源碼分析),大膽推測一下,LinkedHashMap底層用HashMap存儲節點,只是展示給你的是一個Linked狀態。

1、LinkedHashMap容器的自我定位

  看過博主前面HashMapHashtable源碼分析或者瞭解一點Map結構的小夥伴,肯定知道HashMap的定位是單線程處理(讀、寫高效),Hashtable的定位是支持併發讀寫(引入鎖機制,降低效率),那麼這個LinkedHashMap的定位又是什麼呢?先來看個例子:

public class Main {
    public static void main(String[] args) {
        // 新建一個linkedHashMap
        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
        // 依次插入三個key-value
        linkedHashMap.put("老王", "我是誰");
        linkedHashMap.put("老趙", "我在哪");
        linkedHashMap.put("老李", "我要幹嘛");
        // 遍歷輸出linkedHashMap中的key-value
        for (Map.Entry<String, String> entry : linkedHashMap.entrySet()) {
            System.out.println(entry.getKey() + "\t" + entry.getValue());
        }
    }
}

在這裏插入圖片描述
  可以看出LinkedHashMap能按照插入的順序給你輸出,即一種Linked的狀態,其實這就是它的最主要特點。

2、LinkedHashMap容器數據結構圖

  LinkedHashMap容器比較簡單,所有的key-value節點都是存在HashMap容器中(前面說過LinkedHashMap繼承HashMap),只是加上beforeafter指針。一句話概括就是 LinkedHashMap = HashMap + 雙鏈表。
在這裏插入圖片描述
註明:在JDK1.8中,HashMaphash桶實現引入了紅黑樹(參考Java容器之HashMap源碼分析),但是我們此處主要是討論LinkedHashMapLinked屬性,爲了方便畫圖,所以結構示意圖沒有畫出來。

二、LinkedHashMap類的屬性與內部類

1、Entry內部類

  在HashMap存儲key-value的類爲Node(屬性主要包括hashkeyvalue),在LinkedHashMap類用於要記錄Linked狀態,所以增加一個繼承Node的內部類EntryEntry類只是增加beforeafter指針而已,在LinkedHashMapEntry對象存key-value

/**
 * Entry繼承HashMap.Node
 */
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);
    }
}

2、LinkedHashMap新增屬性

  LinkedHashMap類中新增三個主要的屬性,headtail分別指向外部展示的list頭部、尾部,accessOrder屬性的作用是list中節點順序是否隨get方法調用而改變。

/**
 * Linked list 頭部
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * Linked list 尾部
 */
transient LinkedHashMap.Entry<K,V> tail;

/**
 * = true,每次調用get方法都會將查找到的key-value放入尾部
 */
final boolean accessOrder;

備註:這裏的accessOrder有人翻爲訪問順序,其實根本不用翻譯這個詞。因爲它容器與插入順序搞混,從而搞亂LinkedHashMap有序到底什麼有序。你只要記住LinkedHashMapkey-value插入順序有序,accessOrder變量的作用是調用get方法是否會將查找到的節點放入尾部即可!

三、LinkedHashMap類構造器

  LinkedHashMap類一共有5個構造器,涉及initialCapacityloadFactoraccessOrder三個參數的初始化。

/**
 * @param  initialCapacity 初始容量(hashMap.table數組長度)
 * @param  loadFactor      負載因子(hashMap中的參數,
 * 							initialCapacity * loadFactor就是HashMap容器可存放的key-value閾值,這裏不再重複介紹了)
 */
public LinkedHashMap(int initialCapacity, float loadFactor) {
	// super關鍵字,調用父類HashMap的響應構造器
    super(initialCapacity, loadFactor);
    // 默認維護的list不隨get方法調用改變
    accessOrder = false;
}

/**
 * @param  initialCapacity 初始容量(hashMap.table數組長度)
 */
public LinkedHashMap(int initialCapacity) {
	// super關鍵字,調用父類HashMap的響應構造器,負載因子默認是0.75
    super(initialCapacity);
    // 默認維護的list不隨get方法調用改變
    accessOrder = false;
}

/**
 * HashMap.table長度默認16,HashMap默認負載因子爲0.75
 */
public LinkedHashMap() {
	// super關鍵字,調用父類HashMap的響應構造器
    super();
    // 默認維護的list不隨get方法調用改變
    accessOrder = false;
}

/**
 * 複製構造方法
 */
public LinkedHashMap(Map<? extends K, ? extends V> m) {
	// 先初始化父類HashMap
    super();
    accessOrder = false;
    // 然後將容器m中的所有元素複製到當前容器
    putMapEntries(m, false);
}

/**
 * @param  initialCapacity 初始容量(hashMap.table數組長度)
 * @param  loadFactor      負載因子(hashMap中的參數,initialCapacity * loadFactor就是HashMap容器可存放的key-value閾值
 * @param  accessOrder     維護的list是否隨get方法調用而修改
 */
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

四、Linked list維護原理分析

  前面多次提及到LinkedHashMap容器中維護了一個鏈表使得展示給外界是按key-value插入順序排列的,下面我們來看看它到底是如何維護這個虛擬的Linked list

1、相關方法

  與Linked list維護相關的方法有三個,分別如下:

①、afterNodeRemoval方法

  afterNodeRemoval,顧名思義,當刪除了某個節點後需要做的操作。
在這裏插入圖片描述

/**
 * afterNodeRemoval此方法是將節點e從list中刪除,調用hashMap.remove方法已經將它從hashMap中刪除(注意看前面的LinkedHashMap數據結構示意圖,有兩套指針,
 * 一套是HashMap中的hash桶內元素連接的黑色指針,只要把節點上下關係移除即可
 * 另外一套是有顏色的指針,它是維護list的指針,從hash桶中刪除後,還有從list中刪除)
 */
void afterNodeRemoval(Node<K,V> e) {
	// 標記節點e、e.before、e.after
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    
    // 第一步:p的前後置空,選擇e已經從鏈表中移除
    p.before = p.after = null;
    
    // 第二步:還要將e前驅.after = e的後繼
    if (b == null)
    	// b是操作前e.before==null,說明e是頭結點,head更新爲e.after
        head = a;
    else
    	// 否則更新e的前驅.after == e的後繼節點
        b.after = a;
    
    // 第三步:將e的後繼.before = e的前驅
    if (a == null)
    	// 如果沒刪除前e.after後繼 == null,說明e處於list的尾部,將tail指向e的前驅節點
        tail = b;
    else
    	// 否則更新e的後繼.before  = e的前驅
        a.before = b;
}

②、afterNodeInsertion方法

  afterNodeInsertion,顧名思義,當插入了某個節點後需要做的操作。不過這個方法有些奇怪,與我們的預期不符,但是LinkedHashMap插入節點後做的啥操作呢?先賣個關子~,繼續看吧 ~

/**
 * 按照之前的想法,插入一個節點後應該是將插入的節點放入tailde後面,但是這裏並不是這樣幾的
 */
void afterNodeInsertion(boolean evict) { // possibly remove eldest
     LinkedHashMap.Entry<K,V> first;
     // LinkedHashMap中的removeEldestEntry方法永遠返回false(方法體見下文),也就說這個本方法不會執行任何操作。。。
     if (evict && (first = head) != null && removeEldestEntry(first)) {
         K key = first.key;
         removeNode(hash(key), key, null, false, true);
     }
}
/**
 * removeEldestEntry方法永遠返回false
 */
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

③、afterNodeAccess方法

  afterNodeAccess,從名字來看,這個方法應該是訪問了某個key-value後的操作(還記得我們之前特意提到的屬性accessOrder嗎?不記得往前翻翻)。當accessOrder == true時,每當我們調用get方法,都會將查找到的key-value移動到鏈表的尾端。
在這裏插入圖片描述

/**
 * 此方法的作用是將剛剛訪問的節點e放到鏈表的尾端
 */
void afterNodeAccess(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> last;
    // accessOrder = true 時 訪問節點後才需要置於尾端
    // 如果e本身就在尾端,那就不需要操作
    if (accessOrder && (last = tail) != e) {
    	// 記錄節點e、e的前驅、e的後繼
        LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // 第一步:現將p.after置空
        p.after = null;
        // 第二步:將e的前驅.after 連接上e的後繼
        if (b == null)
        	// b記錄e的前驅,前驅爲null,則e在表頭,head置爲e的後繼
            head = a;
        else
        	// 否則 e的前驅.after = e的後繼
            b.after = a;
        // 第三步:將e的後繼.before 連接上e的前驅
        if (a != null)
        	// e的後繼 != null,將e後繼.before = e的前驅
            a.before = b;
        else
        	// 否則e的後繼 == null,即在e表尾(這裏有點多餘,前面已經判斷在表尾不操作。。。)
            last = b;
        // 第四步:將節點e接入到鏈表的尾端
        if (last == null)
        	// last == null,鏈表爲空,head = p
            head = p;
        else {
        	// p.before 指向last(鏈表尾端),尾端.after = p
            p.before = last;
            last.after = p;
        }
        // 第四步:更新鏈表新尾端tail
        tail = p;
        // 鏈表結構性調整,修改次數自增
        ++modCount;
    }
}

2、維護鏈表方法調用

  前面介紹了維護鏈表的方法,但是還沒介紹哪裏調用了捏,別急,這就來說了~

①、get方法調用的調整

  LinkedHashMap重寫了get方法。

public V get(Object key) {
    Node<K,V> e;
    // 調用HashMap.getNode方法,前面說過LinkedHashMap中含有HashMap存放數據,當然得調用這個方法查找,這不是關鍵,沒找到就返null
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // accessOrder = true,訪問之後要將訪問節點放到鏈表尾端,afterNodeAccess方法上面剛講的,不用再說了吧~
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

②、getOrDefault方法調用的調整

  LinkedHashMap提供一個getOrDefault方法,沒找到就返回提供的默認值。

/**
 * {@inheritDoc}
 */
public V getOrDefault(Object key, V defaultValue) {
   Node<K,V> e;
   // 調用HashMap.getNode查找,沒找到就返回默認值
   if ((e = getNode(hash(key), key)) == null)
       return defaultValue;
   // accessOrder = true,訪問之後要將訪問節點放到鏈表尾端,afterNodeAccess方法上面剛講的,不用再說了吧~
   if (accessOrder)
       afterNodeAccess(e);
   return e.value;
}

③、put方法調用的調整

  在LinkedHashMap中並沒有重寫put相關的方法,直接調用HashMap父類put相關的方法就行。那麼問題來了,父類HashMap中的put方法沒有定義維護鏈表相關的方法啊,哪如何維護鏈表呢?

  其實在HashMap類中定了前面介紹的三個方法的空實現!!!
在這裏插入圖片描述
  那麼HashMapput相關的方法有調用這三個方法嗎?請看下面:(在Java容器之HashMap源碼分析詳細分析了這個方法,這裏不再重複,直接看關鍵調用即可。)

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 *
 * @param hash key的hash值(調用hash()方法)
 * @param key 插入鍵值對key
 * @param value 插入鍵值對value
 * @param onlyIfAbsent 設爲true時,表示如果容器已經存在這個key就不進行修改
 * @param evict 爲 false時,表示容器正處於創建(其它map傳入初始化)
 * @return previous value, or null if none
 * 
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 插入成功,調用afterNodeAccess,前面說afterNodeInsertion方法什麼也不幹,
            // 其實插入成功之後調用afterNodeAccess放入尾端,效果是一樣的
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

  這裏可能有部分懂一點面向對象編程OOP的朋友可能會有疑問,子類重寫的方法(前面介紹的三個維護鏈表的方法),在父類中調用會調用到重寫的方法還是本類的方法(三個空實現)?
  咳咳,這裏我裝下筆,面向對象編程OOP三大特徵,封裝繼承多態
  首先我們存key-valueLinkedHashMap對象吧,那麼調用父類hashMap.get方法時,此時還是不是LinkedHashMap對象?顯然是吧,那麼在hashMap.get中調用afterNodeAccessafterNodeInsertion方法同樣是LinkedHashMap對象吧,那當然是調用LinkedHashMap.afterNodeAccessLinkedHashMap.afterNodeInsertion方法了(這裏就是一個多態場景!)如果你不懂話,那就算了,記住LinkedHashMap對象調用hashMap.get時,會調用子類LinkedHashMap中的方法即可。

④、remove方法調用的調整

  在LinkedHashMap中並沒有重寫remove相關的方法,直接調用HashMap父類remove相關的方法就行。那麼HashMap父類remove相關的方法也有調用afterNodeRemoval方法嘛?答案是確定的,附上HashMap父類remove相關的方法源碼(在Java容器之HashMap源碼分析詳細分析了這個方法,這裏不再重複,直接看關鍵調用即可。)

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**
 * Implements Map.remove and related methods.
 *
 * @param hash key對應的hash(調用hash()方法即可計算得到)
 * @param key 待刪除的key-value對應的key
 * @param value key-value對應的value,只有當matchValue == true時,此參數纔有意義
 * @param matchValue 如果設爲true,刪除的時候還需要匹配value才能刪
 * @param movable 設爲false,表示刪除成功了不移動其它節點(一般設爲true,即刪除節點後需要進行調整)
 * @return 刪除成功則返回key對應的value,否則返null
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            // 移除成功後,同樣調用 afterNodeRemoval 方法進行了鏈表的維護
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

  上面介紹了getremoveput三種方法對應的鏈表維護調整,可能會有部分小夥伴覺得擴容也需要維護鏈表。但事實上擴容是不需要調整鏈表的,因爲擴容的本質是將key-value移動到其它hash桶table數組其它鏈表下),並沒有改變放入容器的順序,不信你可以想想把LinkedHashMap容器數據結構示意圖中的一個節點移動到其它hash桶下,看看他會不會改變list順序。

五、總結

  LinkedHashMapHashMap的子類,並且key-value都是放在HashMap.table中存儲,只是把所有key-value按照放入容器的順序用雙鏈表串了起來。那麼HashMap的特徵LinkedHashMap都有,比如不支持併發讀寫、允許放入keyvalue爲空的key-valueLinkedHashMap還有一個特性就是可以按照放入容器的順序取出來。
  總的來說,一句話 LinkedHashMap = HashMap + list
在這裏插入圖片描述

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