寫在前面
今天給大家帶來的是LinkedHashMap源碼分析,說起這個心中滿滿的痛。記得當初畢業的時候去面試,面試官問HashMap、LinkedHashMap、TreeMap哪些是有序的,我回答了HashMap是有序的。然後就讓我回去等消息了,今天我就來談談自己對LinkedHashMap的理解,也希望能對大家有所幫助。
一、繼承關係及主要字段
可以看到LinkedHashMap繼承了HashMap,我前一篇HashMap的文章有提到有個參數是給LinkedHashMap使用的,這一點我後面會解釋。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
主要參數:
/**
* 這是linkedHashMap內部的數據結構Entry,它繼承了HashMap的Node數據機構
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
//可以看到他多了兩個屬性:一個是before後退指針,一個是after後退指針,所以它其實是一個雙
//鏈表
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
/**
* 雙端鏈表的頭節點.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 雙端鏈表的尾節點
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 是否按照訪問順序
*
* @serial
*/
final boolean accessOrder;
二、構造方法
/沒什麼特別的,指定初始容量和負載因子,調用父類hashmap的構造方法
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//指定初始容量,調用父類的構造方法
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//默認構造,調用父類默認構造
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
//前幾種構造方法accessOrder都初始化爲false,這個構造方法可以自己指定,如果爲true,排序將不
//再按照插入順序,而是訪問順序
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
三、put元素的過程以及排序原理
我們看代碼會發現,LinkedHashMap並沒有put方法,其實它用的是父類HashMap的put方法,關於父類put方法的詳細介紹可以看我上一篇文章,這邊不再做過多解釋,那問題來了,父類插入節點是HashMap的Node類型,LinkedHashMap怎麼把它變成自己的Entry類型呢?來看代碼:
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)
//看這裏:爲什麼HashMap不寫成tab[i] = new Node()而是這樣寫?
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) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//節點訪問後的鉤子操作,模板方法 linkedHashMap實現
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
//節點插入後的鉤子操作,也是模板方法 linkedHashMap實現
afterNodeInsertion(evict);
return null;
}
看一下newNode()方法:
// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
//HashMap的實現是創建自己的Node節點
return new Node<>(hash, key, value, next);
}
但是我們發現LinkedHashMap重寫了這個方法:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
//把新建的節點創建成自身的Entry類型的節點
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
//注意這個方法,這是排序的實現方法
linkNodeLast(p);
return p;
}
現在我們知道HashMap那麼設計的原因:爲了兼容子類,這種模板方法的設計模式我們也可以借鑑
好,接下來看一下這個linkNodeLast方法:
// link at the end of list
//看頭上這行註釋我們就知道採用的是尾插法
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
//將tail節點賦值給last節點,注意,第一次插入的時候顯然tail等於null
LinkedHashMap.Entry<K,V> last = tail;
//把新插入的節點p賦值給tail節點
tail = p;
//第一次插入時會走這個邏輯
if (last == null)
//把新插入的節點p賦值給head節點,如果是第一次插入,那麼此時head=tail=p
head = p;
else {
//讓p的後退指針指向last節點,也就是一開始的tail尾節點
p.before = last;
//讓一開始的tail尾節點的前進指針指向p節點,至此就完成了新建的節點掛到了鏈表後面
last.after = p;
}
}
接着看一下上面提到的另外兩個模版方法:
先看第一個:
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//如果accessOrder爲true並且當前被訪問的節點不是尾節點
//因此如果構造函數沒指定accssOrder爲true的話都不會走這個邏輯
if (accessOrder && (last = tail) != e) {
//把e強轉並賦值給p,然後把p的前一個節點賦值給b,p的下一個節點賦值給a
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//讓p的下一個節點置空
p.after = null;
//如果p的前一個節點等於空的話,證明他是頭節點,要將p移到尾節點的話,只要將p的後一個節
//點設置爲頭節點,這樣p就在鏈表之外了
if (b == null)
head = a;
else
//如果p的前一個節點不爲空,那簡單,只要將p前一個節點的前進指針指向p的下一個節點a
//這樣就跳過了p
b.after = a;
//如果p的後一個節點不爲空,就將p的後一個節點的後退指針指向p的前一個節點,因爲是個雙向
//鏈表,所以要這麼做
if (a != null)
a.before = b;
//這裏走的是p的後一個節點爲空的情況,將p的前一個節點賦值給last
else
last = b;
//這裏只有一種情況:p後置節點爲空且p的前置節點爲空也就是說只有一個節點
if (last == null)
//把p設置爲head節點
head = p;
else {
//這裏就是將p放到鏈表的最後,和last節點起個關聯關係
p.before = last;
last.after = p;
}
//尾節點設置成當前p節點
tail = p;
++modCount;
}
}
我們看到如果插入元素過程沒有覆蓋已有元素,或者accessOrder沒有設置爲true,就不會觸發這個排序。
來看下一個方法:
//這個方法對應元素插入之後但是沒有覆蓋原有值的操作
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//這個方法永遠不可能執行,因爲removeEldestEntry這個方法永遠返回false,
//它會留給子類去實現邏輯,推測可以作爲最老的節點刪除 做一個LRU Cache,可以參考mabatis的
//LRUCache實現
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
四、get方法相關
這個方法比較簡單 ,不做過多解釋了
public V get(Object key) {
Node<K,V> e;
//調用的是hashmap的getNode方法,不再做解釋
if ((e = getNode(hash(key), key)) == null)
return null;
//多了一個訪問控制的判斷,如果爲true則每次把被訪問的元素放到鏈表尾
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
五、如何迭代
首先我們知道HashMap的迭代是 HashMap.entrySet.iterator方法來獲取迭代器,我們看源碼不難發現,LinkedhashMap重寫了entrySet方法:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
//new了一個LinkedentrySet對象
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
來看一下LinkedEntrySet的代碼:
//我們先看這一小段就夠了,這個set是LinkedHashMap的內部類,他實現了iterator方法
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
//返回的是LinkedEntryIterator這個迭代器,這也是定義在內部的
return new LinkedEntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
來看一下上面這個迭代器:
//來看一下這個迭代器,他的子類主要是
abstract class LinkedHashIterator {
//下一個節點
LinkedHashMap.Entry<K,V> next;
//當前節點
LinkedHashMap.Entry<K,V> current;
//期待的修改次數,這個和上一篇ArrayList的迭代器同理,不做過多介紹
int expectedModCount;
LinkedHashIterator() {
//首先將 head表頭節點作爲下一個節點
next = head;
//給定期待的修改次數
expectedModCount = modCount;
//當前節點爲null
current = null;
}
public final boolean hasNext() {
//如果下一個節點不爲空
return next != null;
}
//這個方法是next方法的實現,來看一下
final LinkedHashMap.Entry<K,V> nextNode() {
//把next節點保存到e,第一次進來時next節點是表頭節點
LinkedHashMap.Entry<K,V> e = next;
//併發修改校驗
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果下一個元素是null,拋出異常
if (e == null)
throw new NoSuchElementException();
//把e節點設置爲當前節點
current = e;
//next節點通過前進指針指向e的下一個節點
next = e.after;
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
//他繼承了上面這個迭代器並且實現了標準迭代器接口
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
//來看一下next()方法的實現,他其實調用了父類的nextNode方法
public final Map.Entry<K,V> next() { return nextNode(); }
}
以上就是迭代的過程
寫在最後
今天暫時先寫到這裏 ,因爲我要睡覺了,有什麼問題可以給我留言,不過相信看到這裏,你對linkedHashMap的排序實現原理也有了一些自己的理解。如果覺得對自己有幫助的,可以給我點個贊或者關注我,以後會盡量日更。原創不易,轉載請註明出處。