类介绍(注释)
- add、contains、remove 方法,时间复杂度是O(1)。
- LinkedHashMap的遍历耗时,与_capacity无关,与map的size(元素多少)呈线性。_
- HashMap的遍历,可能比_LinkedHashMap更耗时,其和_capacity呈线性关系。
- LinkedHashMap是非线程安全的,并发出错时,会快速失败,抛出
ConcurrentModificationException
。可以使用_Collections.synchronizedMap(new LinkedHashMap(…));_ - LinkedHashMap非常适合于构建
LRU缓存
(least-recently Used)。 - 并不是所有的adds、delete操作都会造成LinkedHashMap结构的变更。
_insertion-ordered 模式下,_修改一个已经存在的key,对应的值,并不会造成结构的变更
access-ordered 模式下,get将造成结构的变更
LinkedHashMap的一些概念
// 头结点,同时也是最早插入的节点。
transient LinkedHashMap.Entry<K,V> head;
// 尾结点,同时也是最后插入的节点。
transient LinkedHashMap.Entry<K,V> tail;
// 继承 Node,为数组的每个元素增加了 before 和 after 属性。
// LinkedHashMap 的数据结构很像是把 LinkedList 的每个元素换成了 HashMap 的 Node,像是两者的结合体.
// 也正是因为增加了这些结构,从而能把 Map 的元素都串联起来,形成一个链表,而链表就可以保证顺序了,就可以维护元素插入进来的顺序。
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);
}
}
// 控制两种访问模式的字段,默认 false
// true 按照访问顺序,会把经常访问的 key 放到队尾
// false 按照插入顺序提供访问
final boolean accessOrder;
常用方法分析
LinkedHashMap() (无参构造方法)
LinkedHashMap 在无参构造方法中,将accessOrder设置为false,即按照插入顺序访问。它的_capacity、与HashMap的默认的一致,为16、0.75。
public LinkedHashMap() {
super();
accessOrder = false;
}
LinkedHashMap按顺序插入节点(put)
首先,put是直接调用HashMap#put
方法。从LinkedHashMap 的重要概念中,可以看到,存在一个继承了HashMap.Node的Entry。
当前put时,即会创建LinkedHashMap#Entry对象。并且,LinkedHashMap中重写了HashMap#put中调用的newNode
、afterNodeInsertion
、afterNodeAccess
方法。
put的具体流程,可以参照HashMap源码。这里来看LinkedHashMap重写的newNode
方法。
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;
}
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
// 备份尾节点
LinkedHashMap.Entry<K,V> last = tail;
// 将新插入的节点,作为尾节点
tail = p;
// 如果原先尾节点为null(原先链表为空,现在有一个元素)
if (last == null)
// 将p也作为头结点(只有一个元素的链表,head=tail=唯一的元素)
head = p;
else {
// 链表有数据,则建立 前后 指向
p.before = last;
last.after = p;
}
}
LinkedHashMap 通过新尾节点,给每个节点增加 before、after 属性,每次新增时,都把节点追加到尾节点等手段,在新增的时候,就已经维护了按照插入顺序的链表结构了。
遍历
LinkedHashMap可以通过以下,实现的Map接口中的方法,进行遍历。
当然,也可以通过迭代器来进行遍历。如map.entrySet().iterator();
得到迭代器,再通过hasNext方法判断是否有下一个节点后,最后通过next来访问下一个节点。
其中的实现都依赖LinkedHashIterator。
final class LinkedEntryIterator extends LinkedHashIterator implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() {
return nextNode();
}
}
// 下个节点
LinkedHashMap.Entry<K,V> next;
// 当前节点
LinkedHashMap.Entry<K,V> current;
// 期望的结构版本号
int expectedModCount;
// 构造方法
LinkedHashIterator() {
// 初始化时,头结点即为 next
next = head;
expectedModCount = modCount;
current = null;
}
// 是否还有下个节点
public final boolean hasNext() {
return next != null;
}
// 获取下个节点
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
// 判断结构版本号 是否有变更。有变更的话抛出错误
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 没有下个节点
if (e == null)
throw new NoSuchElementException();
// 有下个节点,通过e.after,取得下个节点,并赋值给next
current = e;
next = e.after;
// 返回当前节点
return e;
}
LRU 最近最少使用
使用示例
这里,是采用了LinkedHashMap的三个参数的构造方法,其源码如下。其中最主要的,是指定了accessOrder
为true
(默认为false)。 true 按照访问顺序,会把经常访问的 key 放到队尾,get时会造成结构的变更; false 按照插入顺序提供访问。
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
LRU大概的意思就是经常访问的元素会被追加到队尾,这样不经常访问的数据自然就靠近队头,然后我们可以通过设置删除策略,比如当 Map 元素个数大于3时。
public static void main(String[] args) {
// 初始化时,主要指定了accessOrder为true
LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>(16,0.75f,true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return this.size() > 3;
}
};
// 放入元素
map.put("1",1);
map.put("2",2);
map.put("3",3);
map.put("4",4);
// 输出,并测试LRU策略
System.out.println(map);
map.get("2");
System.out.println(map);
map.get("3");
System.out.println(map);
map.get("4");
System.out.println(map);
}
// {2=2, 3=3, 4=4}
// {3=3, 4=4, 2=2}
// {4=4, 2=2, 3=3}
// {2=2, 3=3, 4=4}
可以看到:我们往map中放置四个元素,但输出结果只有三个元素。1 不见了,这个是因为在每次put时,会调用afterNodeInsertion
方法。该方法在允许删除节点时,并在removeEldestEntry
方法返回true的情况下,(我们在用例中重写了removeEldestEntry
方法)会移除头节点。源码如下:
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);
}
}
当我们调用get方法后,发现输出的元素顺序发生改变。被访问元素移动到链表尾部,这个体现了最经常被访问的节点会被移动到链表尾部。在调用get后,头节点即为最早插入-最少被访问的节点。
get方法源码
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
// accessOrder为true时,才会去将访问的元素,放到链表尾部
if (accessOrder)
afterNodeAccess(e);
return e.value;
}