最近事情有点多,今天抽出时间来看看LinkedHashMap的源码,其实一开始是想分析TreeMap来这,但是看了看源代码之后,决定还是等过几天再分析,原因是TreeMap涉及到了树的操作。。而之前没有接触过树的这种数据结构,只是在学校学一点皮毛而已。。所以我还是打算过几天先恶补一下相关的知识再来对TreeMap做分析。
言归正传,我们今天来看LinkedHashMap。从名字上我们可以看出来,这个对插入的值是保持顺序的,即我们插入的顺序就是我们输出的顺序,如果不相信,我们可以用HashMap和LinkedHashMap,按相同的顺序插入相同的值,最后看输出的结果,就可以知道他们的区别了。
我们首先来看LinkedHashMap的继承结构
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
我们可以看到,LinkedHashMap是直接继承了HashMap的,所以在一定程度上来说,他们两个是一样的。只不过LinkedHashMap重写了HashMap的一些方法。从而达到了输出有顺序的目的。
看我之前的一篇博文http://blog.csdn.net/zw0283/article/details/51177547
大家应该对HashMap有一个大致的认识。而LinkedHashMap与HashMap在主要逻辑实现上并无差异,最大的不同,就是LinkedHashMap比HashMap多维护了一个链表,这个多出来的链表,就是存放我们插入顺序信息的。
这里我们在看一下LinkedHashMap的内部Entry实例的结构
有了上边的结构图,对下边源码的理解也更容易一些,好,我们开始分析LinkedHashMap的部分源码
构造方法分析
我们先来看构造方法
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; }
5个构造方法,也是够多的。。不过我们看到,大部分都是调用父类的构造方法,也就是HashMap的构造方法,这里我就不在赘述了,大家可以参考我上一篇博文。
我们还看到,在构造方法里多了一个boolean变量accessOrder,这是什么鬼?
看源码中的注释我们可以知道,这个变量是控制输出的顺序的,一共有两种顺序:
1、按插入顺序输出,类似于队列,先进去的先出来
2、按LRU顺序,何为LRU,就是最近最少使用,打个比方,我们插入值A、B、C、D,如果这样插入的话,那输出的时候就是A、B、C、D,看起来好像跟第一种没什么分别。那我们在测试,插入A、B、C、D、A,我们在输出的时候,发现输出变成了B、C、D、A。A为什么跑到后边去了?这是因为,A被插入了2次,而LRU是最近最少使用,所以A的使用频率要高于BCD,要将使用频率高的放到后边,使用频率小的放到前边。
还有一个不得不提的问题就是,在HashMap中,我们看到有一个空实现的init方法,这个方法在HashMap中没什么用,它的作用是留给子类覆盖的,也就是说,在LinkedhashMap构造方法中,调用super的构造方法后,还会调用自身的重写后的init方法,体现了Java的多态性。
我们来看看被重写后的init方法
void init() { header = new Entry<>(-1, null, null, null); header.before = header.after = header; }
大致就是构建了一个头结点,然后将改节点的前继和后继都指向自己,构成了一个单节点的双向链表。
我们再来看看Entry,即map的内部数据结构
private static class Entry<K,V> extends HashMap.Entry<K,V>{ // 增加了前继和后继,构成双向链表,按插入顺序存储 Entry<K,V> before, after; }
根据上边的Entry结构图来看,应该更容易理解一些。
我们在前边已经知道了LinkedHashMap多了一个链表来保证输出顺序,我们就来看看LinkedHashMap是怎么实现的。
在看代码之前,我们先猜测一下这个LinkedHashMap的数据结构,有了数据结构图之后,相信理解代码也会十分容易。既然继承了HashMap,那整体结构肯定和HashMap一样了,只是细节上有变动,我们大胆猜测一下
上边的图是我参照网上的答案,并结合自己的想法画出来的,网上都是把上边这个图拆成了2个,画是好画了,但是很容易产生误导(反正对我来说有误导)我完全看不懂拆成2个之后的对应关系,索性我就直接自己用Visio画了一个,画的不好看,大家见谅。。
很容易看出来,细的蓝色箭头,是原来HashMap里本身就有的,而粗的箭头,则是LinkedHashMap新增了,每个Entry实例旁边的数字代表的是插入的顺序。我们可以想象一下,如果我们用左手捏住head节点,右手捏住任意节点,用力拽开,会发现这些Entry实例会变成一个环状结构(注意:其实在第6个Entry实例和Head节点处还有一个双向箭头,这里为了不引起混淆就没有画,但实际上是有的)就像这样。(大家注意,我下边这个其实是双向箭头,为了方便我就弄成了单向的。。)
配合上边几个图,相信大家应该对LinkedHashMap的结构有一个了解了,那我们现在注意来分析一下被LinkedHashMap重写的几个方法。
recordAccess方法分析
我们通过源码可以知道,LinkedHashMap并没有直接重写put方法,而是重写了put方法里调用的一些方法,笨一点的方法就是,在HashMap里put调用的每一个方法,都去LinkedhashMap里看一看有没有重写。。。(话说我就是这么找的。。)
// LinkedHashMap重写的方法,HashMap里为空实现 void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; // 判断一下要用哪种顺序,LRU还是正常的顺序,LRU要做特别操作,而正常顺序留不需要操作了 if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } }
我们跟进去看看remove方法
private void remove() { before.after = after; after.before = before; }
这个不难理解,假设现在有ABC,B为当前对象,则第一句为将A的后继指向为B的后继,即C。第二句将B的后继C的前继设为B的前继,即A。所以最后变成了AC,当前对象B就被删除掉了。
为什么要删除呢?我们在来看看addBefore方法
private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; }
结合上边的图我们在来看这个代码,recordAccess调用addBefore传入的参数是当前Map的head节点,所以从这个方法名我们可以看出它要做的是将当前节点(this)加入到head之前,但是,别忘了,LinkedHashMap内部维护的是循环双向链表,所以加入到head之前,意思就是加到链表的结尾。(不知道有没有表述明白,反正我看的时候在这绕了好半天。。)
我用一张图为大家说明一下,在展示图之前,各位要先明白一些问题。就是这个recordAccess是在什么地方调用的?我们看HashMap的put方法可知,是在有重复key的时候才调用的。
/****************HashMap**************/ // 遍历该位置的链表,如果有重复的key,则将value覆盖 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; } }
所以,现在很清楚了,当有重复的key时,相当于“使用频率”增加了,若使用普通的顺序,则不需要做什么,若使用LRU算法的话,就需要把使用频率高的放到后边,自然,使用频率小的就到前边了。所以才会有上边的recordAccess方法。
我们来看看图就明白了
transfer方法分析
transfer方法是当Entry数组需要扩容时调用的。我们来看源码中transfer方法的注释:
/** * Transfers all entries to new table array. This method is called * by superclass resize. It is overridden for performance, as it is * faster to iterate using our linked list. */ @Override void transfer(HashMap.Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e = header.after; e != header; e = e.after) { if (rehash) e.hash = (e.key == null) ? 0 : hash(e.key); int index = indexFor(e.hash, newCapacity); e.next = newTable[index]; newTable[index] = e; } }
重写这个方法的原因主要是为了优化,因为LinkedHashMap内部有一个链表,做查询的时候,相对于HashMap的遍历方式,重写后的遍历链表在效率上要高于原来的处理。不过做的事情都是一样的。将原来的数据转存到一个新的数组里。只不过遍历的方式不一样而已。
get方法分析
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; }