LinkedHashMap继承自HashMap,是Hash表和链表的实现,并且依靠着双向链表保证了迭代顺序是插入的顺序。如果 一个key重新插入到LinkedHashMap中,那么这个插入顺序是无效的,也就是说,如果m.put(K,V)时,调用m.containsKey(k),将会返回true,更新value值,但是顺序不变。
public class TestLinkHashMap {
public static void main(String[] args){
Map map = new LinkedHashMap<>();
map.put("1","1");
map.put("2","2");
map.put("3","3");
//重新插入"1"
map.put("1","4");
for (Map.Entry entry:map.entrySet()) {
System.out.println("key="+entry.getKey()+" "+"value="+entry.getValue());
}
}
}
执行结果如下:
key=1 value=4
key=2 value=2
key=3 value=3
从上面的程序我们可以看见,同一key的多次插入,并不会影响其顺序
循环双向链表的头部存放的是最久访问的节点或最先插入的节点,尾部为最近访问的或最近插入的节点,迭代器遍历方向是从链表的头部开始到链表尾部结束,在链表尾部有一个空的header节点,该节点不存放key-value内容,为LinkedHashMap类的成员属性,循环双向链表的入口;
/**
* 双向链表的头部(eldest)
*/
transient LinkedHashMap.Entry head;
/**
* 双向链表的尾部(youngest)
*/
transient LinkedHashMap.Entry tail;
/**
*
*accessOrder为true时,按访问顺序排序,false时,按插入顺序排序
*
*/
final boolean accessOrder;
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry before, after;
Entry(int hash, K key, V value, Node next) {
super(hash, key, value, next);
}
}
Node newNode(int hash, K key, V value, Node e) {
LinkedHashMap.Entry p =
new LinkedHashMap.Entry(hash, key, value, e);
linkNodeLast(p);
return p;
}
void afterNodeAccess(Node e) { // move node to last
LinkedHashMap.Entry last;
// 若访问顺序为true,且访问的对象不是尾结点
if (accessOrder && (last = tail) != e) {
// 向下转型,记录p的前后结点
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)e, b = p.before, a = p.after;
// p的后结点为空
p.after = null;
// 如果p的前结点为空
if (b == null)
// a为头结点
head = a;
else // p的前结点不为空
// b的后结点为a
b.after = a;
// p的后结点不为空
if (a != null)
// a的前结点为b
a.before = b;
else // p的后结点为空
// 后结点为最后一个结点
last = b;
// 若最后一个结点为空
if (last == null)
// 头结点为p
head = p;
else { // p链入最后一个结点后面
p.before = last;
last.after = p;
}
// 尾结点为p
tail = p;
// 增加结构性修改数量
++modCount;
}
}
就是说在进行put之后就算是对节点的访问了,那么这个时候就会更新链表,把最近访问的放到最后,保证链表。关键参数是accessOrder,这个参数只有在public LinkedHashMap(int initialCapacity, float loadFactor,boolean accessOrder) 中可以手动设置为true,其余时候都默认为false
void afterNodeInsertion(boolean evict) { //evict为true时,则移除eldest即头部
LinkedHashMap.Entry first;
// 如果定义了移除规则,则执行相应的溢出
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
如果用户定义了removeEldestEntry的规则,那么便可以执行相应的移除操作
void afterNodeRemoval(Node e) { // unlink
// 从链表中移除节点
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)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;
}
这个函数是在移除节点后调用的,就是将节点从双向链表中删除。
我们从上面3个函数看出来,基本上都是为了保证双向链表中的节点次序或者双向链表容量所做的一些额外的事情,目的就是保持双向链表中节点的顺序要从eldest到youngest。
在LinkedHashMap中默认返回是false,当需要实现LRU算法的时候继承LinkedHashMap的子类重写这个方法即可,accessOrder 需要初始化为true
protected boolean removeEldestEntry(Map.Entry eldest) {
return false;
}
关于LRU算法的具体实现,可以参考这篇博客基于LinkedHashMap实现LRU缓存调度算法原理及应用
put函数在LinkedHashMap中未重新实现,而get函数则重新实现并加入了afterNodeAccess来保证访问顺序,下面是get函数的具体实现:
public V get(Object key) {
Node e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)//安装访问顺序而不是插入的顺序,此时会发生结构改变
afterNodeAccess(e);
return e.value;
}
这里需要注意的是,如果accessOrder为true时,那么将会产生structural modification
官方描述是这样的:
A structural modification is any operation that adds or deletes one or more
mappings or, in the case of access-ordered linked hash maps, affects
iteration order. In insertion-ordered linked hash maps, merely changing
the value associated with a key that is already contained in the map is not
a structural modification. In access-ordered linked hash maps,
merely querying the map with get is a structural modification.
大致意思如下:对于 access-ordered模式来说,增加或者删除操作,都会产生structural modification。在access-ordered模式下,会影响迭代顺序。例如查询map是调用get方法,就会产生structural modification。在insertion-ordered模式下,如果仅仅是修改一个已经存在的Key所映射的value,那么不会产生structural modification。
LinkHashMap继承HashMap并且实现了Map接口,它的Entry继承自HashMap的Node,在Entry里增加了before和after,保证了链表的有序。增加了head和tail,用于记录双向链表的头部和尾部。其中head在保留HashMap的查找效率的同时,可以按照元素的插入顺序进行遍历,并提供了元素的LRU访问