一、介绍
首先声明一点,在学习LinkedHashMap源码时,HashMap的源码是必需的前置知识点,对HashMap的源码不了解的同学请移步HashMap源码。也希望能对模版方法的设计模式有一定的了解
在往期文章中,我们从源码分析了java集合框架中Map一族的HashMap和TreeMap,他们都有各自的特点,如HashMap是无序的,TreeMap是利用底层红黑树的特点将键值对中的key进行排序并按照排序结果进行遍历的。今天我们介绍Map一族的另一个实现LinkedHashMap。
LinkedHashMap是的底层是双向链表+哈希表,从命名来看,它是双向链表与HashMap的结合体,就是在HashMap的基础上,将每个节点按照一定的顺序(插入顺序或访问顺序)通过双向链表将这些节点串起来。
访问顺序是指调用
put()
、putIfAbsent()
、get()
、getOrDefault()
、compute()
、computeIfAbsent()
、computeIfPresent()
或merge()
方法会导致对相应键值对的访问(假设调用完成后该条目存在)
基于以上介绍,我们可以想象出来HashMap和LinkedHashMap的对比图如下所示
从上图同学们是否觉得LinkedHashMap的结构要比HashMap复杂很多 ?通常我们从两个角度来看LinkedHashMap:
HashMap的角度
从该角度看LinkedHashMap得到的结果如下图
双向链表的角度
从该角度看LinkedHashMap得到的结果如下图
二、使用场景
在jdk中,LinkedHashMap是实现LRU(最近最少使用)算法的推荐底层实现。
文档:A special constructor is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches.
翻译:提供了一个特殊的构造函数来创建一个链接哈希映射,其迭代顺序是其条目最后一次访问的顺序,从最近最少访问到最近访问(访问顺序)。这种map非常适合建造LRU的缓存
三、类的声明
我们来看一下LinkedHashMap的声明,可以大致了解他的功能。
public class LinkedHashMap<K,V> extends HashMap<K,V>
implements Map<K,V>
从类的声明来看
- 继承了HashMap,因此LinkedHashMap是HashMap的扩展。这正是学习LinkedHashMap源码前必须学习HashMap源码的原因。
- 实现了Map接口,说明LinkedHashMap是一个以
<key,value>键值对
存储数据的结构 - 实现了Cloneable接口(由父类HashMap实现),提供了对象克隆方法,但请注意,是浅克隆。
- 实现了Serializable接口(由父类HashMap实现),支持序列化。
下面是LinkedHashMap类的继承关系图
四、内部类Entry
在LinkedHashMap中,将键值对封装成节点的类为其内部类Entry
,该内部类继承于HashMap的内部类Node
,如下图所示
基于HashMap底层为哈希表,以及LinkedHashMap是HashMap的扩展这两个事实,从上图我们可以得知,LinkedHashMap
的内部类Entry
是HashMap
的内部类Node
的扩展,所以
Node
内部类和TreeNode
内部类分别作为HashMap
的普通节点和红黑树节点。Entry
内部类和TreeNode
内部类分别作为LinkedHashMap
的普通节点和红黑树节点。
下面我们看一下,LinkedHashMap
的内部类Entry
对 HashMap
的内部类Node
做了哪些扩展?
// HashMap的内部类Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// ...
}
// LinkedHashMap的内部类Entry
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);
}
}
从上面源码中可以看到,在Node的基础上,Entry添加了两个属性:before
和 after
,分别表示当前节点的前驱节点和后继节点,也正因此,LinkedHashMap将HashMap的哈希表结构中的每一个节点关联起来形成一个双向链表。
五、成员变量
LinkedHashMap只负责维护双向链表,因此只提供了和双向链表相关的属性:头节点和尾节点,而对哈希表的维护由其父类 HashMap维护。
// 表示双向链表的头节点
transient LinkedHashMap.Entry<K,V> head;
// 表示双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
// true-表示该双向链表是按照访问顺序形成的
// false-表示该双向链表是按照插入顺序形成的
// 该字段对双向链表的遍历影响极大。
final boolean accessOrder;
六、构造方法
无参构造
可以看到,先调用父类HashMap的无参构造,然后定义默认的遍历顺序为插入顺序
public LinkedHashMap() { super(); accessOrder = false; }
通过初始容量实例化
调用父类的构造方法
HashMap(int initialCapacity)
,然后定义默认的遍历顺序为插入顺序public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; }
通过初始容量和加载因子实例化
调用父类的构造方法
HashMap(int initialCapacity, float loadFactor)
,然后定义默认的遍历顺序为插入顺序public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; }
通过初始容量、加载因子和访问顺序实例化
调用父类的构造方法
HashMap(int initialCapacity, float loadFactor)
,然后定义遍历顺序,false为插入顺序,true为访问顺序。该构造方法就是为了实现LRU(最近最少使用)算法而存在的。
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
通过传入一个Map集合实例化
先调用父类HashMap的无参构造,定义默认的遍历顺序为插入顺序,再调用父类的
putMapEntries()
方法将map集合批量插入哈希表中。等同于调用父类的
HashMap(Map<? extends K, ? extends V> m)
构造方法,再确定默认的遍历顺序为插入顺序public LinkedHashMap(Map<? extends K, ? extends V> m) { super(); accessOrder = false; putMapEntries(m, false); }
七、linkNodeLast()
该方法从命名上也不难看出,该方法就是将指定节点添加到双向链表的尾部。
读过LinkedList源码或对双向链表有所了解的同学想必没什么问题,就是把原尾节点作为指定节点的前驱,把指定节点作为原尾节点的后继,还要把指定节点设置为尾节点。
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;
}
}
八、newNode()
在讲解HashMap的putVal()
方法保存键值对时,通过调用newNode()
方法将键值对构造成一个节点对象保存在哈希表中。虽然LinkedHashMap也是通过HashMap的putVal()
方法保存键值对,但是需要对newNode()
方法进行重写,以构造一个Entry
类对象作为哈希表的节点。这是设计模式中模版方法的体现。
在本篇文章中,我们只介绍LinkedHashMap在构造Node节点时做了哪些事,至于putVal()
方法的其他过程,我们依然参考HashMap的putVal()
方法。
在下面的源码中我们看到,在此方法中,构造出来的Node节点对象的实际类型为LinkedHashMap.Entry
,然后维护双向链表通过调用linkNodeLast()
放将新节点放在双向链表的尾部。
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;
}
以下为HashMap中的newNode()
方法,方便我们比较
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
九、newTreeNode()
该方法与上面的newNode()
方法类似,唯一区别就是构造出来的Node节点的实际类型为TreeNode类型,然后依然是维护双向链表通过调用linkNodeLast()
放将新节点放在双向链表的尾部。
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);
return p;
}
以下为HashMap中的newTreeNode()
方法,方便我们比较
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
return new TreeNode<>(hash, key, value, next);
}
十、afterNodeAccess()
该方法在访问节点后调用,该方法作为回调函数在父类HashMap中当节点被访问时调用
调用
put()
、putIfAbsent()
、get()
、getOrDefault()
、compute()
、computeIfAbsent()
、computeIfPresent()
或merge()
方法会导致对相应键值对的访问(假设调用完成后该条目存在)
通过源码的分析我们知道,在构造LinkedHashMap实例时我们指定遍历顺序为访问顺序,则对一个节点发生访问后,该方法会将该节点设置为双向链表的尾节点。这是实现LRU算法的关键一环。
// 如果遍历顺序为访问顺序,则将指定节点设置为双向链表的尾节点,否则直接返回
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;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
十一、afterNodeInsertion()
该方法在插入节点后调用,该方法作为回调函数在父类HashMap中当节点被插入时调用
调用
put()
、putIfAbsent()
、compute()
、computeIfAbsent()
或merge()
方法会导致对相应键值对的插入
在LinkedHashMap中,我们需要知道的是在节点插入到哈希表中以后,需要通过afterNodeInsertion()
方法做什么事情?
从源码的分析中可以看出,该方法就是为了将双向链表中第一个节点通过removeNode()
方法从哈希表中删除,但是要满足三个条件:
- evict参数值为true,这取决于调用方传入的参数
- 双向链表不为空
removeEldestEntry()
方法返回值为true
,该方法用来判断是否要移除指定的元素。
所以,该方法的目的就是在插入一个元素后,是否要删除存在时间最长的元素。该方法在LRU(最近最少使用)删除算法中有着关键的作用。
由于在LinkedHashMap中removeEldestEntry()
方法的返回值为false
,因此该方法实际不发生任何操作。
void afterNodeInsertion(boolean evict) {
// possibly remove eldest
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;
}
十二、afterNodeRemoval()
该方法在删除节点后调用,该方法作为回调函数在父类HashMap中当节点被删除时调用
在LinkedHashMap中,我们需要知道的是在节点从哈希表中删除以后,需要通过afterNodeRemoval()
方法做什么事情?
通过对源码的分析,我们看到,该方法做的就是把从哈希表中删除的节点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;
}
十三、常用方法
LinkedHashMap的大多数常用方法都继承自父类HashMap,在这里不做介绍了,请移步HashMap源码,但由于LinkedHashMap的特性,对继承自父类的某些方法进行了重写。
clear()
清空节点
先调用父类的
clear()
方法将哈希表清空,再通过head = tail = null
清空双向链表public void clear() { super.clear(); // 清空双向链表 head = tail = null; }
containsValue()
判断是否存在包含指定value值的键值对节点
从下面源码可以看出,由于LinkedHashMap将哈希表中的各个节点又额外连接成双向链表,因此可以直接通过双向链表来遍历所有节点,而不是像HashMap的
containsValue()
方法一样去遍历哈希表。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的containsValue()方法 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; }
get()
根据指定的key值获取对应的键值对的value值。
从下面源码看出,LinkedHashMap的get()方法比其父类HashMap的get()方法多了一个步骤:即如果当前LinkedHashMap的遍历顺序为按访问顺序遍历,则会调用afterNodeAccess()方法对获取的键值对进行回调处理。
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; } // HashMap的get()方法 public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
十四、总结
- 底层实现为哈希表+双向链表,是对HashMap的扩展。对哈希表的操作由其父类HashMap实现,对双向链表的操作由LinkedHashMap实现。
- LinkedHashMap的有序分两种,一是根据每个节点的访问顺序实现的,二是根据每个节点的插入顺序(默认)实现的
- 线程不安全
纸上得来终觉浅,绝知此事要躬行。
————————————————我是万万岁,我们下期再见————————————————