大部分翻译自:LinkedHashMap
在这篇文章中,我们将会一探LinkedHashMap的内部工作原理。
LinkedHashMap 也是一个HashMap,但它使用额外的数据结构——双向链表,定义了遍历顺序。默认情况下,遍历顺序等同于插入顺序。它也可以是entries最近被访问的顺序,所以它也能很容易被用来当做LRU Cache。在oscache或者是ehcache都使用到了LinkedHashMap。(oscache中是否使用LRU是可以配置的)
LinkedHashMap的数据结构拓展自HashMap
HashMap是基于数组和链表的数据结构。一个entry基于它的hash值确定它在数组的下标位置。如果数组下标位置已经存在元素了,HashMap使用链表头插入法(jdk1.7下),新的entry会插入链表的头部,并将next引用指向旧的entry。
在HashMap中,无法控制迭代顺序(英文术语:iteration order)。
在LinkedHashMap中,迭代顺序是被定义的,要么是插入顺序,要么是访问顺序。
LinkedHashMap区别于HashMap的最大特点是,所有的entry构成了一个双向链表结构。
下图是LinkedHashMap的数据结构的示例,它基于插入顺序定义迭代顺序。为了实现这一点,每个entry元素都会跟踪它的前一个元素和后一个元素。如果LinkedHashmap的大小为0,那么它只包含一个head元素,head元素的before引用和after引用都会指向head本身。
LinkedHashMap的entry拓展了HashMap的Entry,所以它也继承了同样的属性:key、value、hash、next Entry。除此之外,它有一对额外的属性取维护这个双向链表结构,after entry 和 before entry 。
LinkedHashMap继承了Hashmap,所以它的内部数据结构是几乎与HashMap相同的。不同的是,它维护了一个双向链表,而这个双向链表循环链接的实现,是通过head元素这个哨兵节点实现的。每个节点都保持了前一个节点和后一个节点的引用。一个新的节点总是会添加到这个双向链表的末端(不管是基于插入顺序还是访问顺序,都是如此)。为了实现末端插入新节点,末端节点和head节点之间的链接必须要做调整。
1、新节点的after引用指向head节点;
2、新节点的before引用指向当前的末端节点;
3、当前末端节点的after引用指向新节点;
4、head节点的before引用指向新节点;
after = head;
before = head.before;
before.after = this;
after.before = this;
ps:双向链表使用head这个哨兵节点的意义:上面实现末端插入,只需要知道head节点的引用就可以了,通过head节点可以求出:
当前末端节点:head.before
当前末端节点的after引用:head.before.after
head节点的before引用:head.before
在LinkedHashMap的内部静态类Entry中,addBefore()方法对应了把新的节点添加到双向链表的末端的实现。源码如下:
private static class Entry extends HashMap.Entry {
// These fields comprise the doubly linked list used for iteration.
Entry before, after;
Entry(int hash, K key, V value, HashMap.Entry next) {
super(hash, key, value, next);
}
/**
* Removes this entry from the linked list.
*/
private void remove() {
before.after = after;
after.before = before;
}
/**
* Inserts this entry before the specified existing entry in the list.
*/
private void addBefore(Entry existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
/**
* This method is invoked by the superclass whenever the value
* of a pre-existing entry is read by Map.get or modified by Map.set.
* If the enclosing Map is access-ordered, it moves the entry
* to the end of the list; otherwise, it does nothing.
*/
void recordAccess(HashMap m) {
LinkedHashMap lm = (LinkedHashMap)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
....................
}
LinkedHashMap没有对父类HashMap的put(key,vlaue) 方法进行任何直接的修改,它只对put()方法中的addEntry方法和Entry的recordAccess方法进行了重写。基于这一点,可以把它看做模板模式的实现。
* This override alters behavior of superclass put method. It causes newly
* allocated entry to get inserted at the end of the linked list and
* removes the eldest entry if appropriate.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
createEntry(hash, key, value, bucketIndex); //addEntry底层核心是调用createEntry()方法
// Remove eldest entry if instructed, else grow capacity if appropriate
Entry eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
if (size >= threshold)
resize(2 * table.length);
}
}
/**
* This override differs from addEntry in that it doesn't resize the
* table or remove the eldest entry.
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry old = table[bucketIndex];
Entry e = new Entry(hash, key, value, old);//旧的bucketIndex的entry作为参数传给新的entry的next引用
table[bucketIndex] = e;
e.addBefore(header); //createEntry()方法调用addBefore将新节点添加到双向链表的末端
size++;
}
LinkedHashMap(capacity, loadFactor, accessOrderBoolean)构造函数可以用来创建通过访问顺序进行迭代的hashmap,从最近最少访问到最近最多访问。调用put或者get方法就会形成对相应entry的一次访问,在判断LinkedHashMap是通过访问顺序进行迭代后,它会把这个被访问的entry移到双向链表的末端,否则它什么也不做。
public void testLinkedHashMap() {
LinkedHashMap lru = new LinkedHashMap(16, 0.75f, true);
lru.put("one", null);
lru.put("two", null);
lru.put("three", null);
Iterator itr = lru.keySet().iterator();
while (itr.hasNext()) {
System.out.println(itr.next());
}
System.out.println("** Access one, will move it to end **");
lru.get("one");
itr = lru.keySet().iterator();
while (itr.hasNext()) {
System.out.println(itr.next());
}
System.out.println("** Access two, will move it to end **");
lru.put("two", "two");
itr = lru.keySet().iterator();
while (itr.hasNext()) {
System.out.println(itr.next());
}
}
Result:
one
two
three
** Access one, will move it to end **
two
three
one
** Access two, will move it to end **
three
one
two
通过上面示例可以看出,在这个基于访问顺序的LinkedHashMap中,调用get()方法查询map就会引起其内部结构的修改。
备注: In access-ordered linked hash maps, merely querying the map with get is a structural modification. 是官网API中重点划出的句子。但从上面的示例可以看出,调用put()方法存在相同的key覆盖旧的value,基于访问顺序下也会引起结构的修改和迭代顺序的改变。
按照访问的次序来排序的含义:当调用LinkedHashMap的get(key)或者put(key, value)时,如果在map中包含这个key,那么LinkedHashMap会调用Entry的的recordAccess(HashMap)方法,将key对象的entry放在双向链表的末端。
ps:也就是说,当使用put()方法,存在相同的key覆盖旧的value后,也会把该key对应的entry放到双向链表的末端。而如果不存在这个key,按照原先的约定,就是将新的entry放到双向链表的末端。
正是因为LinkedHashMap提供按照访问的次序来排序的功能,所以它才需要改写HashMap的get(key)方法(HashMap不需要排序)和HashMap.Entry的recordAccess(HashMap)方法public Object get(Object key) {
Entry e = (Entry)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
void recordAccess(HashMap m) {
LinkedHashMap lm = (LinkedHashMap)m;
if (lm.accessOrder) {
lm.modCount++;
remove(); //在双向链表上删除该节点
addBefore(lm.header); //将该节点添加到双向链表上head节点的前面,即放在双向链表的末端
}
}
至于put(key, value)方法, LinkedHashMap不需要去改写,用HashMap的就可以了,因为HashMap在其put(key, value)方法里边已经预留了e.recordAccess(this); HashMap的put()源码如下:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
在HashMap中,迭代器必须遍历每个table元素和元素上的链表,遍历的时间复杂度与HashMap的capacity成正比。
在LinkedHashMap中,迭代器只需要遍历双向链表,因此时间复杂度只与map的大小有关,与capacity无关。所以HashMap迭代的开销是更大的。
LinkedHashMap的resize扩容操作是更快的,因为它只需要遍历双向链表,就可以把内容transfer到新的table array上。
void transfer(HashMap.Entry[] newTable) {
int newCapacity = newTable.length;
for (Entry e = header.after; e != header; e = e.after) {
int index = indexFor(e.hash, newCapacity);
e.next = newTable[index];
newTable[index]= e;
}
}
在HashMap的transfer操作,则需要两层循环,先遍历table,在遍历每个table元素上的链表。
containsValue()方法被重写,从而充分利用迭代器更快的性能。
如果基于访问顺序,迭代器的迭代顺序是基于entry被最近访问的顺序,也就是从最近最少访问到最近最多访问的访问顺序。这个类型的map非常适用于构建LRU Cache。removeEldestEntry(Entry)可以被重写,从而实现当一个新的entry添加到map中时自动删除过时的entry的策略。例如:
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > maxCacheSize;
}
最旧的entry可以通过header.after返回,而默认的removeEldestEntry()的实现是返回false。
======================================
参考:oracle官网 LinkedHashMap API
彻头彻尾理解 LinkedHashMap
LinkedHashMap特性 按插入和访问顺序排序