HashMap做为一种经典的数据结构,其根据key定位元素能达到平均O(1)的时间复杂度。 可是,存储于HashMap中的元素显然是无序的,遍历HashMap的顺序得看脸。。。
那如何使得HashMap里的元素变得有序呢?一种思路是,将存放HashMap元素的节点,使用指针将他们串起来。换言之,就像在HashMap里面“嵌入”了一个链表同样。
实际上,jdk的LinkedHashMap就是使用这种思路实现的。java
<!-- more -->node
LinkedHashMap中的代码不算多,这是由于,jdk的设计使用继承复用了代码,在jdk的设计中,LinkedHashMap是HashMap的扩展:算法
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> { /* ... */ }
回想一下HashMap的实现方式中,将key和value打包成的节点有两种:
第一种,传统分离链表法的链表节点。数据结构
static class Node<K,V> implements Map.Entry<K,V> { /* ... */ }
第二种,HashMap为进行优化,必定状况下会将链表重构为红黑树。第二种节点是红黑树节点:函数
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { /* ... */ }
忽然发现,HashMap的TreeNode是继承至LinkedHashMap的Entry的。。。
我的观点是jdk这种作法不是很优雅,自己LinkedHashMap继承HashMap就使得二者之间的逻辑混在了一块儿,而这里的内部实现又反过来继承,逻辑搞得很混乱。优化
LinkedListHashMap须要将节点串成一个“嵌入式”双向链表,所以须要给这两种节点增长两个字段:spa
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
扩展HashMap.Node,增长双向链表字段。
因为TreeNode是继承自LinkedListMap.Entry的,因此它也有这两个字段。设计
再来看下LinkedHashMap中的属性,不多:指针
/** * The head (eldest) of the doubly linked list. */ transient LinkedHashMap.Entry<K,V> head; /** * The tail (youngest) of the doubly linked list. */ transient LinkedHashMap.Entry<K,V> tail;
记录双向链表的表头和表尾。从注释中能够看出,head节点是最老的,tail节点是最新的,也即链表按照由老到新的顺序串起来。code
最后,因为LinkedHashMap是能够设置它组织元素的顺序。一种是链表中的元素是按插入时候的顺序排序,另一种是按照访问的顺序排序。
// 指定顺序是按照访问顺序来,仍是插入顺序来 final boolean accessOrder;
这个accessOrder
指定是否按插入顺序来。
因为对Map中的节点进行了扩展,所以,在建立节点时不能使用原来的节点了,而应该使用从新建立后的。
HashMap将建立节点的操做抽取出来放到了单独的函数中,LinkedHashMap重写便可:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; } Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) { LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p; LinkedHashMap.Entry<K,V> t = new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next); transferLinks(q, t); return t; } // HashMap的TreeNode是继承自LinkedHashMap.Entry的,所以可以参与组织双向链表 TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) { TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next); linkNodeLast(p); return p; } TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p; TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next); transferLinks(q, t); return t; }
接下来,则须要在LinkedHashMap的操做时维护双向链表。
回顾下HashMap的源代码,咱们知道,HashMap在删除节点后,会调用afterNodeRemoval
函数。
这个函数在HashMap中是空的,实际上jdk是将它设计为一个hook,果真,在LinkedHashMap中,就重写了该函数,在其中维护双向链表:
// 当有节点被删除(即有元素被移除),那么也要将它从双向链表中移除 void afterNodeRemoval(Node<K,V> e) { // unlink LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.before = p.after = null; if (b == null) head = a; else b.after = a; if (a == null) tail = b; else a.before = b; }
按照相似的思路,HashMap中在插入元素后会调用afterNodeInsertion
,那是否是LinkedHashMap也在这里实现了相关逻辑,插入元素后维护双向链表节点呢?
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } }
然而,实际上在LinkedHashMap中该函数彷佛没有什么用。由于:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
removeEldestEntry
始终返回false,afterNodeInsertion至关于什么也没作。这个逻辑设计目的是什么,还不能很清楚。也许也是为了让谁去继承?之后再探究。
那插入元素后的在哪里维护了双向链表呢?回到以前的newNode
和newTreeNode
:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; } TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) { TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next); linkNodeLast(p); return p; } // 尾插双向链表节点 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null) head = p; else { p.before = last; last.after = p; } }
因为HashMap中调用newNode时候都是为了装新插入的元素,因此在这里维护双向链表。
感受耦合是否是太紧了。。。若是HashMap因为某个操做须要临时搞个newNode借用下,岂不是会出问题?
下面是replacementNode
和replacementTreeNode
。replacementNode
在HashMap中的做用是,该K V以前是被TreeNode包装的,如今须要拿Node包装它。这也势必会影响双向链表的结构,因此这里也须要额外维护下。
获取的时候,一样,是重写了`afterNodeAccess`钩子,这样在HashMap的获取逻辑结束后,这里的逻辑会被执行,维护双向链表。 void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } }
LinkedHashMap中的顺序有访问序和插入序,只有访问序才须要在访问的时候更新双向链表结构。也即accessOrder
为true才会执行这段逻辑。
最后,注意到:
++modCount; }
通常来讲,只有修改了Map结构的操做,才须要修改modCount以让正在迭代的迭代器感知到了变化。
可是这里,因为迭代器是使用这里的“嵌入式”双向链表进行迭代,而在这里会改变双向链表的结构,若迭代器继续迭代会形成不可预测的结果。
因此,这里须要改变modCount
,阻止迭代器继续迭代。
LinkedHashMap的一个典型应用场景是LRU算法。
因为如今夜已深,如今不敢熬夜身体吃不消,想睡觉了。因此这个坑之后再填
LinkedHashMap还有其它的一些实现细节,如:
clear
的时候也要同时维护双向链表;最后,总结下jdk中对LinkedHashMap中的实现思路: