一、LinkedHashMap的内部数据结构
其实分析一个数据结构主要是分析清楚数据之间的关系,理清出了数据的存储、删除、读取等关系,数据的结构也就清楚了。LinkedHashMap内部也有两个很重要的成员变量:
/**
* The head of the doubly linked list.
*/
private transient LinkedHashMapEntry<K,V> header;
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
private final boolean accessOrder;
其中accessOrder
如果为true
时,表示使用访问排序;为false
时表示使用插入排序,而默认是为false
的。如果要实现访问排序就需要用到LinkedHashMap的这个构造函数:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
在构造的过程中accessOrder
这个参数传入true
即可。再来看看header
这个成员变量的数据类型的代码:
private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
// These fields comprise the doubly linked list used for iteration.
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<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(LinkedHashMapEntry<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();
}
}
可以看出LinkedHashMapEntry
是HashMapEntry
的子类,但是它而外添加了before
和after
两个成员变量用于指向当前节点的前一个节点和后一个节点,形成双链链表。到现在依然还没有理清楚LinkedHashMap
内部数据存储的关系,下面再一步一步来分析,先来看看init()
这个方法:
@Override
void init() {
header = new LinkedHashMapEntry<>(-1, null, null, null);
header.before = header.after = header;
}
init()
这个方法是在LinkedHashMap
父类的构造方法中调用的,这里重写了这个方法。执行完这个init()
方法后就有了这样一个header
节点:
这个节点的before
指针和after
指针都指向了自己,构成了一个简单的双链回环链表。既然要分析清楚数据间的关系,所以从添加元素的方法开始分析,看内部的数据间的关系是如何变化的,就能一目了然了。但是LinkedHashMapEntry
里面并没有put()
方法,那肯定就是使用的父类的,所以再来看看父类的这个put()
方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
for (HashMapEntry<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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
其实LinkedHashMap
是通过重写recordAccess
和addEntry
这两个方法来实现自己特定需求的,而重写recordAccess
这个方法主要是用来实现访问排序的,所以这里就暂时不分析,来看看addEntry
的代码:
void addEntry(int hash, K key, V value, int bucketIndex) {
// Remove eldest entry if instructed
LinkedHashMapEntry<K,V> eldest = header.after;
if (eldest != header) {
boolean removeEldest;
size++;
try {
removeEldest = removeEldestEntry(eldest);
} finally {
size--;
}
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
super.addEntry(hash, key, value, bucketIndex);
}
这个方法中前面干的那些事都是为了删除最老的元素,后面又调用了父类的addEntry
方法,所以又得再来看看父类的addEntry
方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
这个方法的前面部分把该扩容的事情做了后,调用了 createEntry
方法,但是在这个方法里面调用的createEntry
这方法又是被子类LinkedHashMap
重写了的,所以主要实现又来到了子类的createEntry
这个方法:
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> old = table[bucketIndex];
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
来来回回的子类父类相互调用是不是有点晕了呢?现在就通过画图一步一步来分析这里面的每句代码,假设是第一次调用put
方法而且bucketIndex=2
,执行完HashMapEntry<K,V> old = table[bucketIndex];
这句代码后就可以得到一幅图:
此时old = NULL
,接着往下走,假设new LinkedHashMapEntry<>(hash, key, value, old)
的value
为E
,则执行完LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
和table[bucketIndex] = e;
这两行代码后的图为:
真正绕的逻辑来了,当然就是e.addBefore(header);
这句代码,再来看看addBefore
这个方法的代码:
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
现在来分析分析e.addBefore(header);
这行代码,调用这个方法的是e
这个节点,所以addBefore
这个方法中的after
和before
这个两个指针当然是e
节点中的成员变量。现在用H
表示传入的header
这个变量,当执行完 after = existingEntry;
这行代码后如图:
因为existingEntry
就是传进来的header
,而在前面我们画了一个header
的示意图,从header
的那个图中可以看出header.before
就是指向自己的,所以执行完before = existingEntry.before;
这行代码后就是下面这幅图的样子了:
再来理解哈 before.after = this;
这一行代码,因为是e
这个节点调用的addBefore
这个方法,所以before
就是e
这个节点中的变量。但是before
这个指针已经指向了header
,所以before.after
就是指header
中的after
这个指针,另外this
肯定就是指`e这个节点,当执行完这行代码后如图:
理解清楚了前一行代码,再来理解after.before = this;
这行代码就不是事了,所以执行完这行代码后如图:
分析到这里就已经将E
节点put
进来了,按照刚才分析的逻辑,假如现在再put
一个T
节点看看又是怎样一个 结构:
嗯,没错,就是这样一幅图,线条比较乱(3组线,6条线),但是对着代码来看还是很清晰的。另外需要说明的一点是,其实每个节点之间还有一个next
指针的,为了简化就没有在这些过程中画出来,自己心里能明白这一点就好。 看到这里相信对LinkedHashMap
的内部数据结构也有了一个比较清晰的认识了,其是就是在HashMap
的基础上让节点之间实现了一个双向回环链表!
二、LinkedHashMap在LruCache中的使用
前面通过详细的分析LinkedHashMap
的put
方法,也对LinkedHashMap
有了更深刻的认识,相信再来分析get
和remove
方法就不在话下了。现在来看看它的排序算法,LinkedHashMap
中实现了两种排序算法,分别是插入排序和访问排序。插入排序很好理解,就是按照存放元素的顺序来排序;而访问排序就是将最近访问的元素移到最前面,而最不常访问的就自然排到了最后面,当想要删除最不常访问的元素时直接干掉最后面的就OK了,所以要实现LRU(Least Recently Used )算法很容易了。Android
中经常用到的LruCache
算法就是基于LinkedHashMap
实现的,LruCache
的构造函数代码如下:
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
从这这个构造函数可以看出在实例化LinkedHashMap
的时候,第三个参数传的true
,表示要用到LinkedHashMap
的访问排序算法,在LruCache
中也没有特别需要分析的方法了,我觉得只要会用LruCache
并且知道它是用了LinkedHashMap
来实现的LRU
算法即可,因为明白了LinkedHashMap
也就明白了LruCache
实现的核心。
现在来总结下LinkedHashMap
的几个重要的特点:
①LinkedHashMap
也支持key
和value
为null
的情况;
②LinkedHashMap
是在HashMap
的基础上实现了双链回环链表的一种数据结构;
③LinkedHashMap
存放的元素是可以支持有序的; ④LinkedHashMap
内有两种排序算法,一种是基于插入排序;另一种是基于访问排序。
我觉得最需要记住的是第四个特点,在开发的过程中遇到需要类似排序算法的是否要能想到使用LinkedHashMap
,这也是分析LinkedHashMap
内部实现原理的重要原因。