LinkedHashMap 源码阅读
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
先来看一下 LinkedHashMap 的继承关系,它继承了 HashMap,并且实现了 Map 接口。
LinkedHashMap 底层是 数组 + 链表 的形式,它通过双向的链表将插入进去的每个节点链接起来,以达到顺序返回的目的。
本篇内容主要关注 LinkedHashMap 是如何实现和维护这种链表结构的,以及 LinkedHashMap 的遍历是如何实现的。
上图展示的是 LinkedHashMap 中维护的大致结构,每个节点之间通过链相连,构成一个连续的双向链表,其中的节点是 LinkedHashMap 的静态内部类:
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
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);
}
}
这个类继承了 HashMap 中的 Node 类,在其基础上加了两个指针 before 和 after,分别指向它的后一个节点和前一个节点。
接下来具体来看一下,LinkedHashMap 中是如何维护链表结构的:
public static void main(String[] args) {
LinkedHashMap<Object, Object> hashMap = new LinkedHashMap<>();
hashMap.put(1, 1);
System.out.println(hashMap);
}
上面展示的是一个简单的主方法,通过调试这段代码来阅读其 put()
方法的源码。
V put(K key, V value)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
调用的仍然是 HashMap 的 put 方法,这个方法一定不陌生了,具体请看这篇文章:
万字源码解析!彻底搞懂 HashMap【二】:putVal 方法和 resize 方法(重点)
如果仅仅是调用了这个方法肯定是无法形成上面的双向链表的,那唯一的可能就是 LinkedHashMap 中重写了某些被 putVal()
调用的 HashMap 中的方法,通过调试可以很清楚的知道,重写的是 newNode()
方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e)
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;
}
方法中实例化了一个 LinkedHashMap.Entry 对象,然后调用了 linkNodeLast()
方法,就是通过这个方法将链表节点连接起来的
void linkNodeLast(LinkedHashMap.Entry<K,V> p)
// link at the end of list
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;
}
}
首先获取了此时的 last 节点,也就是链表的最后一个元素,然后做了一个判断,如果 last 是空的,就说明此时数组中没有元素,是第一次添加,此时就将 head 也置为 p 节点,否则就将 p 与 last 节点双向连接。
读到这里其实就可以大致了解到 LinkedHashMap 的机制,就是将 HashMap 中的 Node 节点替换为了类中的 Entry,新增了 before 和 after 节点来创造出一个依据顺序连接的双向链表,由此来达到存储插入顺序的目的。
再来看一下删除的方法
V remove(Object key)
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
同样,也是调用了父类 HashMap 中的 removeNode()
方法,然后重写其中调用的某个方法来达到维护链表的顺序。
HashMap 中的这个方法的实现也比较简单,这里就不赘述了,就是通过哈希映射到数组的某个索引,然后寻找需要删除的节点,下面展示的是其找到节点后执行删除逻辑的代码:
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) // 是第一个节点
tab[index] = node.next;
else // 是中间的某个节点
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
在 LinkedHashMap 中重写了 afterNodeRemoval()
方法去维护链表的顺序性,传入的 node 在本次 remove()
中被删除的节点,注意它实际上是 LinkedHashMap.Entry 实例,它还有指向链表前后元素的指针,所以并不会被清除。
void afterNodeRemoval(Node<K,V> 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.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
首先使用 p 将传入的节点保存下来(向下转型变为 LinkedHashMap.Entry<K,V>),使用 a 和 b 保存链表中节点的前后节点,然后就是对链表的维护,方式也比较简单,就是将前一个节点和后一个节点连接。
然后来看一下 LinkedHashMap 是如何遍历的,Map 集合的遍历是依赖于其中的 entrySet,可以通过 entrySet()
来获取这个 Set 集合,方法返回的是一个 Set<Map.Entry<K, V>>
类型的集合,其中 K
是键的类型,V
是值的类型。
通过 Set 集合可以利用迭代器或者增强 for 循环实现遍历,其中使用的迭代器为 LinkedEntryIterator
LinkedEntryIterator
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
这个类在 LinkedHashIterator
的基础上实现了 Iterator 接口,LinkedHashIterator
是一个公用的继承类,很多迭代器都通过继承它来实现一些方法
能保证返回顺序的关键是其中的 nextNode()
方法:
LinkedHashMap.Entry<K,V> nextNode()
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;
}
可以看到此时的 next 指向的是 e.after
即使用的是链表中维护的顺序,这样就能保证返回的顺序性。