JDK1.8LinkedHashMap實現原理及源碼分析
首先附上我的幾篇其它文章鏈接感興趣的可以看看,如果文章有異議的地方歡迎指出,共同進步,順便點贊謝謝!!!
Android framework 源碼分析之Activity啓動流程(android 8.0)
Android studio編寫第一個NDK工程的過程詳解(附Demo下載地址)
面試必備1:HashMap(JDK1.8)原理以及源碼分析
Android事件分發機制原理及源碼分析
View事件的滑動衝突以及解決方案
Handler機制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源碼
Android三級緩存原理及用LruCache、DiskLruCache實現一個三級緩存的ImageLoader
概述
本文對LinkedHashMap的源碼分析是基於JDK1.8,因爲LinkedHashMap是在HashMap的基礎上進行的功能擴展,所以需要掌握HashMap的源碼和實現原理,如果不瞭解請先閱讀我的另一篇HashMap的實現原理和源碼分析
重點:本文如果有分析的不對的地方請大家留言指正!!!!!
再次強調一下:讀此文章之前需要先去了解HashMap的源碼請先閱讀我的另一篇HashMap的實現原理和源碼分析,以便理解。
LinkedHashMap的數據結構
想要知道 LinkedHashMap的實現原理,就必須先去了解它的數據結構(即存儲機制),和HashMap一樣LinkedHashMap的數據結構也是通過數組和鏈表組成的散列表,同樣線程也是不安全的允許null值null鍵
,不同的是LinkedHashMap在散列表的基礎上內部維持的是一個雙向鏈表在每次增、刪、改、 查時增加或刪除或調整鏈表的節點順序。
- 在默認情況下LinkedHashMap遍歷時的順序是按照插入節點順序,我們可一再構造器中通過傳入
accessOrder=true
參數,使得其遍歷順序按照訪問的順序輸出。 - 因繼承自HashMap的一些特性LinkedHashMap都有,比如擴容的策略,哈希桶長度一定是2的N次方等等。
- 本質上LinkedHashMap是通過複寫父類HashMap的幾個抽象方法,去實現有序輸出
LinkedHashMap的鏈表節點LinkedHashMapEntry<K,V>:
LinkedHashMap與HashMap都是有數組和鏈表組成的散列表,不同的是LinkedHashMap的LinkedHashMapEntry<K,V>
繼承HashMap的Node<K,V>
,並在其基礎上進行擴展成一個雙向鏈表其源碼如下:
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
LinkedHashMapEntry<K,V> before, after;//分別指向當前節點的前後節點
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);//其他的就是父類HashMap的Node
}
}
此外LinkedHashMap還增加了兩個成員變量分別指向鏈表的頭節點和尾節點
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMapEntry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMapEntry<K,V> tail;
增、改put(key,value)方法源碼
LinkedHashMap並沒有重寫HashMap的put、putVal()方法,只是重寫了putVal()中調用的以下三個方法
- 構建新節點時調用的
newNode()
方法,去構建LinkedHashMapEntry節點,而不是Node節點 - 節點被訪問後
afterNodeAccess(e)
抽象方法 - 節點被插入後
afterNodeInsertion(evict)
抽象方法
public V put(K key, V value) {
//計算hash值,調用putVal方法,與父類相同,再此不做分析
return putVal(hash(key), key, value, false, true);
}
在這裏我將從這三個複寫的方法源碼進行分析,至於put()、putVal()方法的詳細分析請閱讀上文HashMap的實現原理和源碼分析中put方法源碼
1:重寫了newNode()方法源碼
不同的是重寫了在putVal()方法中調用的newNode方法,並且在創建新節點時將該節點鏈接在鏈表尾部 linkNodeLast(p)
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
//創建的是LinkedHashMapEntry節點
LinkedHashMapEntry<K,V> p =
new LinkedHashMapEntry<K,V>(hash, key, value, e);
//並且在創建新節點時將該節點鏈接在鏈表尾部
linkNodeLast(p);
return p;
}
// link at the end of list 將新增的節點,放在在鏈表的尾部
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
LinkedHashMapEntry<K,V> last = tail;
tail = p;
//集合之前是空的
if (last == null)
head = p;
else {
//將新節點連接在鏈表的尾部
p.before = last;
last.after = p;
}
}
2:複寫了afterNodeAccess(Node e )
當節點被訪問後,即put的鍵已經存在時,調用afterNodeAccess(Node e)
方法進行排序
/**
* 當put(key,value)中key存在的時候,即訪問某個存在的節點時
* 如果assessOrder=true,將該節點移動到最後
* @param e
*/
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;//記錄尾部節點的臨時變量
if (accessOrder && (last = tail) != e) {//如果assessOrder&&該節點不在鏈表尾部則將其移動到尾部
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;//p記錄當前訪問的節點 b和a分別記錄當前節點的前、後節點
p.after = null;//將當前節點的after置null,因爲鏈表尾部節點沒有after節點
//以下是斷鏈和重新連接鏈表的過程
if (b == null)
//b==null表示,p的前置節點是null,即p以前是頭結點,所以更新現在的頭結點是p的後置節點a
head = a;
else
//否則直接將當前節點的前後節點相連,移除當前節點
b.after = a;
if (a != null)
//a != null表示p的後置節點不是null,則更新後置節點a的前置節點爲b
a.before = b;
else//如果原本p的後置節點是null,則p就是尾節點。 此時 更新last的引用爲 p的前置節點b
last = b;
if (last == null)//原本尾節點是null 則,鏈表中就一個節點
//記錄新的鏈表頭
head = p;
else {
//否則 更新當前節點p的前置節點爲 原尾節點last, last的後置節點是p
p.before = last;
last.after = p;
}
//修改成員變量 爲節點爲當前節點
tail = p;
//修改modCount
++modCount;
}
}
3:複寫了afterNodeInsertion(Node e)
/**
* 當插入新節點後,根據evict和判斷是否需要刪除最老插入的節點
* @param evict 爲false時表示初始化時調用
*/
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMapEntry<K,V> first;//記錄鏈表頭節點
//是否初始化&&鏈表不爲null&&是否移除最老的節點,莫認返回false 則不刪除節點
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
/**
*LinkedHashMap 默認返回false 則不刪除節點。 返回true 代表要刪除最早的節點。
* 通常構建一個LruCache會在達到Cache的上限是返回true
* @param eldest 移除最老的節點
* @return
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
注意: 這兩個方法void afterNodeInsertion(boolean evict)
和boolean removeEldestEntry(Map.Entry<K,V> eldest)
是構建LruCache需要的回調,在LinkedHashMap裏可以忽略它們。
查:get(key)和getOrDefault( key, defaultValue)方法源碼
和父類HashMap相比複寫了get(key)和getOrDefault( key, defaultValue)
兩個方法,其查找過程和父類的相同,只是在查找完成後根據構造器中的accessOrder=true
時調用了afterNodeAccess(e)
方法會將當前被訪問到的節點e,移動至內部的雙向鏈表的尾部
public V get(Object key) {
Node<K,V> e;
//獲取Node節點過程和父類HashMap的相同,在這裏不做分析
if ((e = getNode(hash(key), key)) == null)
return null;
//accessOrder,將當前訪問節點移動到雙向鏈表的尾部
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
/**
* {@inheritDoc}
*/
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
//獲取Node節點過程和父類HashMap的相同,在這裏不做分析
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
// //accessOrder,將當前訪問節點移動到雙向鏈表的尾部
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
刪:remove()方法源碼
LinkedHashMap的remove()和HashMap的邏輯相同,故而沒有重寫removeNode()
方法,在這裏不做過多分析,詳細過程請閱讀HashMap的實現原理和源碼分析中的remove()
方法的源碼,只是重寫了removeNode()
方法中刪除節點後調用的afterNodeRemoval()
這個回調方法,其源碼如下:
/**
* 在刪除節點e時,同步將e從雙向鏈表上刪除
* @param e 被刪除的節點
*/
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
//待刪除節點 p 的前置後置節點都置空
p.before = p.after = null;
//如果前置節點是null,則現在的頭結點應該是後置節點a
if (b == null)
head = a;
else
//否則將前置節點b的後置節點指向a
b.after = a;
if (a == null)//同理如果後置節點時null ,則尾節點應是b
tail = b;
else
//否則更新後置節點a的前置節點爲b
a.before = b;
}
擴展:複寫了containsValue(Object value)
相比HashMap的實現,更爲高效。
public boolean containsValue(Object value) {
//一次for循環遍歷鏈表,查找Value相同的
for (LinkedHashMapEntry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}
而HashMap的containsValue
方法,是由兩個for循環組成,查詢效率相對較低源碼如下:
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {//遍歷數組
for (Node<K,V> e = tab[i]; e != null; e = e.next) {//遍歷鏈表
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
遍歷:entrySet()方法源碼
這裏需要和上文HashMap的entrySet()
方法源碼對比分析,更容易理解
LinkedHashMap的entrySet()
方法源碼:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
//直接返回 LinkedEntrySet() 集合
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
遍歷主要用的是LinkedEntrySet的Iterator<Map.Entry<K,V>> iterator()
方法,LinkedEntrySet是LinkedHashMap 的內部類,繼承成AbstractSet<Map.Entry<K,V>>
集合,其詳細源碼比較簡單不在這裏進行詳細分析
LinkedHashMap中內部類LinkedEntrySet
的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();
}
....省略部分源碼...
}
通過上文HashMap的extrySet方法源碼和以上源碼分析可以知道,當我們HashMap或LinkedHashMap調用entrySet()的Itrrator()方法返回的Iterator不同,分別是new EntryIterator();
和new LinkedEntryIterator()
,通過返回的Iterator對象進行集合的遍歷過程,接下來我將對其其源碼入手分析集合的遍歷過程
LinkedHashMap的迭代器LinkedEntryIterator
源碼
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
//迭代器的next方法就是返回通過調用`nextNode()`返回下一個節點
public final Map.Entry<K,V> next() { return nextNode(); }
}
nextNode()
方法是父類LinkedHashIterator
中的方法,LinkedHashMap的本質LinkedHashIterator
是實現集合遍歷,其源碼分析如下
abstract class LinkedHashIterator {
LinkedHashMapEntry<K,V> next;//下一個節點
LinkedHashMapEntry<K,V> current;//當前操作的節點
int expectedModCount;
/**
* 構造器做初始化動作
* 需要注意的是:expectedModCount的作用:
* 和HashMap一樣,LinkedHashMap不是線程安全的,所以在迭代的時候,會將modCount賦值到迭代器的expectedModCount屬性中,
* 然後進行迭代,如果在迭代的過程中HashMap被其他線程修改了,modCount的數值就會發生變化,
* 這個時候expectedModCount和ModCount不相等,迭代器就會拋出 ConcurrentModificationException()異常
*/
LinkedHashIterator() {
//初始化的時候next爲雙向鏈表的表頭
next = head;
//遍歷前,先記錄modCount值
expectedModCount = modCount;
//當前節點初始化爲null
current = null;
}
public final boolean hasNext() {
//判斷是否有下一個節點,即判斷next是否爲null
return next != null;
}
/**
* nextNode()的方法,就是Iterator中next方法中調用的,
* 其遍歷LinkedHashMap過程就是,就是從內部維護的雙向鏈表的表頭開始循環輸出。
*/
final LinkedHashMapEntry<K,V> nextNode() {
//e用於記錄返回的節點
LinkedHashMapEntry<K,V> e = next;
//線程安全判斷處理
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//current =next
current = e;
//next指向next的下一個節點,遍歷鏈表
next = e.after;
return e;
}
/**
* Iterator刪除方法 本質上還是調用了HashMap的removeNode方法
* 只是在調用之前,通過modCount != expectedModCount時拋出併發修改異常,處理線程不安全問題,
* 如果相等則調用HashMap的removeNode方法移除節點
*
*/
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;
//調用HashMap的,removeNode方法
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
HashMap的遍歷本質上是通過Iterator的next()
方法實現,而next()
就是調用nextNode()
方法,從nextNode
的源碼可以看出:迭代LinkedHashMap
,就是從內部維護的雙鏈表的表頭開始循環輸出。而雙鏈表節點的順序在LinkedHashMap的增、刪、改、查時都會更新。以滿足按照插入順序輸出,還是訪問順序輸出。
總結
本文是在上文HashMap的實現原理和源碼分析基礎上做的分析
- LinkedHashMap的數據結構就是在HashMap的散列表的基礎上維持了一個雙向鏈表,在每次增、刪、改、 查時增加或刪除或調整鏈表的節點順序。
- LinkedHashMap就是複寫了HashMap提供的幾個抽象方法,在每次插入數據,或者訪問、修改數據時,會增加節點、或調整鏈表的節點順序以改變它迭代遍歷時的順序。