LinkedHashMap是前面分析过的HashMap的子类,主要的作用就是在HashMap的基础上可以保证元素的插入顺序或访问顺序,内存访问算法中很经典的LRU算法就可以基于LinkedHashMap实现,在面试中也很常见。
LinkedHashMap继承自HashMap,关于map的操作基本一样,只是加入了维护访问顺序加入了一些新的特性。
- 重写HashMap.Node,加入了before和after两个属性,用于表示双向链表中的前后结点。
- 关键方法如put()、get()、remove等基本与父类操作相同,只是有少量的改动或者直接调用父类方法。
- 三个关键方法:afterNodeInsertion()、afterNodeAccess()、afterNodeRemoval()在执行完可能会影响插入或访问顺序的操作后调用这三个方法,来维持顺序。
一、基本构造函数
1 | public class LinkedHashMap<K,V> |
1、加入双向链表
可以看出,LinkedHashMap对结点加入了新的属性,before和after,用以表示形成双向链表。
同时加入了head和tail结点,表示双向链表的头结点和尾结点,通过链表头结点到尾结点的顺序来表示map中元素的插入顺序或访问顺序。
2、accessOrder
accessOrder是LinkedHashMap中一个关键的属性。
上面说到LinkedHashMap能够保存元素的插入顺序或者是访问顺序,就是通过accessOrder这一变量来决定的。
accessOrder默认为false,即保存插入顺序。
accessOrder为true时,保存访问顺序,其实访问顺序只是在插入顺序的基础上,在每次访问元素时,对链表顺序进行修改。
3、构造函数
从构造函数可以看出,LinkedHashMap的构造函数都是调用父类的,默认将accessOrder设置为false。
最后一个构造函数可以自己指定accessOrder。
下面来看一下map最常用的几个方法是如何实现的。
二、put()方法
首先还是put方法,看过源码才知道原来LinkedHashMap并没有重写父类的put方法,所以还是要再回到前面分析的HashMap的put方法中。
1 | public V put(K key, V value) { |
以上代码是直接从上篇分析中粘下来的,去掉了所有的注释,又新加入了两个注释,这两个也是唯一与LinkedHashMap相关的地方。
1、newNode
LinkedHashMap与HashMap在map的存储方式上实现过程是一样的,区别在于LinkedHashMap需要通过双向链表来保存map的插入顺序,所以,区别之一就是新建的结点肯定不能是父类中原有的结点,所以LinkedHashMap重写了父类的newNode方法。
1 | Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { |
可以看出,在新建一个node之后,又调用了linkNodeLast方法,目的在于把这个新建的结点加入到双向链表的尾结点上,这样就保证了插入的顺序。
2、afterNodeInsertion()方法
上面的put方法中第二个注释的地方,调用了afterNodeInsertion方法,
在父类HashMap中,afterNodeInsertion和其他两个方法并没有具体实现
1 | // Callbacks to allow LinkedHashMap post-actions |
注释写的很明白,是为了LinkedHashMap准备的。方法名也很清楚,就是插入一个结点之后要做的事。下面是LinkedHashMap中重写的方法:
1 | void afterNodeInsertion(boolean evict) { // possibly remove eldest |
正常来说,在插入结点的时候通过链表保存插入顺序,这就足够了,那么为什么还要调用这个方法呢,插入之后还有什么需要做的呢?
注释里面写了,这个方法可能的作用就是删除掉最老的元素,也就是离当前访问最远的元素。没错,这也是实现LRU时要重写的一个方法。下面的removeEldestEntry给出了判断的条件,重写时可以自由指定,比如当size大于指定的值后,就返回true。
1 | public boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { |
返回true后,就会删除掉head结点,也就是链表中的第一个结点。
三、get方法
1、重写get方法
与put方法不一样的是LinkedHashMap直接重写了get方法,原因在于在get的过程中需要判断accessOrder,这是子类的属性
1 | public V get(Object key) { |
看一下父类的get方法
1 | public V get(Object key) { |
子类中并没有重写getNode方法,所以两个get方法唯一的区别就在于,子类在getNode之后会判断accessOrder,以判断是否调用afterNodeAccess方法。
也就是说,get方法正常来说是不会影响map的结构的也不会影响插入的顺序,但是这个操作会影响访问的顺序,所以如果accessOrder为true,那么在调用get方法之后,访问顺序就发生了变化,就需要调用afterNodeAccess方法来改变访问顺序。
2、afterNodeAccess()方法
1 | void afterNodeAccess(Node<K,V> e) { // move node to last |
afterNodeAccess做的很简单,当访问元素e之后,它就成了最新访问的,所以需要把它移动到链表的尾部。
四、remove方法
1、直接调用父类remove方法
remove方法与put方法基本类似,子类都没有重写,再来看一下父类中的实现:
1 | public V remove(Object key) { |
这也是从上篇分析中粘下来的,由于不用新建结点,所以remove方法只是在后面调用了afterNodeRemoval方法,这是唯一的区别
2、afterNodeRemoval()方法
1 | void afterNodeRemoval(Node<K,V> e) { // unlink |
afterNodeRemoval方法也很简单,就是更新删除的结点e的前后结点的指针,以保证链表的结构。
五、总结
了解HashMap之后再来看LinkedHashMap就会快很多。
两个类是父子关系,所以大部分实现方法都是相同的。
子类是父类的扩展,扩展的地方就是保证插入或访问的顺序,而扩展的方式就是通过加入一个双向链表来维持。
子类主要重写了几个可能会影响到插入或访问顺序的方法,比如put,get,remove
重写的方法主要就是加入了对链表的维护
- put方法,新建结点时指定前后结点,超过了某范围后可以进行删除
- get方法在访问元素时更新链表结构来更新访问顺序
- remove方法也是在删除结点后更新链表的结构