前言:
在上一篇博客中,我们系统的介绍了HashMap,HashMap是非线程安全的类,一般情况下作为局部变量使用是完全可以的。
HashMap的存放是无序的,按照一定的规则映射的,所以,遍历其元素的顺序与添加的顺序是不一致的。
如果我们想按照添加的顺序来遍历元素该如何做呢?
那就需要使用本次介绍的LinkedHashMap来完成了。
注意:由于LinkedHashMap大量使用了HashMap的方法,所以不太明白HashMap工作原理的同学,可以先看下笔者另一篇关于HashMap的文章,https://blog.csdn.net/qq_26323323/article/details/86219905
1.LinkedHashMap结构分析
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>{
// 双端链表的头元素
transient LinkedHashMap.Entry<K,V> head;
// 双端链表的尾元素
transient LinkedHashMap.Entry<K,V> tail;
// 默认为false,指的是添加顺序;
// 当指定为true时,访问顺序,这个在后面再详细说明
final boolean accessOrder;
// 不同于HashMap.Node
// 继承了HashMap.Node,同时还扩展了自己的属性before、after
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);
}
}
通过对其结构的分析,可以确定,LinkedHashMap就是HashMap与LinkedList的结合体。
其继承HashMap,通过HashMap来实现基本功能;
其添加head和tail属性,并且在Entry节点中,添加before和after属性,来实现双向链表的功能。
2.LinkedHashMap构造方法
// 1.空参构造
public LinkedHashMap() {
// 直接调用new HashMap()
super();
accessOrder = false;
}
// 2.初始化数组容量
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
// 3.初始化数组容量和负载因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
// 4.初始化数组容量和负载因子和AccessOrder
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
// 5.以一个Map来初始化
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
之前介绍过HashMap,所以对上述super方法不再介绍,LinkedHashMap都是直接调用HashMap的方法
注意这里的AccessOrder,默认值都是false。
3.添加操作
发现在LinkedHashMap中没有找到put方法,说明其直接使用了HashMap.put()方法
但是在LinkedHashMap中其重写了newNode()方法,有关于put方法的具体过程,笔者不再分析,有兴趣的可以看一下笔者上一篇关于HashMap的文章
// LinkedHashMap.newNode()
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.linkNodeLast()
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
// last为null,说明当前元素为第一个元素,直接社会为头元素
if (last == null)
head = p;
else {
// 将p元素,放置到链表的末尾
p.before = last;
last.after = p;
}
}
总结:添加操作一方面将节点存放到哈希表中,另一方面使用双端链表来维持其节点添加的前后关系
4.删除操作
删除操作也是直接使用了HashMap.remove()方法,
我们在关注HashMap.remove()方法时看到以下几个方法
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }// 在HashMap.removeNode()方法最后实现了这个空参方法
这个afterNodeRemoval方法主要就是给LinkedHashMap使用的
// LinkedHashMap.afterNodeRemoval()
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;
}
主要功能就是将双端链表中的元素删除掉
5.查询操作
这个get方法,在LinkedHashMap中实现了
public V get(Object key) {
Node<K,V> e;
// 还是直接调用HashMap.getNode()方法
if ((e = getNode(hash(key), key)) == null)
return null;
// 默认accessOrder为false,
// 我们来看下,假如其为true时,afterNodeAccess在做什么
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
// LinkedHashMap.afterNodeAccess()
// 将被访问的node放到链表的最后
void afterNodeAccess(Node<K,V> e) { // move node to last
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;
}
}
这个查询操作的afterNodeAccess方法弄的一脸蒙圈,这个在干嘛呢?
这个需要结合刚才提到的添加操作来一起看。
HashMap.put()方法中有一个空实现方法afterNodeInsertion(),而LinkedHashMap默认实现了这个方法,如下所示
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
// 删除最早的元素,这时需要removeEldestEntry()==true
// 但是这个方法在LinkedHashMap中直接返回了false
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
// LinkedHashMap.removeEldestEntry()
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
还是没明白!!!
接着看...
6.LinkedHashMap实现LRU算法
很多缓存组件都有具体的缓存删除算法,LRU作为一种经典算法,最近最少使用算法,将最近最少使用的数据删除。
摘自网友的一个实现,来自:https://www.cnblogs.com/xiaoxi/p/6170590.html
public class LRUCache extends LinkedHashMap
{
public LRUCache(int maxSize)
{
// 注意这个true,设置的就是LinkedHashMap.accessOrder
super(maxSize, 0.75F, true);
maxElements = maxSize;
}
// 这个方法似曾相识,就是刚才put()方法中调用的方法
protected boolean removeEldestEntry(java.util.Map.Entry eldest)
{
// 如果map中添加的元素超过maxElement最大限制之后,这个就会返回true
// 结合刚才put()方法,在添加新元素的时候就会删除最近最少使用的元素
return size() > maxElements;
}
private static final long serialVersionUID = 1L;
protected int maxElements;
}
那么关键问题来了,什么叫做最近最少使用呢?
我们使用LinkedList来维持元素的添加顺序,最新添加的元素放在链表尾部。
如果元素在被访问后,就将元素移动到链表尾部,那么越靠近链表尾部的元素就越是活跃的元素,同理,在链表头部的元素则是已经很久没有访问的元素,符合我们的最近最少使用原则,则可以删除之。
LinkedHashMap是如何实现的呢?我们在看一下其get()方法,其中调用了afterNodeAccess()方法,而这个方法就是将被访问的元素放置到链表尾。
同时在做put操作的时候,我们重写了removeEledestEntry()方法,这样每次添加元素的时候都会检查是否超过最大元素个数,超过了则删除链表最头部的元素