LinkedHashMap源码分析

前言:LinkedHashMap继承HashMap,所以它是线程不安全的,但是它有序,下面就让我们来对其内部原理进行分析。

注:本文jdk源码版本为jdk1.8.0_172


1.LinkedHashMap介绍

LinkedHashMap底层数据结构为双向链表,能保证元素按照插入顺序访问,也能以访问顺序访问,可以用来实现LRU策略缓存。

1 public class LinkedHashMap<K,V>
2     extends HashMap<K,V>
3     implements Map<K,V>

可以把LinkedHashMap看成LinkedList+HashMap。由于继承HashMap所以其默认容量为16,扩容因子为0.75,非同步,允许[key,value]为null。

2.具体源码分析

先看LinkedHashMap的重要属性

 1 // Entry继承HashMap的Node
 2 static class Entry<K,V> extends HashMap.Node<K,V> {
 3     Entry<K,V> before, after;
 4     Entry(int hash, K key, V value, Node<K,V> next) {
 5         super(hash, key, value, next);
 6     }
 7 }
 8 /**
 9  * The head (eldest) of the doubly linked list.
10  */
11 // 旧数据放在head节点 
12 transient LinkedHashMap.Entry<K,V> head;
13 
14 /**
15  * The tail (youngest) of the doubly linked list.
16  */
17 // 新数据放在tail节点
18 transient LinkedHashMap.Entry<K,V> tail;
19 
20 /**
21  * The iteration ordering method for this linked hash map: <tt>true</tt>
22  * for access-order, <tt>false</tt> for insertion-order.
23  *
24  * @serial
25  */
26 // false-按插入顺序存储数据 true-按访问顺序存储数据
27 final boolean accessOrder;

分析:

从源码上可知以下几点:

#1.LinkedHashMap的底层数据结构继承至HashMap的Node,并且其内部存储了前驱和后继节点。

#2.LinkedHashMap通过accessOrder来控制元素的相关顺序,false-按插入顺序存储数据,true-按访问顺序存储数据,默认为false。

构造函数:

 1 public LinkedHashMap() {
 2     super();
 3     accessOrder = false;
 4 }
 5 
 6   public LinkedHashMap(int initialCapacity) {
 7     super(initialCapacity);
 8     accessOrder = false;
 9 }
10 
11    public LinkedHashMap(int initialCapacity, float loadFactor) {
12     super(initialCapacity, loadFactor);
13     accessOrder = false;
14 }
15 
16 public LinkedHashMap(int initialCapacity,
17                      float loadFactor,
18                      boolean accessOrder) {
19     super(initialCapacity, loadFactor);
20     this.accessOrder = accessOrder;
21 }
22 
23     public LinkedHashMap(Map<? extends K, ? extends V> m) {
24     super();
25     accessOrder = false;
26     putMapEntries(m, false);
27 }

分析:

从构造函数可以,accessOrder默认为false,当然也可自定义。

LinkedHashMap的实现比较精妙,很多方法都是通过HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能,而不需要重写诸如put方法,因此在LinkedHashMap的源码中并未发现put方法,这里分析其实现的钩子方法。

afterNodeAccess(Node<K,V> e),主要在执行put方法并且已存在元素时进行调用,如果accessOrder为true,会把访问到的元素移动到双向链表的末尾。

 1 void afterNodeAccess(Node<K,V> e) { // move node to last
 2         LinkedHashMap.Entry<K,V> last;
 3         // 如果accessOrder为true,并且访问的节点不是尾节点
 4         if (accessOrder && (last = tail) != e) {
 5             // 取出前驱和后继节点
 6             LinkedHashMap.Entry<K,V> p =
 7                 (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
 8             p.after = null;
 9             // p的前向节点为空,则将a赋值给头节点
10             if (b == null)
11                 head = a;
12             else
13                 // 这里其实就是把p节点从链表中移除
14                 b.after = a;
15             // 构建双向链表
16             if (a != null)
17                 a.before = b;
18             else
19                 last = b;
20            // 把p节点放到双向链表尾
21             if (last == null)
22                 head = p;
23             else {
24                 p.before = last;
25                 last.after = p;
26             }
27             // 尾节点为p
28             tail = p;
29             // 修改次数自增
30             ++modCount;
31             // 对于双向链表,画图可以很好理解
32         }
33     }

分析:

该函数会在调用put方法出现覆盖key操作时调用,该方法主要作用就是将访问的节点移动到双向链表的末尾,但是有附加条件accessOrder必须为true,否则该操作失效。

afterNodeInsertion(boolean evict):该方法会在插入节点后被调用。

 1 void afterNodeInsertion(boolean evict) { // possibly remove eldest
 2     LinkedHashMap.Entry<K,V> first;
 3     // evict 驱逐的意思
 4     // 如果evict为true,其头节点不为空,其确定移除最老元素
 5     // removeEldestEntry默认返回为false,也就是不删除元素
 6     if (evict && (first = head) != null && removeEldestEntry(first)) {
 7         K key = first.key;
 8         removeNode(hash(key), key, null, false, true);
 9     }
10 }
11 
12 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
13     return false;
14 }

分析:

该函数的主要作用就是判断是否需要移除最老的元素,但是需要我们重写removeEldestEntry方法才能实现,因为该方法默认返回false,即不删除。

afterNodeRemoval(Node<K,V> e):该方法会在节点被remove后调用。

 1 void afterNodeRemoval(Node<K,V> e) { // unlink
 2     // 取出其前驱和后继节点
 3     LinkedHashMap.Entry<K,V> p =
 4         (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
 5     // 把节点从双向链表中删除
 6     p.before = p.after = null;
 7     if (b == null)
 8         head = a;
 9     else
10         b.after = a;
11     if (a == null)
12         tail = b;
13     else
14         a.before = b;
15 }

分析:

该函数为典型的将双向链表的节点从链表中remove掉,逻辑还是比较简单的,理解应该不难。

get方法:

 1 public V get(Object key) {
 2     Node<K,V> e;
 3     // 通过getNode方法取出节点,如果为null则直接返回null
 4     if ((e = getNode(hash(key), key)) == null)
 5         return null;
 6     // 如果accessOrder为true,则需要把节点移动到链表末尾
 7     if (accessOrder)
 8         afterNodeAccess(e);
 9     return e.value;
10 }

分析:

通过getNode方法获取元素,该方法在HashMap中(后续会对jdk1.8的HashMap做具体分析),从这里也可以看出LinkedHashMap设计的精妙之处。

afterNodeAccess方法前面已经分析过了,将节点移动至双向链表的尾部。

LinkedHashMap如何实现按元素插入顺序遍历元素的,具体原理如下:

 1 // newNode函数会在put操作时被调用,LinkedHashMap重写了HashMap的newNode方法
 2 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
 3     // 创建节点
 4     LinkedHashMap.Entry<K,V> p =
 5         new LinkedHashMap.Entry<K,V>(hash, key, value, e);
 6     // 将新节点加入链表尾
 7     linkNodeLast(p);
 8     return p;
 9 }
10 
11    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
12     // 取出链表尾
13     LinkedHashMap.Entry<K,V> last = tail;
14     // 更新链表尾部元素
15     tail = p;
16     // 构建双向链表
17     if (last == null)
18         head = p;
19     else {
20         p.before = last;
21         last.after = p;
22     }
23 }

分析:

由于LinkedHashMap重写了newNode方法,在创建新的节点的时候,就会将节点挂在现有节点的尾部,从而实现按插入顺序访问元素。

而按照访问顺序遍历元素是基于LRU算法(最近最少使用)进行遍历。

3.总结

LinkedHashMap继承HashMap并实现了HashMap中预留的钩子函数,因此不必重写HashMap的很多方法,设计非常巧妙。

#1.LinkedHashMap默认容量为16,扩容因子默认为0.75,非同步,允许[key,value]为null。

#2.LinkedHashMap底层数据结构为双向链表,可以看成是LinkedList+HashMap。

#3.如果accessOrder为false,则可以按插入元素的顺序遍历元素,如果accessOrder为true,则可以按访问顺序遍历元素。


by Shawn Chen,2019.09.14日,晚。

posted @ 2019-09-14 22:25  developer_chan  阅读(633)  评论(0编辑  收藏  举报