一、概述
LinkedHashMap類是繼承自HashMap類,但是在HashMap的數據結構基礎上,使得每個桶的元素又通過新Entry特殊的結構,組成一條雙向鏈表。有了雙向鏈表的結構,就能保證LinkedHashMap的實例在默認情況下能夠保持元素的插入順序。
二、源碼剖析
(1) 類的聲明
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
LinkedHashMap的繼承實現結構比較簡單,就是繼承了HashMap類,然後實現了Map類,讓LinkedHashMap擁有Map的特性。
(2) 元素結構
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);
}
}
LinkedHashMap類的結點Entry類實際是繼承了HashMap類的結點Node類,並且在此基礎上添加了before和after兩個引用,用來記錄雙向鏈表的前後結點。
(3) 成員變量
//序列化標識ID
private static final long serialVersionUID = 3801124242820219131L;
//記錄頭結點
transient LinkedHashMap.Entry<K,V> head;
//記錄尾結點
transient LinkedHashMap.Entry<K,V> tail;
//當accessOrder設置爲false時,會按照插入順序進行排序(在創建新節點的時候,把該節點放到了尾部),當accessOrder爲true時,會按照訪問順序(也就是插入和訪問都會將當前節點放置到尾部,尾部代表的是最近訪問的數據)
final boolean accessOrder;
LinkedHashMap類中除了記錄了頭尾結點外,最重要的設置屬性accessOrder來維持LinkedHashMap的實例中元素的順序,有兩種情況:當accessOrder設置爲false時,會按照插入順序進行排序(在創建新節點的時候,把該節點放到了尾部);當accessOrder爲true時,會按照訪問順序(也就是插入和訪問都會將當前節點放置到尾部,尾部代表的是最近訪問的數據)。
對於成員變量accessOrder來說,使用final關鍵字修飾,表示不能改變:
a、和局部變量的不同點在於,成員變量有默認值,因此必須手動賦值
b、final的成員變量可以定義的時候直接賦值,或者使用構造方法在構造方法體裏面賦值,但是隻能二者選其一
c、如果沒有直接賦值,那就必須保證所有重載的構造方法最終都會對final的成員變量進行了賦值
(4) 構造方法
//無參構造函數
public LinkedHashMap() {
//調用父類HashMap的構造函數(下同)
super();
//設置默認的排序規則爲插入順序(下同)
accessOrder = false;
}
//帶初始容量的構造函數,initialCapacity也只是建議容量,並非最終容量
public LinkedHashMap(int initialCapacity) {
//調用父類HashMap的構造函數
super(initialCapacity);
//設置默認的排序規則爲插入順序
accessOrder = false;
}
//帶初始容量和負載因子的構造函數
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//帶初始容量、負載因子、排序規則的構造函數
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//參數爲Map的構造函數
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
//將原Map中的元素插入新LinkedHashMap中
putMapEntries(m, false);
}
可以看到LinkedHashMap類幾乎所有的構造函數都是調用的父類HashMap的構造函數,只是多了一步設置成員變量accessOrder爲false的操作。當LinkedHashMap()是帶Map的構造函數的時候,就需要調用HashMap的putMapEntries()方法,使得原Map的元素變得有序了,並且順序就爲元素插入順序。
(5) putMapEntries()方法
//將一個Map中的所有元素添加到LinkedHashMap中,排序規則爲evict
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//獲取m的元素個數,必須大於0才執行
int s = m.size();
if (s > 0) {
如果LinkedHashMap中的容器table爲null
if (table == null) { // pre-size
//獲取根據m的元素個數s獲取table的新容量,此時是float類型ft
float ft = ((float)s / loadFactor) + 1.0F;
//判斷新容量是否小於2的30次方MAXIMUM_CAPACITY,小於則取ft的整數部分爲t,大於則取MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果新容量t是否大於了閾值threshold,如果大於了,那麼設置閾值threshold爲大於等於形容量t的最小2次冪
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果容器table不爲null,那麼先判斷m中的元素個數s是否超過了閾值,超過則調用HashMap的resize()方法進行擴容
else if (s > threshold)
resize();
//容器準備就緒,就開始往LinkedHashMap循環放入元素
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//調用HashMap的putVal()方法添加元素
putVal(hash(key), key, value, false, evict);
}
}
}
resize()方法涉及HashMap的動態擴容,是HashMap的核心方法之一,就不再多說,詳細請看《我的jdk源碼(十三):HashMap 一磕到底,追根溯源!》。putVal()方法雖然是寫在HashMap類裏,但是裏面調用的afterNodeAccess()方法,HashMap中並沒具體實現,而是在LinkedHashMap中重寫了該方法,並且LinkedHashMap還重寫了newNode()方法,那麼我們結合putVal()方法和afterNodeAccess()方法一起看一下。
(6) putVal()方法和LinkedHashMap.afterNodeAccess()等方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判斷當前桶是否爲空,空的就需要初始化(resize 中會判斷是否進行初始化)。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根據當前 key 的 hashcode 定位到具體的桶中並判斷是否爲空,爲空表明沒有 Hash 衝突就直接在當前位置創建一個新桶即可。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已經存在元素
else {
Node<K,V> e; K k;
// 如果當前桶有值( Hash 衝突),那麼就要比較當前桶中的key、key 的 hashcode與寫入的 key 是否相等,相等就賦值給e,在第下面會統一進行賦值及返回。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 將第一個元素賦值給e,用e來記錄
e = p;
// 如果當前桶爲紅黑樹,那就要按照紅黑樹的方式寫入數據。
else if (p instanceof TreeNode)
// 放入樹中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 該鏈爲鏈表
else {
//如果是個鏈表,就需要將當前的 key、value 封裝成一個新節點寫入到當前桶的後面(形成鏈表)。
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;
}
// 判斷鏈表中結點的key值與插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循環
break;
// 用於遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表
p = e;
}
}
// 接着判斷當前鏈表的大小是否大於預設的閾值,大於時就要轉換爲紅黑樹。
if (e != null) {
// 記錄e的value
V oldValue = e.value;
// onlyIfAbsent爲false或者舊值爲null
if (!onlyIfAbsent || oldValue == null)
//用新值替換舊值
e.value = value;
// 調用afterNodeAccess()方法進行鏈表排序
afterNodeAccess(e);
// 返回舊值
return oldValue;
}
}
// 結構性修改
++modCount;
// 最後判斷是否需要進行擴容。超過最大容量就擴容,實際大小大於閾值則擴容。
if (++size > threshold)
resize();
// 調用afterNodeInsertion()方法進行鏈表排序
afterNodeInsertion(evict);
return null;
}
//這是LinkedHashMap重寫後的newNode()方法
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放到雙向鏈表的最後
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;
}
}
//這是LinkedHashMap重寫後的afterNodeAccess()方法,當accessOrder爲true並且傳入的節點不是最後一個時,然後將該節點放到尾部
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;
}
}
//這是LinkedHashMap重寫後的afterNodeInsertion()方法,目的是移除鏈表中最老的節點對象,也就是當前在頭部的節點對象,但實際上在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;
}
由源碼可以得知,當accessOrder設置爲false時,會按照插入順序進行排序,當accessOrder爲true時,會按照訪問順序進行排序。具體的操作就是,把元素放到雙向鏈表的末尾。
(7) get()方法
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類其實也比較好理解,動態擴容就是HashMap的resize()方法,LinkedHashMap類只是會根據屬性accessOrder值來進行排序,當accessOrder爲默認值false的時候,每次插入元素的時候,就將插入的元素放到雙向鏈表的末尾;當accessOrder爲true時,每次調用put方法和get方法的時候,都會將元素放到鏈表的末尾。敬請期待《 我的jdk源碼(十七):Objects 》。
更多精彩內容,敬請掃描下方二維碼,關注我的微信公衆號【Java覺淺】,獲取第一時間更新哦!