LinkedHashMap字面上意思是链表HashMap,那么到底增加了什么特性呢?
从一个简单的case剖析下去
Map map = new HashMap<>();
map.put(3, 3);
map.put(5, 5);
map.put(1, 1);
map.forEach((k, v) -> System.out.print(k + " "));
结果是
1 3 5
Map map = new LinkedHashMap<>();
map.put(3, 3);
map.put(5, 5);
map.put(1, 1);
map.forEach((k, v) -> System.out.print(k + " "));
结果是
3 5 1
LinkedHashMap是不是发现了什么!没错,LinkedHashMap按照put的顺序输出(注意,不是排序)。
第一个问题,LinkedHashMap是如何记录输入的顺序的呢?
我们跟着代码进去一步步的看,首先还是先进入put方法,因为不管怎么样,你肯定是在put方法内部进行的吧?
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else{
……
}
……
是不是看newNode很可疑(强行解释一波),我们点进去看看
/* ------------------------------------------------------------ */
// LinkedHashMap support
/*
* The following package-protected methods are designed to be
* overridden by LinkedHashMap, but not by any other subclass.
* Nearly all other internal methods are also package-protected
* but are declared final, so can be used by LinkedHashMap, view
* classes, and HashSet.
*/
// Create a regular (non-tree) node
Node newNode(int hash, K key, V value, Node next) {
return new Node<>(hash, key, value, next);
}
// For conversion from TreeNodes to plain nodes
Node replacementNode(Node p, Node next) {
return new Node<>(p.hash, p.key, p.value, next);
}
// Create a tree bin node
TreeNode newTreeNode(int hash, K key, V value, Node next) {
return new TreeNode<>(hash, key, value, next);
}
// For treeifyBin
TreeNode replacementTreeNode(Node p, Node next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
/**
* Reset to initial default state. Called by clone and readObject.
*/
void reinitialize() {
table = null;
entrySet = null;
keySet = null;
values = null;
modCount = 0;
threshold = 0;
size = 0;
}
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node p) { }
不点进去不知道,一点进去吓一跳啊,看最上面的注释,这不就是说这些方法都是特地为LinkedHashMap而准备的吗?
老规矩,我们只看newNode,其它的先不管,这时候要去看LinkedHashMap对newNode的重写
Node newNode(int hash, K key, V value, Node e) {
LinkedHashMap.Entry p =
new LinkedHashMap.Entry(hash, key, value, e);
linkNodeLast(p);
return p;
}
嗯? LinkedHashMap.Entry是个什么东东,进去看看
static class Entry extends HashMap.Node {
Entry before, after;
Entry(int hash, K key, V value, Node next) {
super(hash, key, value, next);
}
}
原来是Node的子类,加了两个指针,类似于双链表的next和pre。
OK,接下来看linkNodeLast这个方法是干嘛的,不用看猜也能知道是将节点放到双链表的尾部。
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry p) {
LinkedHashMap.Entry last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
双链表的基本操作,不解释
最后newNode这个方法就完事了,有一个点要注意,不然到下面讲删除的时候转不过弯来,那就是newNode返回的是LinkedHashMap.Entry这个子类,所以hashmap中的node才会有before和after这两个指针。
接着,你会看到讲hashmap的时候没有讲到的那两个方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
……
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//!!!!here!!!!!
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
//!!!!here!!!!!
afterNodeInsertion(evict);
return null;
}
首先,我们看一下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;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
可以看到,只有当accessOrder为true的时候,这个方法才起作用,那accessOrder这个参数是干嘛用的呢?好的,我们在LinkedHashMap中搜索一下,看看都在哪里出现了。
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
……
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
……
public LinkedHashMap() {
super();
accessOrder = false;
}
……
public LinkedHashMap(Map extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
……
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
很明显的看到,只有在构造LinkedHashMap的时候,accessOrder才会被赋值,而且只有最后一个构造函数可以将它设置为true。
这个参数字面意思就是访问排序,也就是说按照访问的顺序进行排序。先暂时这么理解。关于这个参数以及使用场景,又可以扯一波,留到末尾讲。
OK,再来看afterNodeInsertion这个方法
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
和上面不一样的是,此方法一般由removeEldestEntry这个方法控制是否执行,所以我们看看这个方法是干嘛的?
protected boolean removeEldestEntry(Map.Entry eldest) {
return false;
}
这个方法其实是给你重写用的,返回true表示删除最不常访问的。
来个LRU缓存的简易版实现瞧瞧
public class LRUCache extends LinkedHashMap{
public static void main(String[] args) {
LRUCache lruCache = new LRUCache<>();
lruCache.put("A", 1);
lruCache.put("B", 2);
lruCache.put("C", 3);
//此时顺序是A->B->C
lruCache.get("B");
//此时顺序是A->B->C
lruCache.put("D", 4);
//此时的数据为B->C->D
}
public LRUCache(){
super(16, 0.75f, true);
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
//如果size大于3就删除最不常访问的key
return this.size() > 3;
}
}
什么是LRU缓存?
最近最少使用,也就是不常访问的删掉,一般常访问的移到末尾,那么当空间不足时,删除头部的数据即可。
OK,put方法相当于分析完了,那么接下来分析比较重要的remove方法
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
这个都是常规操作,接着往下看removeNode方法
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
……
++modCount;
--size;
//!!!!here!!!!
afterNodeRemoval(node);
return node;
}
}
return null;
}
前面的和hashmap的一毛一样,不再分析,主要看afterNodeRemoval这个方法做了啥?
首先我们的疑问是,LinkedHashMap是如何做到添加删除都做到O(1)复杂度的?
void afterNodeRemoval(Node e) { // unlink
//这里分别拿到了e的前后节点,那么删除e还不是很简单的事情?
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;
}
前面说过,LinkedHashMap的节点node其实就是LinkedHashMap.Entry,所以这里做一个向下强转,可以拿到before和after的信息,一个双链表想要删除中间某一个节点,那么必须要拿到它的前后节点。这里因为直接传来要删除的节点,而它又包含前后节点的信息,所以O(1)的实际复杂度就能够完成了。
添加删除分析完了,最后就是解释开头那个case的结果了,为什么顺序输出?
public void forEach(BiConsumer super K, ? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
for (LinkedHashMap.Entry e = head; e != null; e = e.after)
action.accept(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
这个就比较简单了,因为LinkedHashMap是直接遍历的双向链表,而不是hashmap。