LinkedHashMap是HashMap的子类,通过维护一个双向链表,实现Map有序遍历元素的特性。
因此,对于LinkedHashMap来说,其基本特性如下:
基本特性 | 结论 |
---|---|
元素是否允许为null | key和value可以为null |
元素是否允许重复 | key重复会覆盖,value可以重复 |
是否有序 | 有 |
是否线程安全 | 否 |
源码分析
本文使用的是JDK 1.8.0_201的源码。
成员变量
LinkedHashMap是继承HashMap的成员变量,实现有序的特性,还维护了额外几个成员变量:
成员变量 | 作用 |
---|---|
transient LinkedHashMap.Entry<K,V> head; | 链表的头部 |
transient LinkedHashMap.Entry<K,V> tail; | 链表的尾部 |
final boolean accessOrder; | 遍历的模式,默认是false |
put操作
LinkedHashMap没有实现自己的put操作,继承自HashMap。问题在于,LinkedHashMap是怎么维护元素的插入顺序的呢?换句话说,LinkedHashMap的双向链表是在哪里维护的?答案是在HashMap put方法中调用的模板方法newNode()。HashMap中存在许多的模板方法,以方便LinkedHashMap去实现自己的特性。
对于LinkedHashMap来说,newNode()方法实现如下:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
// 创建一个Entry
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 维护双向链表
linkNodeLast(p);
return p;
}
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;
}
}
get操作
LinkedHashMap自己实现了get()方法,代码如下:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
// 与HashMap的区别在这里
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
上面的代码与HashMap的区别在于accessOrder条件的判断,如果accessOrder为true,程序就会去执行afterNodeAccess(e)方法。
LRU缓存实现
accessOrder变量,是LinkedHashMap中一个有趣的属性。我们先看源码文档注释:
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.
对于insertion-ordered模式来说,即accessOrder为false的情况,只有添加和删除操作,才会改变元素的顺序。而对于access-ordered模式来说,即accessOrder为true的情况,即使是get()操作,都会改变元素的顺序。
access-ordered模式,是实现LRU缓存(最近最少使用)的关键。所谓最近最少使用,就是说当缓存满了,优先淘汰那些最近最少被访问的数据。
LinkedHashMap的具体实现就是,每次访问时,都把访问的数据移到双向队列的尾部,那么队列头部的元素就是最少被访问的数据,每次要淘汰数据时,就删除队列头部的数据。
回过头再看前面get操作中的afterNodeAccess(e)方法,就会发现这个方法正是每次访问时,把访问数据移到队尾的关键。
总结
1. LinkedHashMap是有序的吗?它是如何保证有序的?
LinkedHashMap通过维护一个双向链表,从而保证元素的顺序。需要注意的是,LinkedHashMap保证的有序是指元素的插入顺序或者访问顺序,而不是指元素字典顺序。至于LinkedHashMap具体保证的是插入顺序还是访问顺序,通过初始化时accessOrder字段的值来确定。如果accessOrder=true,表示访问顺序,false表示插入顺序。
2. 如何使用LinkedHashMap实现一个LRU缓存?
LinkedHashMap有两种维护顺序的模式,insetion-ordered模式跟access-ordered模式,当成员变量accessOrder为true,即access-ordered模式时,每次访问元素时,就会将元素移到队列的尾部,这样头部的元素就成了最少被访问的元素。当需要淘汰数据时,头部的元素先被淘汰。这个特性非常适合用来实现LRU缓存。
至于何时淘汰数据,LinkedHashMap提供了一个模板方法removeEldestEntry()。具体做法如下:
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int size;
public LRUCache(int size) {
super(size, 0.75f, true);
this.size = size;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当缓存的元素个数大于设置的大小时,淘汰最老的那个元素
return this.size() > this.size;
}
}
3. 为什么LinkedHashMap的遍历性能会比HashMap好?
因为LinkedHashMap的遍历是通过双向链表实现的,与size的大小有关,即与map的实际元素个数有关。与此同时,HashMap的遍历,是需要遍历底层数组的,举个例子,一个只存放了一个元素的HashMap,其底层数组的大小至少是16,那么这个遍历有15次是多余的。由于负载因子的存在(不考虑负载因子大于0的情况),HashMap的遍历总是会存在多余的次数。