面试必备:LinkedHashMap源码解析(JDK8)
1 概述
在上文中,我們已經聊過了HashMap,本篇是基于上文的基礎之上。所以如果沒看過上文,請先閱讀面試必備:HashMap源碼解析(JDK8)?
本文將從幾個常用方法下手,來閱讀LinkedHashMap的源碼。?
按照從構造方法->常用API(增、刪、改、查)的順序來閱讀源碼,并會講解閱讀方法中涉及的一些變量的意義。了解LinkedHashMap的特點、適用場景。
如果本文中有不正確的結論、說法,請大家提出和我討論,共同進步,謝謝。
2 概要
概括的說,LinkedHashMap 是一個關聯數組、哈希表,它是線程不安全的,允許key為null,value為null。?
它繼承自HashMap,實現了Map<K,V>接口。其內部還維護了一個雙向鏈表,在每次插入數據,或者訪問、修改數據時,會增加節點、或調整鏈表的節點順序。以決定迭代時輸出的順序。
默認情況,遍歷時的順序是按照插入節點的順序。這也是其與HashMap最大的區別。?
也可以在構造時傳入accessOrder參數,使得其遍歷順序按照訪問的順序輸出。
因繼承自HashMap,所以HashMap上文分析的特點,除了輸出無序,其他LinkedHashMap都有,比如擴容的策略,哈希桶長度一定是2的N次方等等。?
LinkedHashMap在實現時,就是重寫override了幾個方法。以滿足其輸出序列有序的需求。
示例代碼:
根據這段實例代碼,先從現象看一下LinkedHashMap的特征:?
在每次插入數據,或者訪問、修改數據時,會增加節點、或調整鏈表的節點順序。以決定迭代時輸出的順序。
? ? ? ? Map<String, String> map = new LinkedHashMap<>();
? ? ? ? map.put("1", "a");
? ? ? ? map.put("2", "b");
? ? ? ? map.put("3", "c");
? ? ? ? map.put("4", "d");
? ? ? ? Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
? ? ? ? while (iterator.hasNext()) {
? ? ? ? ? ? System.out.println(iterator.next());
? ? ? ? }
? ? ? ? System.out.println("以下是accessOrder=true的情況:");
? ? ? ? map = new LinkedHashMap<String, String>(10, 0.75f, true);
? ? ? ? map.put("1", "a");
? ? ? ? map.put("2", "b");
? ? ? ? map.put("3", "c");
? ? ? ? map.put("4", "d");
? ? ? ? map.get("2");//2移動到了內部的鏈表末尾
? ? ? ? map.get("4");//4調整至末尾
? ? ? ? map.put("3", "e");//3調整至末尾
? ? ? ? map.put(null, null);//插入兩個新的節點 null
? ? ? ? map.put("5", null);//5
? ? ? ? iterator = map.entrySet().iterator();
? ? ? ? while (iterator.hasNext()) {
? ? ? ? ? ? System.out.println(iterator.next());
? ? ? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
輸出:
1=a
2=b
3=c
4=d
以下是accessOrder=true的情況:
1=a
2=b
4=d
3=e
null=null
5=null
1
2
3
4
5
6
7
8
9
10
11
3 節點
LinkedHashMap的節點Entry<K,V>繼承自HashMap.Node<K,V>,在其基礎上擴展了一下。改成了一個雙向鏈表。
? ? 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);
? ? ? ? }
? ? }
1
2
3
4
5
6
同時類里有兩個成員變量head tail,分別指向內部雙向鏈表的表頭、表尾。
? ? //雙向鏈表的頭結點
? ? transient LinkedHashMap.Entry<K,V> head;
? ? //雙向鏈表的尾節點
? ? transient LinkedHashMap.Entry<K,V> tail;
1
2
3
4
5
4 構造函數
? ? //默認是false,則迭代時輸出的順序是插入節點的順序。若為true,則輸出的順序是按照訪問節點的順序。
? ? //為true時,可以在這基礎之上構建一個LruCach
? ? final boolean accessOrder;
? ? public LinkedHashMap() {
? ? ? ? super();
? ? ? ? accessOrder = false;
? ? }
? ? //指定初始化時的容量,
? ? public LinkedHashMap(int initialCapacity) {
? ? ? ? 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中的所有數據到 本集合中。
? ? ? ? putMapEntries(m, false);
? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
小結:?
構造函數和HashMap相比,就是增加了一個accessOrder參數。用于控制迭代時的節點順序。
5 增
LinkedHashMap并沒有重寫任何put方法。但是其重寫了構建新節點的newNode()方法.?
newNode()會在HashMap的putVal()方法里被調用,putVal()方法會在批量插入數據putMapEntries(Map<? extends K, ? extends V> m, boolean evict)或者插入單個數據public V put(K key, V value)時被調用。
LinkedHashMap重寫了newNode(),在每次構建新節點時,通過linkNodeLast(p);將新節點鏈接在內部雙向鏈表的尾部。
? ? //在構建新節點時,構建的是`LinkedHashMap.Entry` 不再是`Node`.
? ? 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;
? ? }
? ? //將新增的節點,連接在鏈表的尾部
? ? 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;
? ? ? ? }
? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
以及HashMap專門預留給LinkedHashMap的afterNodeAccess() afterNodeInsertion() afterNodeRemoval() 方法。
? ? // Callbacks to allow LinkedHashMap post-actions
? ? void afterNodeAccess(Node<K,V> p) { }
? ? void afterNodeInsertion(boolean evict) { }
? ? void afterNodeRemoval(Node<K,V> p) { }
1
2
3
4
? ? //回調函數,新節點插入之后回調 , 根據evict 和 ? 判斷是否需要刪除最老插入的節點。如果實現LruCache會用到這個方法。
? ? void afterNodeInsertion(boolean evict) { // possibly remove eldest
? ? ? ? LinkedHashMap.Entry<K,V> first;
? ? ? ? //LinkedHashMap 默認返回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
? ? protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
? ? ? ? return false;
? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
void afterNodeInsertion(boolean evict)以及boolean removeEldestEntry(Map.Entry<K,V> eldest)是構建LruCache需要的回調,在LinkedHashMap里可以忽略它們。
6 刪
LinkedHashMap也沒有重寫remove()方法,因為它的刪除邏輯和HashMap并無區別。?
但它重寫了afterNodeRemoval()這個回調方法。該方法會在Node<K,V> removeNode(int hash, Object key, Object value,?
boolean matchValue, boolean movable)方法中回調,removeNode()會在所有涉及到刪除節點的方法中被調用,上文分析過,是刪除節點操作的真正執行者。
? ? //在刪除節點e時,同步將e從雙向鏈表上刪除
? ? void afterNodeRemoval(Node<K,V> e) { // unlink
? ? ? ? LinkedHashMap.Entry<K,V> p =
? ? ? ? ? ? (LinkedHashMap.Entry<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;
? ? ? ? //同理如果后置節點時null ,則尾節點應是b
? ? ? ? if (a == null)
? ? ? ? ? ? tail = b;
? ? ? ? else//否則更新后置節點a的前置節點為b
? ? ? ? ? ? a.before = b;
? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
7 查
LinkedHashMap重寫了get()和getOrDefault()方法:
? ? 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;
? ? }
? ? public V getOrDefault(Object key, V defaultValue) {
? ? ? ?Node<K,V> e;
? ? ? ?if ((e = getNode(hash(key), key)) == null)
? ? ? ? ? ?return defaultValue;
? ? ? ?if (accessOrder)
? ? ? ? ? ?afterNodeAccess(e);
? ? ? ?return e.value;
? ?}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
對比HashMap中的實現,LinkedHashMap只是增加了在成員變量(構造函數時賦值)accessOrder為true的情況下,要去回調void afterNodeAccess(Node<K,V> e)函數。
? ? public V get(Object key) {
? ? ? ? Node<K,V> e;
? ? ? ? return (e = getNode(hash(key), key)) == null ? null : e.value;
? ? }
1
2
3
4
在afterNodeAccess()函數中,會將當前被訪問到的節點e,移動至內部的雙向鏈表的尾部。
? ? void afterNodeAccess(Node<K,V> e) { // move node to last
? ? ? ? LinkedHashMap.Entry<K,V> last;//原尾節點
? ? ? ? //如果accessOrder 是true ,且原尾節點不等于e
? ? ? ? if (accessOrder && (last = tail) != e) {
? ? ? ? ? ? //節點e強轉成雙向鏈表節點p
? ? ? ? ? ? LinkedHashMap.Entry<K,V> p =
? ? ? ? ? ? ? ? (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
? ? ? ? ? ? //p現在是尾節點, 后置節點一定是null
? ? ? ? ? ? p.after = null;
? ? ? ? ? ? //如果p的前置節點是null,則p以前是頭結點,所以更新現在的頭結點是p的后置節點a
? ? ? ? ? ? if (b == null)
? ? ? ? ? ? ? ? head = a;
? ? ? ? ? ? else//否則更新p的前直接點b的后置節點為 a
? ? ? ? ? ? ? ? b.after = a;
? ? ? ? ? ? //如果p的后置節點不是null,則更新后置節點a的前置節點為b
? ? ? ? ? ? if (a != null)
? ? ? ? ? ? ? ? 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;
? ? ? ? ? ? }
? ? ? ? ? ? //尾節點的引用賦值成p
? ? ? ? ? ? tail = p;
? ? ? ? ? ? //修改modCount。
? ? ? ? ? ? ++modCount;
? ? ? ? }
? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
值得注意的是,afterNodeAccess()函數中,會修改modCount,因此當你正在accessOrder=true的模式下,迭代LinkedHashMap時,如果同時查詢訪問數據,也會導致fail-fast,因為迭代的順序已經改變。
7.2 containsValue
它重寫了該方法,相比HashMap的實現,更為高效。
? ? public boolean containsValue(Object value) {
? ? ? ? //遍歷一遍鏈表,去比較有沒有value相等的節點,并返回
? ? ? ? for (LinkedHashMap.Entry<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;
? ? }
1
2
3
4
5
6
7
8
9
對比HashMap,是用兩個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;
? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
8 遍歷
重寫了entrySet()如下:
? ? public Set<Map.Entry<K,V>> entrySet() {
? ? ? ? Set<Map.Entry<K,V>> es;
? ? ? ? //返回LinkedEntrySet
? ? ? ? return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
? ? }
? ? final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
? ? ? ? public final Iterator<Map.Entry<K,V>> iterator() {
? ? ? ? ? ? return new LinkedEntryIterator();
? ? ? ? }
? ? }
1
2
3
4
5
6
7
8
9
10
最終的EntryIterator:
? ? final class LinkedEntryIterator extends LinkedHashIterator
? ? ? ? implements Iterator<Map.Entry<K,V>> {
? ? ? ? public final Map.Entry<K,V> next() { return nextNode(); }
? ? }
? ? abstract class LinkedHashIterator {
? ? ? ? //下一個節點
? ? ? ? LinkedHashMap.Entry<K,V> next;
? ? ? ? //當前節點
? ? ? ? LinkedHashMap.Entry<K,V> current;
? ? ? ? int expectedModCount;
? ? ? ? LinkedHashIterator() {
? ? ? ? ? ? //初始化時,next 為 LinkedHashMap內部維護的雙向鏈表的扁頭
? ? ? ? ? ? next = head;
? ? ? ? ? ? //記錄當前modCount,以滿足fail-fast
? ? ? ? ? ? expectedModCount = modCount;
? ? ? ? ? ? //當前節點為null
? ? ? ? ? ? current = null;
? ? ? ? }
? ? ? ? //判斷是否還有next
? ? ? ? public final boolean hasNext() {
? ? ? ? ? ? //就是判斷next是否為null,默認next是head ?表頭
? ? ? ? ? ? return next != null;
? ? ? ? }
? ? ? ? //nextNode() 就是迭代器里的next()方法 。
? ? ? ? //該方法的實現可以看出,迭代LinkedHashMap,就是從內部維護的雙鏈表的表頭開始循環輸出。
? ? ? ? final LinkedHashMap.Entry<K,V> nextNode() {
? ? ? ? ? ? //記錄要返回的e。
? ? ? ? ? ? LinkedHashMap.Entry<K,V> e = next;
? ? ? ? ? ? //判斷fail-fast
? ? ? ? ? ? if (modCount != expectedModCount)
? ? ? ? ? ? ? ? throw new ConcurrentModificationException();
? ? ? ? ? ? //如果要返回的節點是null,異常
? ? ? ? ? ? if (e == null)
? ? ? ? ? ? ? ? throw new NoSuchElementException();
? ? ? ? ? ? //更新當前節點為e
? ? ? ? ? ? current = e;
? ? ? ? ? ? //更新下一個節點是e的后置節點
? ? ? ? ? ? next = e.after;
? ? ? ? ? ? //返回e
? ? ? ? ? ? return e;
? ? ? ? }
? ? ? ? //刪除方法 最終還是調用了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;
? ? ? ? ? ? removeNode(hash(key), key, null, false, false);
? ? ? ? ? ? expectedModCount = modCount;
? ? ? ? }
? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
值得注意的就是:nextNode() 就是迭代器里的next()方法 。?
該方法的實現可以看出,迭代LinkedHashMap,就是從內部維護的雙鏈表的表頭開始循環輸出。?
而雙鏈表節點的順序在LinkedHashMap的增、刪、改、查時都會更新。以滿足按照插入順序輸出,還是訪問順序輸出。
總結
LinkedHashMap相對于HashMap的源碼比,是很簡單的。因為大樹底下好乘涼。它繼承了HashMap,僅重寫了幾個方法,以改變它迭代遍歷時的順序。這也是其與HashMap相比最大的不同。?
在每次插入數據,或者訪問、修改數據時,會增加節點、或調整鏈表的節點順序。以決定迭代時輸出的順序。
accessOrder ,默認是false,則迭代時輸出的順序是插入節點的順序。若為true,則輸出的順序是按照訪問節點的順序。為true時,可以在這基礎之上構建一個LruCache.
LinkedHashMap并沒有重寫任何put方法。但是其重寫了構建新節點的newNode()方法.在每次構建新節點時,將新節點鏈接在內部雙向鏈表的尾部
accessOrder=true的模式下,在afterNodeAccess()函數中,會將當前被訪問到的節點e,移動至內部的雙向鏈表的尾部。值得注意的是,afterNodeAccess()函數中,會修改modCount,因此當你正在accessOrder=true的模式下,迭代LinkedHashMap時,如果同時查詢訪問數據,也會導致fail-fast,因為迭代的順序已經改變。
nextNode() 就是迭代器里的next()方法 。?
該方法的實現可以看出,迭代LinkedHashMap,就是從內部維護的雙鏈表的表頭開始循環輸出。?
而雙鏈表節點的順序在LinkedHashMap的增、刪、改、查時都會更新。以滿足按照插入順序輸出,還是訪問順序輸出。
它與HashMap比,還有一個小小的優化,重寫了containsValue()方法,直接遍歷內部鏈表去比對value值是否相等。
*
那么,還有最后一個小問題?為什么它不重寫containsKey()方法,也去循環比對內部鏈表的key是否相等呢?
---------------------?
作者:張旭童?
來源:CSDN?
原文:https://blog.csdn.net/zxt0601/article/details/77429150?
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
總結
以上是生活随笔為你收集整理的面试必备:LinkedHashMap源码解析(JDK8)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CouncurrentHashMap源码
- 下一篇: 面试必备:ArrayList源码解析(J