先看一個例子,咱們想在頁面展現一週內的消費變化狀況,用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<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,在哪些地方使用到該變量了,同時怎麼理解?咱們能夠看下面的方法介紹
前面咱們提到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; }
首先: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; } }
其次:關於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中已有的存儲順序。
咱們看到在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; }
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方法也直接使用了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的解釋,以及經常使用方法的闡釋,如有不對之處,請批評指正,望共同進步,謝謝!