linkHashMap分析
1. link体现在哪里?
LinkHashmap继承与HashMap。重写了其中的几个方法。并且在hashMap的基础上维护了一个双向列表。用于连接所有的实体,链表连接的顺序通常来说是插入的顺序,注意这里说的是通常,也是可以改变的。简单来说,LinkHashmap维护了一个链表,用于保持节点的顺序(插入或者查找)。
从源码角度来看
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
final boolean accessOrder;
}
在LinkHashMap中。新增了几个属性
-
LinkedHashMap.Entry<K,V> head:链表的头节点
-
LinkedHashMap.Entry<K,V> tail:链表的尾节点
-
boolean accessOrder:链表连接的顺序
说明
在构建列表的时候,是按照插入顺序来构建的,并且利用accessOrder是可以实现LRU的功能。其实他自己来实现了这样的功能
- true:访问模式,意思就是在访问的时候,会将这次访问的元素移动到链表的的末尾。
- false:插入模式,在插入的时候会将这个元素连接到链表的末尾。但是访问的时候并不会。
链表什么时候链接?链表怎么链接?
在put方法里面newNode的时候链接链表的。并且通过尾插法
LinkHashMap重写了HashMap的newNode方法,自己写了一个内部类Entry,在这里就链接链表。这样就能在之前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;
}
// 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);
}
}
// 链表的连接在linkNodeLast方法
//这里的连接操作也很简单,尾插法。
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;
}
}
2. 和HashMap不同的地方在哪里?
重写了几个拓展的方法,并且存放的Node的类型不一样,还有迭代器相关的部分。
-
支持顺序
-
重写了newNode方法,创建了LinkedHashMap.Entry对象。这个对象在上面已经介绍了。
-
重写了下面三个重要的拓展方法
void afterNodeAccess(Node<K,V> p) { } //访问节点 void afterNodeInsertion(boolean evict) { } //插入节点 void afterNodeRemoval(Node<K,V> p) { } //移除节点通过这三个方法并且结合双向链表。能实现很多有意思的东西。链表的移动的时间复杂度是o(1)。
-
支持在插入和访问的时候做拓展。
-
不同于HashMap的迭代器和EntrySet
3. accessOrder分析
accessOrder为true,访问顺序具体是怎么做的,实现的样子是什么。
先从列子开始
@Test
public void testLinkHashMap(){
LinkedHashMap<String, String> hashMap = new LinkedHashMap<>(16,0.75F,true);
hashMap.put("b","b");
hashMap.put("a","a");
hashMap.put("c","c");
System.out.println(hashMap);
hashMap.get("a");
System.out.println(hashMap);
}
结果:
{b=b, a=a, c=c} {b=b, c=c, a=a}
可以看到,设置为true之后,在访问的时候会将当前访问的节点移动到链表的末尾。这样一来,链表的头就是最老的元素了(没有访问过得),要实现LRU的话,直接移除链表的第一个节点就好了。前提是链表的长度不是无限的,否则LRU就实在没有意义。
怎么实现的?
要从get方法开始,从get方法进去,可以看到下面的代码
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;
}
//继续看,重点就是afterNodeAccess方法可以继
afterNodeAccess
//这里的代码逻辑也很简单, 就是对双向链表的操作。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//首先,当前这个节点不是末尾节点。
if (accessOrder && (last = tail) != e) {
//p就是e,b是p.before,a是p.after;
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
// 没有前继节点,那只能说明是当前的这个节点是头节点
if (b == null)
//直接将头结点赋值为a。
head = a;
else
//不是头结点,就正常连。
b.after = a;
//a后面不是null
if (a != null)
//a的前一个就是b
a.before = b;
else //a==null。
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
// 将p赋值给尾节点。
tail = p;
//这是快速失败的关键。记录真实修改数。
++modCount;
}
}
就是将p移动到尾节点, 在这个中间对几种情况进行判断,比如头结点,尾节点。
accessOrder为false,访问顺序具体是怎么做的,实现的样子是什么。
先从列子开始
@Test
public void testLinkHashMap(){
LinkedHashMap<String, String> hashMap = new LinkedHashMap<>(16,0.75F);
hashMap.put("b","b");
hashMap.put("a","a");
hashMap.put("c","c");
System.out.println(hashMap);
hashMap.get("a");
System.out.println(hashMap);
hashMap.remove("a");
System.out.println(hashMap);
hashMap.put("a","a");
System.out.println(hashMap);
}
结果
{b=b, a=a, c=c} {b=b, a=a, c=c} {b=b, c=c} {b=b, c=c, a=a}
从put方法进去在看看,其实就是上面说的构建链表的地方,newNode的时候构建的,因为accesOrder是false,所以,在get的时候不会走afterNodeAccess,但是这里我还得在说一下afterNodeInsertion方法
//evict的为true,这个值是从putVal方法里面传递进来的
//在removeEldestEntry这个方法是干嘛的?可以继续看看,如果说evict为true,并且head不是null,并且removeEldestEntry为true的话,就会移除头节点。这个听起来就是LRU的实现。
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);
}
}
在linkHashMap里面默认返回为false。那如果重写了这个方法,然后基于某种条件返回true,比如,元素的数量不能超过100,超过的话,就会移除头节点。配合accessOrder为true,在使用一次之后就会将节点移动到链表末尾。这不就实现了LRU了吗?建议看看removeEldestEntry方法上面的注释。说的很明确。具体的列子看看往下看,在第四节里面会说。
至于afterNodeRemoval方法,这方法里面干的事情就是在元素从table中移除之后,这个元素也得从链表里面移除。
//看人家这注释,很明确unlink。
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;
}
4. linkHashMap实现LRU
说明:
这样的例子多的是,可以查看LinkHashMap类关系图,看看。下面的这个是我自己的demo
class MyLruCache<K,V> extends LinkedHashMap<K,V>{
public MyLruCache(){
super(16,0.75F,true);
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return this.size()>3;
}
}
@Test
public void testLruCache(){
MyLruCache<String, String> lruCache = new MyLruCache<>();
lruCache.put("b","b");
lruCache.put("a","a");
lruCache.put("c","c");
System.out.println("初始化:" + lruCache);
lruCache.get("a");
System.out.println("获取a之后:" + lruCache);
lruCache.put("d","d");
System.out.println("put d之后" + lruCache);
}
初始化:{b=b, a=a, c=c} 获取a之后:{b=b, c=c, a=a} put d之后{c=c, a=a, d=d}
可以看到,获取a之后,a放在了末尾,放了d之后,b为队首。b被移除。
发现了一个好玩的事情
@Test
public void testLinkHashMap骚操作(){
LinkedHashMap<String, String> hashMap = new LinkedHashMap<>(16,0.75F,true);
hashMap.put("b","b");
hashMap.put("a","a");
hashMap.put("c","c");
System.out.println(hashMap);
hashMap.put("a","a1");
System.out.println(hashMap);
}
答案:
{b=b, a=a, c=c} {b=b, a=a1, c=c}
如果accessOrder为true。在key重复的时候,并且onlyIfAbsent为false。那么在key相同的时候的前提下, onlyIfAbsent为false或者key对应的value为null,就会覆盖原来的值。会调用afterNodeAccess(e);方法。就会将当前访问的key放在列表的末尾。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
关于LinkHashMap就分析到这里了。如有不正确的地方,欢迎指出。谢谢。