我们平时用LinkedHashMap的时候,都会写下面这段
LinkedHashMap map = new LinkedHashMap<>();
map.put("student", "333");
map.put("goods", "222");
map.put("product", "222");
然后我们通常都会去看 put 方法,但是我们点到LinkedHashMap内部后,发现没有put方法,这是为什么呢?
其实这个不难,因为LinkedHashMap继承子HashMap
public class LinkedHashMap
extends HashMap
implements Map
{
}
这就好理解了。因为put是集成自HashMap,那么LinkedHashMap的数据也是 数组+链表 的形式存储的吗?我们慢慢往下看
在HashMap中,put一个数据的时候,会调用一个newNode方法来创建节点,而LinkedHashMap重写了该方法
Node newNode(int hash, K key, V value, Node e) {
LinkedHashMapEntry p =
new LinkedHashMapEntry(hash, key, value, e);
linkNodeLast(p);
return p;
}
在每次创建节点的时候,都调用了一次linkNodeLast方法,来拼接链表。
tail代表链表尾巴,head代表链表脑袋
entry.before代表前驱
entry.after代表后置
private void linkNodeLast(LinkedHashMapEntry p) {
LinkedHashMapEntry last = tail;
tail = p;
//判断尾部是否是空的,为空就认为链表没创建,拼接在头上
if (last == null)
head = p;
else {
//在最后一个节点的before上放前一个节点
p.before = last;
//在after上放置当前节点
last.after = p;
}
}
通过这个方法,我们就对LinkedHashMap有了一个初步的了解了。before和after分别指向前驱和后置,这是典型的双向链表的结构,稍等,我去画个赋予灵魂的配图。
有了这个图就好理解多了~
当我们 put 数据的时候,除了创建节点之外,还有一个操作,就是HashMap会回调一个 afterNodeInsertion方法,我们看一下LinkedHashMap的实现
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMapEntry first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry eldest) {
return false;
}
其实这个方法是移出最老的节点,但这段代码在jdk1.8里就不在被执行了,除非你自己集成LinkedHashMap重写removeEldestEntry方法。因为removeEldestEntry=false,OK,当我们在put数据的时候,整个双链表就建立起来了,接下来我们看下get有什么操作吧
final boolean accessOrder;
public V get(Object key) {
Node e;
if ((e = getNode(hash(key), key)) == null)
return null;
//顺序访问模式
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
LinkedHashMap的get操作首先会从 HashMap 维护的数据中通过hash获取Node,然后判断accessOrder属性,如果等于true就调用afterNodeAccess方法
那么accessOrder是个什么呢?有什么用呢?
其实accessOrder是个标记位,用来标记数据是否按照访问顺序处理,如果设置为true,那么我们每次访问数据,这个数据都会被移动到链表尾部,就会导致链表尾部的访问频次是最高的(年老的变量),链表头部是访问频次最低的(年轻的变量),这个特性正好适合做LRU缓存。如果设置为false,也就是默认的模式,那么就是按照存储顺序存储数据,访问也不会触发置尾操作。我们接下来看一下它是怎么做到的置尾吧。
首先通过这个构造方法,把accessOrder初始化成true,默认是false
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
然后我们试一下效果
Map map = new LinkedHashMap<>(
1 << 4, 0.75f, true
);
map.put("node1", "node1");
map.put("node2", "node1");
map.put("node3", "node1");
map.put("node4", "node1");
map.get("node1");
System.out.println(map);
{node2=node1, node3=node1, node4=node1, node1=node1}
和预期一样,访问了一次node1,它就把node1放在链表的尾巴上了,这个操作主要是在afterNodeAccess内,我们接下来看下是怎么实现的吧
void afterNodeAccess(Node e) { // move node to last
LinkedHashMap.Entry last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)e, b = p.before, a = p.after;
p.after = null;
//如果我们get的数据是表头的数据,那么表头就需要更新为表头的后置
//比如node1->node2->node3,我们获取node1的时候,node1要跑到队尾
//所以node2就是老大
if (b == null)
head = a;
else
//否则的话,把get的数据的前置节点和get的数据的后置节点连接
//比如,get node2,node2的before正是node1
//因为node2要去队尾了,所以node1就不能在绑定after为node2了,要改成node3
b.after = a;
//a等于空说明p是队尾。因为只有队尾的后置节点是空的
if (a != null)
//把操作数据的后置节点连接上操作数据的前置节点
//比如,get node2,node的after便是node3
//node3的before在没改变的时候是node2,结果node2要去队尾,所以要连接都node1去
a.before = b;
else
//a等于空说明什么?说明p的后置节点是空的。说明p可能是队尾
last = b;
//假设last等于b的时候。结果b是空的,按照规则,before为空就要成为头
if (last == null)
head = p;
else {
//把操作数据的前置节点设置成队尾,准备去队尾了。。。
p.before = last;
//把刚才队尾的后置节点,设置成刚刚操作的node2,实锤了,真的都队尾了
last.after = p;
}
//执行队尾赋值
tail = p;
++modCount;
}
}
这个方法我在啰嗦总结一下吧
1.先把操作数据的前置和后置找处理
2.然后把它前置和它后置做链接
3.把它的前置链接到之前的队尾上,再把之前的队尾的后置链接到它身上
4.最后把队尾改成操作的数据即可
最后再让我这个灵魂画手配张图吧~
最后聊一下resize吧。既然是集成自HashMap,那么肯定也是到达了扩容阀值就要扩容的
我们去找LinkedhashMap内部,发现没有重写resize,那就说明它的扩容是由父类HashMap完成的。具体的扩容过程,可以看我另一篇讲解HashMap的文章