LinkedHashMap是什么?
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
LinkedHashMap是HashMap的扩展,它根据元素的插入顺序或者访问顺序(accessOrderd属性指定),使用双向链表,将所有元素连接起来,使得对HashMap的遍历变得有序。
示意图如下:
(图片引用自:https://blog.csdn.net/justloveyou_/article/details/71713781)
为什么要设计这个类?
这个实现是为了解决HashMap和HashTable无序问题,而又不增加像TreeMap那样对树操作的高额成本。
LinkedHashMap与LRU有什么关系?
LRU是一种缓存淘汰算法,LRU(Least recently used,最远使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。如果对LRU不清楚的同学可以参考这篇文章:https://blog.csdn.net/zhoucheng05_13/article/details/79829601
下面是一段LinkedHashMap源码的注释文档:
<p>A special {@link #LinkedHashMap(int,float,boolean) constructor} is
* provided to create a linked hash map whose order of iteration is the order
* in which its entries were last accessed, from least-recently accessed to
* most-recently (<i>access-order</i>). This kind of map is well-suited to
* building LRU caches.
上面源码注释的意思是,LinkedHashMap的一个特殊的构造器LinkedHashMap(int,float,boolean)被用来创建一个从最远到最近被访问的访问顺序排序的LinkedHashMap。这样的map非常适合用于实现LRU缓存。
LinkedHashMap是怎么实现LRU的呢?
1. 通过构造器指定accessOrder为true
上面提到的构造器源码如下:
/**
* Constructs an empty {@code LinkedHashMap} instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - {@code true} for
* access-order, {@code false} for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
重点在于它设置了accessOrder属性,关于该属性的定义如下:
/**
* 这个属性定义了迭代器的遍历顺序,若为true,则使用访问顺序(LRU),若为false,则使用
* 插入顺序。该属性是final类型的常量,只能赋值一次。
* 该属性默认情况下为false。
* The iteration ordering method for this linked hash map: {@code true}
* for access-order, {@code false} for insertion-order.
*
* @serial
*/
final boolean accessOrder;
2. 在元素被访问后将其移动到链表的末尾
在元素被访问时,如果accessOrder为true,即该map通过访问顺序排序,那么被访问的元素会被移动到链表的末尾,以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;
}
可以看到,在指定的key存在的情况下,会判断accessOrder的属性,如果为true,会调用afterNodeAccess(e)方法,该方法源码如下:
//当Node被访问后,将其移动到链表的最后
void afterNodeAccess(Node<K,V> e) { // move node to last
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;
}
}
哪些操作算访问操作?
这个在源码官方文档中同样有详细的说明:
Invoking the {@code put}, {@code putIfAbsent},
* {@code get}, {@code getOrDefault}, {@code compute}, {@code computeIfAbsent},
* {@code computeIfPresent}, or {@code merge} methods results
* in an access to the corresponding entry (assuming it exists after the
* invocation completes). The {@code replace} methods only result in an access
* of the entry if the value is replaced. The {@code putAll} method generates one
* entry access for each mapping in the specified map, in the order that
* key-value mappings are provided by the specified map's entry set iterator.
* <i>No other methods generate entry accesses.</i> In particular, operations
* on collection-views do <i>not</i> affect the order of iteration of the
* backing map.
上面的文档可以分为几个点:
- 调用put、putIfAbsent、get、getOrDefault、compute、computeIfAbsent、computeIfPresent、merge方法会引起对相应Entry的访问。
- replace方法只有在value被替换时才算访问操作
- putAll方法对传入map中的每一个映射都产生了一次访问(根据map的Iterator顺序)。
除此以外,没有任何方法会产生对entry的访问。尤其需要注意的是!在集合视图中的操作不会影响背后Map的迭代顺序。
上面的规则都可以从源码中找打答案,以replace()方法为例(该方法在HashMap中):
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e); //注意这里!
return oldValue;
}
return null;
}
从上面我们可以看到,只有当key存在,并对value进行了替换之后,才会调用afterNodeAccess(e); 方法,产生顺序的调整。
所以,归根结底,只有在源码中调用了afterNodeAccess(e);方法,才会调整节点顺序,即算作对entry的访问操作。
LinkedHashMap对LRU还提供了那些支持?
LinkedHashMap对LRU策略青睐有加,它专门为其设计了一个方法:
/* @param eldest The least recently inserted entry in the map, or if
* this is an access-ordered map, the least recently accessed
* entry. This is the entry that will be removed it this
* method returns {@code true}. If the map was empty prior
* to the {@code put} or {@code putAll} invocation resulting
* in this invocation, this will be the entry that was just
* inserted; in other words, if the map contains a single
* entry, the eldest entry is also the newest.
* @return {@code true} if the eldest entry should be removed
* from the map; {@code false} if it should be retained.
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
该方法的说明文档长达20余行,为了节省篇幅,这里就不贴出来了,下面我会对其一一说明。
首先,这个方法的作用是决定当有新元素插入时,是否要移除Eldest的元素。它的调用时机是在新元素被插入到map中后,被put和putAll方法调用。文档中这样说道:“它为实现者提供了一个机会,可以在每次添加新条目时删除最老的条目。这在使用map做缓存时非常有用:它允许map通过删除最旧的元素来减少内存消耗。”下面是官方文档中举的一个简单例子:
private static final int MAX_ENTRIES = 100;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
这个例子的作用是,当map增长到100时,每次插入新的元素就删除一个最老的元素,使得容量始终保持在100.
这个方法没有直接修改map,而是通过返回值决定是否允许map修改自身。要在这个方法中直接修改map也是允许的,不过如果要这么做,那么必须返回false,以防止map被重复修改。
在默认的实现中,这个方法仅仅返回false,所以这个map表现的像一个普通的map,最老的元素永远不会被移除。如果需要实现特定的功能,我们需要向下面这样重写该方法:
LinkedHashMap map = new LinkedHashMap(){
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size()>100;
}
};
总结
常听说Map可以用于实现缓存,当阅读了这个类过后才有了直观的感受。从源码中的方法可以看出,该类的确是为缓存量身定制的。