一. 前言
先看一個例子,我們想在頁面展示一周內的消費變化情況,用echarts面積圖進行展示。如下:
我們在后台將數據構造完成
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出來
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原理(二) 擴容機制及存取原理》。
LinkedHashMap可以說是HashMap和LinkedList的集合體,既使用了HashMap的數據結構,又借用了LinkedList雙向鏈表的結構(關於LinkedList可參考Java集合 LinkedList的原理及使用),那么這樣的結構如何實現的呢,我們看一下LinkedHashMap的類結構
我們看到LinkedHashMap中定義了一個Entry靜態內部類,定義了5個構造器,一些成員變量,如head,tail,accessOrder,並繼承了HashMap的方法,同時實現了一些迭代器方法。我們先看一下Entry類
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到底什么關系呢。
看上面的結構圖,定義了頭結點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的解釋,以及常用方法的闡釋,若有不對之處,請批評指正,望共同進步,謝謝!