【源码】LinkedHashMap源码剖析

时间:2023-02-07 09:00:41

注:以下源码基于jdk1.7.0_11

【源码】LinkedHashMap源码剖析

之前的两篇文章通过源码分析了两种常见的Map集合,HashMap和Hashtable。本文将继续介绍另一种Map集合——LinkedHashMap。 顾名思义,LinkedHashMap除了是一个HashMap之外,还带有LinkedList的特点,也就是说能够保持遍历的顺序和插入的顺序一致,那么它是怎么做到的呢?下面我们开始分析。 首先看构造器。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>

LinkedHashMap直接继承自HashMap,所以拥有HashMap的大部分特性,比如支持null键和值,默认容量为16,装载因子为0.75,非线程安全等等。 但是LinkedHashMap还有很多个性的地方,下面来看成员变量:
 private transient Entry<K,V> header;//内部双向链表的头结点
/**
*代表这个链表的排序方式,true代表按照访问顺序,false代表按照插入顺序。
*/
private final boolean accessOrder;

LinkedHashMap比HashMap多了两个成员变量,其中header代表内部双向链表的头结点,后面我们就会发现,LinkedHashMap除了有个桶数组容纳所有Entry之外,还有一个双向链表保存所有Entry引用。遍历的时候,并不是去遍历桶数组,而是直接遍历双向链表,所以LinkedHashMap的遍历时间不受桶容量的限制,这是它和HashMap的重要区别之一。 而这个accessOrder代表的是是否按照访问顺序,true代表是,默认是插入顺序。所以我们可以将accessOrder置为true来实现LRU算法,这可以用来做缓存。 再看构造器:
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

构造器首先都会调用父类也就是HashMap的构造器来初始化桶数组,而accessOrder之后会被初始化,除了最后面的一个构造器允许指定accessOrder外,其他构造器都默认将accessOrder置为了false 读者可能很奇怪,不是还有个header么,这个双向链表为啥不在构造器中初始化呢?这得回到HashMap中查看hashMap的构造器了:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
... ...
init();
}

HashMap构造器最后一步调用了一个init方法,而这个init方法在HashMap中是个空实现,没有任何代码。这其实就是所谓的“钩子”,具体代码由子类实现,如果子类希望每次构造的时候都去做一些特定的初始化操作,可以选择复写init方法。 我们看到LinkedHashMap中确实复写了init:
 @Override
void init() {
header = new Entry<>(-1, null, null, null);//初始化双向链表
header.before = header.after = header;//不光是双向链表,还是循环链表
}

在init方法中,果然初始化了双向链表,而且我们还发现,这不光是个双向链表,还是个循环链表。
HashMap内部的Entry类并没有before和after指针,也就是说LinkedHashMap自己重写了一个Entry类
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;//前驱、后继指针
Entry(int hash, K key, V value, HashMap.Entry<K,V> 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<K,V> 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<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
这里的Entry选择继承父类的Entry类,也就是说LinkedHashMap中的Entry拥有三个指针,除了前驱后继指针外用于双向链表的连接外,还有一个next指针用于解决hash冲突(引用链)。 除此之外,Entry新增了几个方法,remove和addbefore用来操作双向链表不用多说。而recordAccess方法比较特殊,这个方法在HashMap中也是空实现,并在put方法中会调用此方法:
 public V put(K key, V value) {//HashMap的put方法
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> 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;
}
}
... ...
}

此外,在LinkedHashMap的get方法中,也会调用此方法:

 public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}

也就是说,只要涉及到访问结点,那么就会调用这个方法。观察该方法的逻辑:如果accessOrder为true,那么会调用addBefore方法将当前Entry放到双向链表的尾部,最终在我们遍历链表的时候就会发现最近最少使用的结点的都集中在链表头部(从近期访问最少到近期访问最多的顺序),这就是LRU。
LinkedHashMap并没有复写put方法,但是却复写了addEntry和createEntry方法,之前分析HashMap的时候我们就知道了,put方法会调用addEntry将键值对挂到桶的某个合适位置,而addEntry又会调用createEntry方法创建一个键值对对象。因而,LinkedHashMap其实是间接更改了put方法,想想也很容易理解,LinkedHashMap除了要向桶中添加键值对外,还需向链表中增加键值对,所以必须得修改put方法。
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after;//标记最少访问的对象
if (removeEldestEntry(eldest)) {//判断是否需要删除这个对象---->可由子类实现来提供缓存功能
removeEntryForKey(eldest.key);
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);//添加到链表尾部
size++;
}

createEntry方法会将键值对分别挂到桶数组和双向链表中。 比较有意思的是addEntry方法,它提供了一个可选的操作,我们可以通过继承LinkedHashMap并复写removeEldestEntry方法让该子类可以自动地删除最近最少访问的键值对——这可以用来做缓存!!
LinkedHashMap自定义了迭代器以及迭代规则,LinkedHashMap是通过内部的双向链表来完成迭代的,遍历时间与键值对总数成正比,而HashMap遍历时间与容量成正比,所以通常情况下,LinkedHashMap遍历性能是优于HashMap的,但是因为需要额外维护链表,所以折中来看,两者性能相差无几。
 private abstract class LinkedHashIterator<T> implements Iterator<T> {
Entry<K,V> nextEntry = header.after;//指向链表首部
Entry<K,V> lastReturned = null;
int expectedModCount = modCount;
public boolean hasNext() {
return nextEntry != header;
}
public void remove() {
if (lastReturned == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
LinkedHashMap.this.remove(lastReturned.key);
lastReturned = null;
expectedModCount = modCount;
}
Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (nextEntry == header)
throw new NoSuchElementException();
Entry<K,V> e = lastReturned = nextEntry;
nextEntry = e.after;
return e;
}
}
总结: 1.LinkedHashMap继承自HashMap,具有HashMap的大部分特性,比如支持null键和值,默认容量为16,装载因子为0.75,非线程安全等等; 2.LinkedHashMap通过设置accessOrder控制遍历顺序是按照插入顺序还是按照访问顺序。当accessOrder为true时,可以利用其完成LRU缓存的功能; 3.LinkedHashMap内部维护了一个双向循环链表,并且其迭代操作时通过链表完成的,而不是去遍历hash表。