LinkedHashMap源码分析
文章目录
继承体系
实现的接口:
- Map
- Clonable
- Serializable
继承的类:
- 间接继承 AbstractMap(抽象类)
- 直接继承 HashMap
LinkedHashMap直接继承了HashMap,间接继承了AbstractMap,拥有HashMap所拥有的所有特性,同时自己在结构上发生了变化(从名字可以看出,底层数据结构使用了链表),使其增加了可以按照一定顺序访问的特性。
存储结构
因为LinkedHashMap继承了HashMap,所以底层为数组 + 单链表 + 红黑树,同时在每个节点添加了一对指针,也就是双向链表,这使其增加了可以按照一定顺序访问的特性。
属性
- head:双向链表的头节点,旧数据存在头节点。
- tail:双向链表的尾节点,新数据存在尾节点。
- accessOrder:是否需要按访问顺序排序,如果为false则按插入顺序存储元素,如果是true则按访问顺序存储元素。
/**
* 双向链表的头节点
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 双链表的尾节点
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 此链接哈希映射的迭代排序方法:访问顺序为true ,插入顺序为false 。
*/
final boolean accessOrder;
内部类
实际上一个存储节点有三个指针,next继承子HashMap.Node,用于单链表存储于桶中,before和after用于双向链表存储所有元素。
/**
* HashMap.Node的子类,在HashMap.Node的基础上添加了一对指针
*/
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中的HashMap.Node
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
}
构造方法
/*
构造一个具有指定初始容量和负载因子的空插入排序LinkedHashMap实例。
参数:
initialCapacity - 初始容量
loadFactor – 负载因子
抛出:
IllegalArgumentException – 如果初始容量为负或负载因子为非正
*/
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
/*
构造一个具有指定初始容量和默认加载因子 (0.75) 的空插入排序LinkedHashMap实例。
参数:
initialCapacity - 初始容量
抛出:
IllegalArgumentException – 如果初始容量为负
*/
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
/*
构造一个具有默认初始容量 (16) 和加载因子 (0.75) 的空插入排序LinkedHashMap实例。
*/
public LinkedHashMap() {
super();
accessOrder = false;
}
/*
构造一个插入顺序的LinkedHashMap实例,其映射与指定的映射相同。 LinkedHashMap实例是使用默认加载因子 (0.75) 和足以在指定映射中保存映射的初始容量创建的。
参数:
m - 其映射将放置在此map中的map
抛出:
NullPointerException – 如果指定的地图为空
*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
/*
构造一个具有指定初始容量、加载因子和排序模式的空LinkedHashMap实例。
参数:
initialCapacity - 初始容量
loadFactor – 负载因子
accessOrder – 排序模式 - 访问顺序为true ,插入顺序为false
抛出:
IllegalArgumentException – 如果初始容量为负或负载因子为非正
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
注意观察:前4个构造方法都将 accessOrder(为true是按照访问顺序,为false是按照插入顺序)都等于false,说明双向链表是 按插入顺序存储元素 。
最后一个构造方法accessOrder从参数传递,意味着可以将其设置为true,也就是按照访问顺序存储元素,这是实现 LRU缓存策略 的关键。
成员方法
三个后置处理方法
afterNodeInsertion
afterNodeAccess
afterNodeRemoval
这些方法在HasMap的方法中调用过,但是在HashMap中没有实际实现,而在LinkedHashMap中有了实际实现,是LinkedHashMap中重要的回调方法。
HashMap中定义的这三个方法:
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
afterNodeInsertion(boolean evict)方法
直接翻译方法名是在节点插入后做的事情,也就插入新节点的后置处理,这个方法在HashMap的putVal方法中被调用。
在LinkedHashMap中的实现:
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);
}
}
// afterNodeInsertion调用的removeEldestEntry方法
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
首先来分析参数:boolean evict
- evict 翻译过来是驱逐,赶出的意思。
- 如果 evict 为 true ,且头节点不为空,且确定移除最老的元素,那么就调用
HashMap.removeNode()
把头节点移除(注意: 这里的头节点是双向链表的头节点,而不是某个桶中的第一个元素 ) afterNodeInsertion
在LinkedHashMap中用来在移除元素后修改双向链表- if 判断中默认调用的
removeEldestEntry
方法默认返回false,也就是不移除最早插入元素。
afterNodeAccess(Node<K,V> e)方法
在节点访问之后被调用,主要在put()
已经存在的元素或get()
时被调用,如果accessOrder
为true,调用这个方法把访问到的节点移动到双向链表的末尾。
void afterNodeAccess(Node<K,V> e) { // move node to last
// 指针last,用于指向链表的尾部
LinkedHashMap.Entry<K,V> last;
// 如果accessOrder为true并且当前访问到的节点不位于链表尾部,就执行以下逻辑
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;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
// 将p节点重新插入到链表的尾部
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
// 更新尾结点为p
tail = p;
++modCount;
}
}
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节点从链表中删除
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
增添方法
LinkedHashMap并没有重写put方法,实际上调用的是父类的put方法,但重要的是重写了构建新节点的newNode()
方法。newNode()
方法就是创建一个新的节点并返回,但是因为LinkedHashMap的重写,返回的就不是HashMap中的内部类Node的对象,而是LinkedHashMap中内部类Entry的对象。
来看看具体实现:
重写后的newNode
方法在返回节点之前会调用linkNodeLast
方法。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;
}
// newNode方法中调用的linkNodeLast方法。
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
// 如果链表中没有节点,就将head指针指向新创建的p节点
if (last == null)
head = p;
else {
// 将新创建的p节点插入到链表的尾部。
p.before = last;
last.after = p;
}
}
移除方法
同样没有重写父类的remove()
方法,所以逻辑与HashMap中发删除方法逻辑上没有什么变化。但是LinkedHashMap重写了afterNodeRemoval
方法,该方法会在removeNode
方法会涉及删除节点的操作中被回调,同时也是真正实现删除节点的方法。
查询方法
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;
}
LinkedHashMap实现LRU缓存淘汰策略
LRU(Least Recently Used),是一种缓存淘汰策略,翻译过来是最近最少使用,也就是优先淘汰最近最少使用的元素。
accessOrder
设置为true就可以实现LRU。
import java.util.LinkedHashMap;
import java.util.Map;
class LRU<K, V> extends LinkedHashMap<K, V> {
// 保证缓存容量
private int capacity;
public LRU(int capacity, float loadFactor) {
super(capacity, loadFactor, true);
this.capacity = capacity;
}
/**
* 重写LinkedHashMap的removeEldestEntry方法,当元素数量大于缓存(capacity)容量时删除旧元素
*
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > this.capacity;
}
}
测试:
/**
* 测试实现LRU
*/
public class LRUTest {
public static void main(String[] args) {
// 创建一个只有5个元素的缓存
LRU<Integer, Integer> lru = new LRU<>(5, 0.75f);
lru.put(1, 1);
lru.put(2, 2);
lru.put(3, 3);
lru.put(4, 4);
lru.put(5, 5);
lru.put(6, 6);
lru.put(7, 7);
System.out.println(lru.get(4));
lru.put(6, 999);
System.out.println(lru);
}
}
控制台打印:
4
{3=3, 5=5, 7=7, 4=4, 6=999}
可以看到可以看到最旧的元素被删除了,同时最近访问过的元素被移动到了末尾位置。
总结
- LinkedHashMap继承了HashMap,具有HashMap的所有特性
- LinkedHashMap内部维护了一个双向链表存储所有的元素(在内部类Entry继承HashMap.Node的基础上加了一对指针)
- 成员变量中新增了
accessOrder
属性:- 如果
accessOrder
为false,则可以按插入元素的顺序遍历元素 - 如果
accessOrder
为true,则可以按访问元素的顺序遍历元素
- 如果
- LinkedHashMap很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法
- 默认的LinkedHashMap并不会移除旧元素,如果需要移除旧元素,则需要重写
removeEldestEntry()
方法设定移除策略 - LinkedHashMap可以用来实现LRU缓存淘汰策略