前言: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日,晚。