Java LinkedHashMap

维护操作顺序的哈希表

LinkedHashMap是HashMap的扩展增强,保存了操作顺序(插入顺序/访问顺序)

LinkedHashMap.java
1
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

实现上包含头尾链表引用
头指向最老的数据,尾指向最年轻的数据

LinkedHashMap.java
1
2
3
4
5
6
//eldest
transient LinkedHashMap.Entry<K,V> head;
//youngest
transient LinkedHashMap.Entry<K,V> tail;

final boolean accessOrder;

链表节点结构装饰器模式扩展了HashMap的节点结构,增加两个指针用来维护双端队列

LinkedHashMap.java
1
2
3
4
5
6
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实现中预留了三个空方法,分别在操作后调用,LinkedHashMap覆写了三个方法,加入扩展逻辑

HashMap.java
1
2
3
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

插入顺序


默认采取插入顺序

LinkedHashMap.java
1
2
3
4
public LinkedHashMap() {
super();
accessOrder = false;
}

插入逻辑并没有覆写afterNodeInsertion方法,而是覆写了新建节点方法
LinkedHashMap没有单独维护链表结构,而是把HashMap所有节点都增加前后指针直接串起来,链表数据一体

LinkedHashMap.java
1
2
3
4
5
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;
}

访问顺序


一个有趣的开关是遍历顺序的控制,false时是按插入顺序,true时是按访问顺序。通常使用的都是插入顺序,要想变成访问顺序需要在构造时配置。

LinkedHashMap.java
1
2
3
4
public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

访问后把Entry移到链表尾部

LinkedHashMap.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
//采取访问顺序才操作
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)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;
}
}

局限性

注意: 如果采用了访问顺序,那么get操作也会改变内部结构,因此这种模式不能进行循环遍历

Sample
1
2
3
4
5
6
7
LinkedHashMap<String, Integer> map = new LinkedHashMap<String, Integer>(10, 0.75f, true);
map.put("foo", 1);
map.put("bar", 2);
map.put("baz", 3);
for (String s : map.keySet()) {
map.get(s); //java.util.ConcurrentModificationException
}

LRU实现

LinkedHashMap不是抽象类,但是它留了一个protected方法,面向用户的可自定义配置方法,这个方法在put或者putAll之后会被调用,也就是说数据插入后,有机会进行后处理。配合访问顺序,LinkedHashMap可以当成是天然的LRU缓存实现

LinkedHashMap.java
1
2
3
4
5
6
7
8
9
10
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
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);
}
}

注意点:泛型,负载因子,非继承实现

Sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class LruCache<K, V> {
private Map<K, V> cache;

public LruCache(final int capacity) {
this.cache = new LinkedHashMap<K, V>(capacity, 1, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return this.size() > capacity;
}
};
}

public V get(K key) {
return cache.get(key);
}
public void set(K key, V value) {
cache.put(key, value);
}
public Set<K> keySet() {
return cache.keySet();
}
public static void main(String[] args) {
LruCache<String, Integer> cache = new LruCache<String, Integer>(3);
cache.set("foo", 1);
cache.set("bar", 2); // not use
cache.set("baz", 3);
cache.get("foo");
cache.get("baz");
cache.set("quz", 4);
System.out.println(cache.keySet()); // [foo, baz, quz]
}
}