旧游无处不堪寻。无寻处,惟有少年心。
1.LinkedHashMap概述
LinkedHashMap来自于JDK1.4,直接继承自 HashMap,并且在 HashMap 基础上,通过维护由所有Entry节点构成的双向链表,来保证元素有序。
LinkedHashMap 继承了 HashMap,因此具有和 HashMap一样的快速查找特性。LinkedHashMap 对 HashMap 高度复用,因此建议先学习HashMap的相关知识:Java容器深度总结:HashMap
2.LinkedHashMap的定义
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
LinkedHashMap类图:
LinkedHashMap数据结构:
由于JDK1.8开始,HashMap底层使用的数据结构是:数组+单链表+红黑树,因此LinkedHashMap的底层数据结构则是:HashMap的底层数据结构+双向链表。其中双向链表的性质由after和before指针维护。
3.主要类属性
LinkedHashMap在拥有HashMap的相关属性时,还具有以下自己特有的属性。
3.1 transient LinkedHashMap.Entry< K,V > head
用于保存双向链表首结点的引用,也就是头结点指针。
3.2 transient LinkedHashMap.Entry< K,V > tail
用于保存双向链表尾结点的引用,也就是尾指针。
3.3 final boolean accessOrder
LinkedHashMap排序方式标志。true:按访问顺序;false:按插入顺序。
4.Entry节点
LinkedHashMap的Entry继承了HashMap的Node类,并且每个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);
}
}
Entry的类图:
5.构造函数
5.1 LinkedHashMap()
构造一个具有默认初始容量 (16) 和 默认加载因子 (0.75) 的空LinkedHashMap 实例,并且双向链表维持的顺序默认为元素插入顺序(accessOrder = false)。
public LinkedHashMap() {
//调用父类HashMap的无参构造器
super();
accessOrder = false;
}
5.2 LinkedHashMap(initialCapacity)
构造一个具有指定初始容量和默认加载因子 (0.75) 的LinkedHashMap 实例。并且双向链表维持的顺序默认为元素插入顺序。
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
需要注意的是:HashMap的容量必须是2的整数次幂,因此对于传入的初始容量initialCapacity,其哈希表的真实容量是大于等于initialCapacity的最小2的整数次幂的数,例如:initialCapacity = 10,哈希表实际的容量为16。
5.3 public LinkedHashMap(initialCapacity, loadFactor)
构造一个具有指定初始容量和加载因子的空 LinkedHashMap 实例。并且双向链表维持的顺序默认为元素插入顺序。
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
5.4 LinkedHashMap(initialCapacity,loadFactor,accessOrder)
构造一个具有指定初始容量、加载因子和排序方式的空 LinkedHashMap 实例。
public LinkedHashMap(int initialCapacity,float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
5.5 LinkedHashMap(Map<? extends K,? extends V> m)
构造一个包含指定Map中的元素的 LinkedHashMap 实例。所创建的 LinkedHashMap 实例具有默认的加载因子 (0.75) 和 足以容纳指定映射中映射关系的初始容量,并且双向链表维持的顺序默认为元素插入顺序。
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
//调用父类的方法
putMapEntries(m, false);
}
6.双向链表的维护
6.1 插入节点后的维护(linkNodeLast)
LinkedHashMap并没有重写其父类HashMap的put方法,而是重写了linkNodeLast()方法将新节点连接到双向链表的尾部。
HashMap在put元素时,会根据计算出的key所在的桶位的节点类型,再创建普通Node节点还是红黑树TreeNode节点,然后newNode()或newTreeNode()方法创建节点,LinkedHashMap重写了这两个方法,并且linkNodeLast()方法会在这两个方法中被调用,以维护双向链表的顺序。
// Overrides method 'newNode' in java.util.HashMap
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;
}
// Overrides method 'newTreeNode' in java.util.HashMap
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;
}
linkNodeLast()方法:
/**
* 新节点链接到双向链表尾部
*
* @param p 新节点
*/
private void linkNodeLast(LinkedHashMap.Entry<K, V> p) {
LinkedHashMap.Entry<K, V> last = tail;
//如果tail和head都为null,那么新添加第一个节点时,tail和head都指向该节点
tail = p;
if (last == null)
head = p;
/*否则,将新节点链接到双向链表末尾,新节点成为新的tail节点*/
else {
p.before = last;
last.after = p;
}
}
6.2 删除元素后的维护(afterNodeRemoval)
HashMap调用removeNode()方法实现对HashMap中元素的删除,removeNode()方法调用了afterNodeRemoval()方法,该方法在HashMap中为空实现。LinkedHashMap通过重写HashMap中的afterNodeRemoval()方法,实现双向链表中该元素结点的删除。
// HashMap中afterNodeRemoval的实现
// HashMap移除节点后调用,为空实现
void afterNodeRemoval(Node<K,V> p) { }
//Overrides method 'afterNodeRemoval' in java.util.HashMap
// 参数 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;
}
6.3 访问元素后双向链表的顺序维护(afterNodeAccess)
LinkedHashMap中的双向链表默认按元素插入的顺序排列,也可以在构造LinkedHashMap是指定为元素的访问顺序(accessOrder = true)。
afterNodeAccess()方法会在一个元素节点被访问到时被调用,同样该方法在HashMap中为空实现,LinkedHashMap 中覆写了afterNodeAccess()方法。
也就是说,当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。保证链表尾部是最近访问的节点,那么链表首部就是最近未使用的节点。
//在元素被访问时,会调用afterNodeAccess方法,HashMap中的方法为空实现
void afterNodeAccess(Node<K, V> p) {
}
/**
* LinkedHashMap 中重写的afterNodeAccess方法,用于将被访问到的节点移动到双向链表末尾
*
* @param e 被访问的节点
*/
void afterNodeAccess(Node<K, V> e) { // move node to last
LinkedHashMap.Entry<K, V> last;
/*如果e不是尾节点,那么尝试移动e到尾部*/
if (accessOrder && (last = tail) != e) {
//p记录e,b保存p在大链表中的前驱,a保存p在大链表中的后继
LinkedHashMap.Entry<K, V> p =
(LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
//p的后继置空
p.after = null;
//如果b为null,表明p为头节点
if (b == null)
//头节点设置为p的后继a
head = a;
else
//否则b的后继设置为a
b.after = a;
/*如果a不为null,a的前驱设置为b*/
if (a != null) {
a.before = b;
}
/*否则,尾节点设置为b*/
else {
last = b;
}
//如果,last为null
if (last == null)
//那么头节点指向p
head = p;
else {
/*否则,将p链接在链表的最后*/
p.before = last;
last.after = p;
}
//尾节点指向p
tail = p;
++modCount;
}
}
两种排序方式:
public class LinkedHashMapTest {
public static void main(String[] args) throws Exception {
LinkedHashMap map = new LinkedHashMap(16, 0.75f, false);
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
map.put(5, 5);
System.out.println("按元素插入顺序:"+map.keySet());
LinkedHashMap map2 = new LinkedHashMap(16, 0.75f, true);
map2.put(1, 1);
map2.put(2, 2);
map2.put(3, 3);
map2.put(4, 4);
map2.put(5, 5);
map2.get(1);
System.out.println("按元素访问顺序:"+map2.keySet());
}
}
输出:
按元素插入顺序:[1, 2, 3, 4, 5]
按元素访问顺序:[2, 3, 4, 5, 1]
7.LRU缓存
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。
7.1 LRU基于链表的实现算法
以链表尾部保存最近访问数据为例 :
- 新数据插入到链表尾部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表尾部;
- 指定LRU缓存的容量,当链表长度大于容量时,将链表头部的数据丢弃。
7.2 LinkedHashMap与LRU缓存
LinkedHashMap已经提供了基于访问顺序的迭代机制,最近被访问的节点在尾部,最远被访问的节点在头部,并且提供了链表头部的数据丢弃的实现。
7.2.1 afterNodeInsertion()方法
由于LRU缓存必然要有容量限制,因此在每次添加完元素之后需要判断,当前元素个数是否大于缓存容量:
若元素个数 > 缓存容量 ,则删除链表头部最近未使用的节点。
因此,LinkedHashMap在成功插入元素操作之后,不光会调用linkNodeLast方法,将新元素插入链表尾部,最后还会调用afterNodeInsertion()方法确保元素个数不会超过缓存容量。
//HashMap提供的空实现
void afterNodeInsertion(boolean evict) {
}
/**
* LinkedHashMap重写的实现
*
* @param evict 构造器中传递false,单独调用方法传递true
*/
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K, V> first;
//如果evict为true,并且大链表头节点不为null,并且removeEldestEntry(first)方法返回true
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
//那么调用removeNode移除头节点,这一移除方法中具有afterNodeRemoval方法
removeNode(hash(key), key, null, false, true);
}
}
Tip:
- afterNodeInsertion()方法在HashMap中提供空实现。
- evict 参数只有在调用包含指定Map中的元素的构造方法中被指定为false。
7.2.2 removeEldestEntry()方法
afterNodeInsertion()方法内部调用了removeEldestEntry()方法并以返回值作为是否需要移除头节点的判断条件之一。在LinkedHashMap中,该方法始终返回false。
// 始终返回 fasle
// 重写该方法 实现LRU缓存
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return false;
}
也就是说,LinkedHashMap默认是不会删除链表首部最近未使用的节点。
removeEldestEntry()方法在实现LRU缓存的过程中,起着不可或缺的作用:在该方法中比较节点总数和缓存容量的大小,当元素个数 > 缓存容量时返回true,这样afterNodeInsertion()方法就能去删除最近未使用的节点,从而保证缓存空间足够。
7.3 LinkedHashMap实现LRU缓存
基于上面的分析,我们可以了解到:LinkedHashMap实现LRU缓存的关键在于重写removeEldestEntry()方法,因此LinkedHashMap实现LRU缓存的步骤为:
- 设定最大缓存空间 maxEntries;
- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
- 覆盖 removeEldestEntry() 方法实现,在节点多于 maxEntries 就会将最近未使用的数据移除。
示例代码:
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int maxEntries;//cache 容量
public LRUCache(int maxEntries) {
super(16, 0.75f, true);
this.maxEntries = maxEntries;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size() > maxEntries;
}
public static void main(String[] args) {
LRUCache cache = new LRUCache(3);
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);
cache.get(1); // cache命中
cache.put(4, 4);
System.out.println(cache.keySet());
}
}
输出:
[3, 1, 4]
8.迭代器
LinkedHashMap的迭代器在设计上与HashMap有异曲同工之处。
LinkedHashMap的迭代器有三种:
- LinkedKeyIterator
- LinkedValueIterator
- LinkedEntryIterator
他们都继承自LinkedHashIterator抽象类,并且实现了Iterator接口。其中LinkedHashIterator为这些迭代器提供了对Iterator接口方法的基本实现,他们惟一的区别就是next()方法的实现不同。
注意:LinkedHashMap的三种迭代器都是快速失败(fail-fast)的。