LinkedHashMap如何保證順序性

一. 前言

先看一個例子,咱們想在頁面展現一週內的消費變化狀況,用echarts面積圖進行展現。以下:
html

咱們在後臺將數據構造完成java

HashMap<String, Integer> map = new HashMap<>();
map.put("星期一", 40);
map.put("星期二", 43);
map.put("星期三", 35);
map.put("星期四", 55);
map.put("星期五", 45);
map.put("星期六", 35);
map.put("星期日", 30);

然而頁面上一展現,發現並不是如此,咱們打印出來看,發現順序並不是咱們所想,先put進去的先get出來node

for (Map.Entry<String, Integer> entry : map.entrySet()){
    System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue());
}
/**
 * 結果以下:
 * key: 星期二, value: 40
 * key: 星期六, value: 35
 * key: 星期三, value: 50
 * key: 星期四, value: 55
 * key: 星期五, value: 45
 * key: 星期日, value: 65
 * key: 星期一, value: 30
 */

那麼如何保證預期展現結果如咱們所想呢,這個時候就須要用到LinkedHashMap實體。數組

二. 初識LinkedHashMap

首先咱們把上述代碼用LinkedHashMap進行重構數據結構

LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("星期一", 40);
map.put("星期二", 43);
map.put("星期三", 35);
map.put("星期四", 55);
map.put("星期五", 45);
map.put("星期六", 35);
map.put("星期日", 30);
for (Map.Entry<String, Integer> entry : map.entrySet()){
    System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue());
}

這個時候,結果正如咱們所預期架構

key: 星期一, value: 40
key: 星期二, value: 43
key: 星期三, value: 35
key: 星期四, value: 55
key: 星期五, value: 45
key: 星期六, value: 35
key: 星期日, value: 30

LinkedHashMap繼承了HashMap類,是HashMap的子類,LinkedHashMap的大多數方法的實現直接使用了父類HashMap的方法,關於HashMap在前面的章節已經講過了,《HashMap原理(一) 概念和底層架構》《HashMap原理(二) 擴容機制及存取原理》app

LinkedHashMap能夠說是HashMap和LinkedList的集合體,既使用了HashMap的數據結構,又借用了LinkedList雙向鏈表的結構(關於LinkedList可參考Java集合 LinkedList的原理及使用),那麼這樣的結構如何實現的呢,咱們看一下LinkedHashMap的類結構
echarts

咱們看到LinkedHashMap中定義了一個Entry靜態內部類,定義了5個構造器,一些成員變量,如head,tail,accessOrder,並繼承了HashMap的方法,同時實現了一些迭代器方法。咱們先看一下Entry類this

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

咱們看到這個靜態內部類很簡單,繼承了HashMap的Node內部類,咱們知道Node類是HashMap的底層數據結構,實現了數組+鏈表/紅黑樹的結構,而Entry類保留了HashMap的數據結構,同時經過before,after實現了雙向鏈表結構(HashMap中Node類只有next屬性,並不具有雙向鏈表結構)。那麼before,after和next到底什麼關係呢。
code

看上面的結構圖,定義了頭結點head,當咱們調用迭代器進行遍歷時,經過head開始遍歷,經過before屬性能夠不斷找到下一個,直到tail尾結點,從而實現順序性。而在同一個hash(在上圖中表現了同一行)鏈表內部after和next效果是同樣的。不一樣點在於before和after能夠鏈接不一樣hash之間的鏈表。

前面咱們發現數據結構已經徹底支持其順序性了,接下來咱們再看一下構造方法,看一下比起HashMap的構造方法是否有不一樣。

// 構造方法1,構造一個指定初始容量和負載因子的、按照插入順序的LinkedList
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}
// 構造方法2,構造一個指定初始容量的LinkedHashMap,取得鍵值對的順序是插入順序
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}
// 構造方法3,用默認的初始化容量和負載因子建立一個LinkedHashMap,取得鍵值對的順序是插入順序
public LinkedHashMap() {
    super();
    accessOrder = false;
}
// 構造方法4,經過傳入的map建立一個LinkedHashMap,容量爲默認容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子爲默認值
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super(m);
    accessOrder = false;
}
// 構造方法5,根據指定容量、裝載因子和鍵值對保持順序建立一個LinkedHashMap
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

咱們發現除了多了一個變量accessOrder以外,並沒有不一樣,此變量到底起了什麼做用?

/**
 * The iteration ordering method for this linked hash map: <tt>true</tt>
 * for access-order, <tt>false</tt> for insertion-order.
 *
 * @serial
 */
final boolean accessOrder;

經過註釋發現該變量爲true時access-order,即按訪問順序遍歷,若是爲false,則表示按插入順序遍歷。默認爲false,在哪些地方使用到該變量了,同時怎麼理解?咱們能夠看下面的方法介紹

二. put方法

前面咱們提到LinkedHashMap的put方法沿用了父類HashMap的put方法,但咱們也提到了像LinkedHashMap的Entry類就是繼承了HashMap的Node類,一樣的,HashMap的put方法中調用的其餘方法在LinkedHashMap中已經被重寫。咱們先看一下HashMap的put方法,這個在《HashMap原理(二) 擴容機制及存取原理》中已經有說明,咱們主要關注於其中的不一樣點

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @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;
    /**
     * 若是當前HashMap的table數組還未定義或者還未初始化其長度,則先經過resize()進行擴容,
     * 返回擴容後的數組長度n
     */
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //經過數組長度與hash值作按位與&運算獲得對應數組下標,若該位置沒有元素,則new Node直接將新元素插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //不然該位置已經有元素了,咱們就須要進行一些其餘操做
    else {
        Node<K,V> e; K k;
        //若是插入的key和原來的key相同,則替換一下就完事了
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        /**
         * 不然key不一樣的狀況下,判斷當前Node是不是TreeNode,若是是則執行putTreeVal將新的元素插入
         * 到紅黑樹上。
         */
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //若是不是TreeNode,則進行鏈表遍歷
        else {
            for (int binCount = 0; ; ++binCount) {
                /**
                 * 在鏈表最後一個節點以後並無找到相同的元素,則進行下面的操做,直接new Node插入,
                 * 但條件判斷有可能轉化爲紅黑樹
                 */
                if ((e = p.next) == null) {
                    //直接new了一個Node
                    p.next = newNode(hash, key, value, null);
                    /**
                     * TREEIFY_THRESHOLD=8,由於binCount從0開始,也便是鏈表長度超過8(包含)時,
                     * 轉爲紅黑樹。
                     */
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                /**
                 * 若是在鏈表的最後一個節點以前找到key值相同的(和上面的判斷不衝突,上面是直接經過數組
                 * 下標判斷key值是否相同),則替換
                 */
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            //onlyIfAbsent爲true時:當某個位置已經存在元素時不去覆蓋
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //最後判斷臨界值,是否擴容。
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

1. newNode方法

首先:LinkedHashMap重寫了newNode()方法,經過此方法保證了插入的順序性。

/**
 * 使用LinkedHashMap中內部類Entry
 */
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;
}
/**
 * 將新建立的節點p做爲尾結點tail,
 * 固然若是存儲的第一個節點,那麼它便是head節點,也是tail節點,此時節點p的before和after都爲null
 * 不然,創建與上一次尾結點的鏈表關係,將當前尾節點p的前一個節點(before)設置爲上一次的尾結點last,
 * 將上一次尾節點last的後一個節點(after)設置爲當前尾結點p
 * 經過此方法實現了雙向鏈表功能,完成before,after,head,tail的值設置
 */
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

2. afterNodeAccess方法

其次:關於afterNodeAccess()方法,在HashMap中沒給具體實現,而在LinkedHashMap重寫了,目的是保證操做過的Node節點永遠在最後,從而保證讀取的順序性,在調用put方法和get方法時都會用到。

/**
 * 當accessOrder爲true而且傳入的節點不是最後一個時,將傳入的node移動到最後一個
 */
void afterNodeAccess(Node<K,V> e) {
    //在執行方法前的上一次的尾結點
    LinkedHashMap.Entry<K,V> last;
    //當accessOrder爲true而且傳入的節點並非上一次的尾結點時,執行下面的方法
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //p:當前節點
        //b:當前節點的前一個節點
        //a:當前節點的後一個節點;
        
        //將p.after設置爲null,斷開了與後一個節點的關係,但還未肯定其位置
        p.after = null;
        /**
         * 由於將當前節點p拿掉了,那麼節點b和節點a之間斷開了,咱們先站在節點b的角度創建與節點a
         * 的關聯,若是節點b爲null,表示當前節點p是頭結點,節點p拿掉後,p的下一個節點a就是頭節點了;
         * 不然將節點b的後一個節點設置爲節點a
         */
        if (b == null)
            head = a;
        else
            b.after = a;
        /**
         * 由於將當前節點p拿掉了,那麼節點a和節點b之間斷開了,咱們站在節點a的角度創建與節點b
         * 的關聯,若是節點a爲null,表示當前節點p爲尾結點,節點p拿掉後,p的前一個節點b爲尾結點,
         * 可是此時咱們並無直接將節點p賦值給tail,而是給了一個局部變量last(即當前的最後一個節點),由於
         * 直接賦值給tail與該方法最終的目標並不一致;若是節點a不爲null將節點a的前一個節點設置爲節點b
         *
         * (由於前面已經判斷了(last = tail) != e,說明傳入的節點並非尾結點,既然不是尾結點,那麼
         * e.after必然不爲null,那爲何這裏又判斷了a == null的狀況?
         * 以個人理解,java可經過反射機制破壞封裝,所以若是都是反射建立出的Entry實體,可能不會知足前面
         * 的判斷條件)
         */
        if (a != null)
            a.before = b;
        else
            last = b;
        /**
         * 正常狀況下last應該也不爲空,爲何要判斷,緣由和前面同樣
         * 前面設置了p.after爲null,此處再將其before值設置爲上一次的尾結點last,同時將上一次的尾結點
         * last設置爲本次p
         */
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        //最後節點p設置爲尾結點,完事
        tail = p;
        ++modCount;
    }
}

咱們前面說到的linkNodeLast(Entry e)方法和如今的afterNodeAccess(Node e)都是將傳入的Node節點放到最後,那麼它們的使用場景如何呢?

在前面講解HashMap時,提到了HashMap的put流程,若是在對應的hash位置上尚未元素,那麼直接new Node()放到數組table中,這個時候對應到LinkedHashMap中,調用了newNode()方法,就會用到linkNodeLast(),將新node放到最後,而若是對應的hash位置上有元素,進行元素值的覆蓋時,就會調用afterNodeAccess(),將本來可能不是最後的node節點拿到了最後。如

LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("1月", 20);
//此時就會調用到linkNodeLast()方法,也會調用afterNodeAccess()方法,但會被阻擋在
//if (accessOrder && (last = tail) != e) 以外
map.put("2月", 30);
map.put("3月", 65);
map.put("4月", 43);
//這時不會調用linkNodeLast(),會調用afterNodeAccess()方法將key爲「1月」的元素放到最後
map.put("1月", 35);
//這時不會調用linkNodeLast(),會調用afterNodeAccess()方法將key爲「2月」的元素放到最後
map.get("2月");
//調用打印方法
for (Map.Entry<String, Integer> entry : map.entrySet()){
    System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue());
}

結果以下:

key: 3月, value: 65
key: 4月, value: 43
key: 1月, value: 35
key: 2月, value: 30

而若是是執行下面這段代碼,將accessOrder改成false

LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, false);
map.put("1月", 20);
//此時就會調用到linkNodeLast()方法,也會調用afterNodeAccess()方法,但會被阻擋在
//if (accessOrder && (last = tail) != e) 以外
map.put("2月", 30);
map.put("3月", 65);
map.put("4月", 43);
//這時不會調用linkNodeLast(),會調用afterNodeAccess()方法將key爲「1月」的元素放到最後
map.put("1月", 35);
map.get("2月");
//調用打印方法
for (Map.Entry<String, Integer> entry : map.entrySet()){
    System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue());
}

結果以下:

key: 1月, value: 35
key: 2月, value: 30
key: 3月, value: 65
key: 4月, value: 43

你們看到區別了嗎,accessOrder爲false時,你訪問的順序就是按照你第一次插入的順序;而accessOrder爲true時,你任何一次的操做,包括put、get操做,都會改變map中已有的存儲順序。

3. afterNodeInsertion方法

咱們看到在LinkedHashMap中還重寫了afterNodeInsertion(boolean evict)方法,它的目的是移除鏈表中最老的節點對象,也就是當前在頭部的節點對象,但實際上在JDK8中不會執行,由於removeEldestEntry方法始終返回false。看源碼:

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);
    }
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

三. get方法

LinkedHashMap的get方法與HashMap中get方法的不一樣點也在於多了afterNodeAccess()方法

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

在這裏就再也不多講了,getNode()方法在HashMap章節已經講過,而前面剛把afterNodeAccess講了。

四.remove方法

remove方法也直接使用了HashMap中的remove,在HashMap章節並無講解,由於remove的原理很簡單,經過傳遞的參數key計算出hash,據此可找到對應的Node節點,接下來若是該Node節點是直接在數組中的Node,則將table數組該位置的元素設置爲node.next;若是是鏈表中的,則遍歷鏈表,直到找到對應的node節點,而後創建該節點的上一個節點的next設置爲該節點的next。

LinkedHashMap重寫了其中的afterNodeRemoval(Node e),該方法在HashMap中沒有具體實現,經過此方法在刪除節點的時候調整了雙鏈表的結構。

void afterNodeRemoval(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    //將待刪除節點的before和after都設置爲null
    p.before = p.after = null;
    /**
     * 若是節點b爲null,表示待刪除節點p爲頭部節點,該節點拿掉後,該節點的下一個節點a就爲頭部節點head
     * 不然設置待刪除節點的上一個節點b的after屬性爲節點a 
     */
    if (b == null)
        head = a;
    else
        b.after = a;
    /**
     * 若是節點a爲null,表示待刪除節點p爲尾部節點,該節點拿掉後,該節點的上一個節點a就爲尾部節點tail
     * 不然設置待刪除節點的下一個節點a的before屬性爲節點b 
     */
    if (a == null)
        tail = b;
    else
        a.before = b;
}

五. 總結

LinkedHashMap使用的也較爲頻繁,它基於HashMap,用於HashMap的特色,又增長了雙鏈表的結構,從而保證了順序性,本文主要從源碼的角度分析其如何保證順序性,accessOrder的解釋,以及經常使用方法的闡釋,如有不對之處,請批評指正,望共同進步,謝謝!

相關文章
相關標籤/搜索