本文主要是介绍面试必备:LinkedHashMap源码解析(JDK8),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
概括的说,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");map.get("4");map.put("3", "e");map.put(null, null);map.put("5", null);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
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);}}
同时类里有两个成员变量head tail
,分别指向内部双向链表的表头、表尾。
transient LinkedHashMap.Entry<K,V> head;transient LinkedHashMap.Entry<K,V> tail;
4 构造函数
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;}public LinkedHashMap(Map<? extends K, ? extends V> m) {super();accessOrder = false;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);
将新节点链接在内部双向链表的尾部。
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()
方法。
void afterNodeAccess(Node<K,V> p) { }//访问元素之后调用,accessOrder为true的情况下,将访问的元素移到链表末尾,更新双向链表
void afterNodeInsertion(boolean evict) { }//新增元素之后调用,根据evict判断是否需要删除最老插入的节点void afterNodeRemoval(Node<K,V> p) { }//移除元素之后调用,更新双向链表
void afterNodeInsertion(boolean evict) { 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;}
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()
会在所有涉及到删除节点的方法中被调用,上文分析过,是删除节点操作的真正执行者。
void afterNodeRemoval(Node<K,V> e) { LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;p.before = p.after = null;if (b == null)head = a;elseb.after = a;if (a == null)tail = b;elsea.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;}
对比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;}
在afterNodeAccess()
函数中,会将当前被访问到的节点e,移动至内部的双向链表的尾部。
void afterNodeAccess(Node<K,V> e) { LinkedHashMap.Entry<K,V> last;if (accessOrder && (last = tail) != e) {LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;p.after = null;if (b == null)head = a;elseb.after = a;if (a != null)a.before = b;elselast = b;if (last == null) head = p;else {p.before = last;last.after = p;}tail = p;++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) {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;}
对比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;}
8 遍历
重写了entrySet()
如下:
public Set<Map.Entry<K,V>> entrySet() {Set<Map.Entry<K,V>> es;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();}}
最终的EntryIterator:
final class LinkedEntryIterator extends LinkedHashIteratorimplements 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 = head;expectedModCount = modCount;current = null;}public final boolean hasNext() {return next != null;}final LinkedHashMap.Entry<K,V> nextNode() {LinkedHashMap.Entry<K,V> e = next;if (modCount != expectedModCount)throw new ConcurrentModificationException();if (e == null)throw new NoSuchElementException();current = 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;}}
- 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值是否相等。
转载自:面试必备:LinkedHashMap源码解析(JDK8)
这篇关于面试必备:LinkedHashMap源码解析(JDK8)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!