LinkedHashMap详细解析
概论
- LinkedHashMap 通过特有底层双向链表的支持,使得LinkedHashMap可以保存元素之间的顺序,例如插入顺序或者访问顺序,而HashMap因为没有双向链表的支持,所以就不能保持这种顺序,所以它的访问就是随机的了
- 和HashMap一样,还是通过数组存储元素的
- 这里的顺序指的是遍历的顺序,定义了头结点head,当我们调用迭代器进行遍历时,通过head开始遍历,通过after属性可以不断找到下一个,直到tail尾结点,从而实现顺序性。在同一个hash(其实更准确的说是同一个下标,数组index ,在上图中表现了同一列)链表内部next和HashMap.Node.next 的效果是一样的。不同点在于before和after可以连接不同hash之间的链表,也就是说双向链表是可以跨任何index 连接的,也就是说将LinkedHashMap里面的所有元素按照特定的顺序连接起来的
LinkedHashMap 的最终形态
- 图中的黑色箭头表示next指针,这跟HashMap是一样的;
- 图中的红色箭头表示before和after指针,分别指向这个节点的前面和后面一个节点,这就可以实现有序访问;
初识LinkedHashMap
我们想在页面展示一周内的消费变化情况,用echarts面积图进行展示。如下:
我们在后台将数据构造完成
然而页面上一展示,发现并非如此,我们打印出来看,发现顺序并非我们所想,先put进去的先get出来
那么如何保证预期展示结果如我们所想呢,这个时候就需要用到LinkedHashMap实体,首先我们把上述代码用LinkedHashMap进行重构
这个时候,结果正如我们所预期
key: 星期一, value: 40
key: 星期二, value: 43
key: 星期三, value: 35
key: 星期四, value: 55
key: 星期五, value: 45
key: 星期六, value: 35
key: 星期日, value: 30
LinkedHashMap 的继承关系
继承关系图
LinkedHashMap继承了HashMap类,是HashMap的子类,LinkedHashMap的大多数方法的实现直接使用了父类HashMap的方法,LinkedHashMap可以说是HashMap和LinkedList的集合体,既使用了HashMap的数据结构,又借用了LinkedList双向链表的结构保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
构造方法
我们发现除了多了一个变量accessOrder之外,并无不同,此变量到底起了什么作用?
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
final boolean accessOrder;
通过注释发现该变量为true时access-order,即按访问顺序遍历,如果为false,则表示按插入顺序遍历。默认为false
LinkedHashMap 的关键内部构成
Entry
这个元素实际上是继承自HashMap.Node 静态内部类 ,我们知道HashMap.Node 实际上是一个单链表,因为它只有next 节点,但是这里LinkedHashMap.Entry保留了HashMap的数据结构,同时有before, after 两个节点,一个前驱节点一个后继节点,从而实现了双向链表
accessOrder
通过注释发现该变量为true时access-order,即按访问顺序遍历,此时你任何一次的操作,包括put、get操作,都会改变map中已有的存储顺序
如果为false,则表示按插入顺序遍历。默认为false 也就是按照插入顺序
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
final boolean accessOrder;
head tail
为了方便操作,加上有是双向链表所有这里定义了两个特殊的节点,头结点和尾部节点
/**
* 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;
newNode方法
LinkedHashMap重写了newNode()方法,通过此方法保证了插入的顺序性,在此之前我们先看一下HashMap 的newNode()方法
// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
然后我们再看一下LinkedHashMap的newNode()方法
这里调用了一个方法 linkNodeLast(),我们看一下这个方法,但是这和方法不止完成了串联后置,也完成了串联前置,所以插入的顺序性是通过这个方法保证的。
afterNodeAccess 方法
这里将HashMap 中的实现也帖了出来
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
关于afterNodeAccess()方法,在HashMap中没给具体实现,而在LinkedHashMap重写了,目的是保证操作过的Node节点永远在最后,从而保证读取的顺序性,在调用put方法和get方法时都会用到
前面说到的newNode()方法中调用的 linkNodeLast(Entry e)方法和现在的afterNodeAccess(Node e)都是将传入的Node节点放到最后,那么它们的使用场景如何呢?
在上一节讲HashMap的put流程,如果在对应的hash位置上还没有元素,那么直接new Node()放到数组table中,这个时候对应到LinkedHashMap中,调用了newNode()方法,就会用到linkNodeLast(),将新node放到最后,
如果对应的hash位置上有元素,进行元素值的覆盖时,就会调用afterNodeAccess(),将原本可能不是最后的node节点移动到了最后,下面给出了删减之后的代码逻辑
下面给出一个例子
运行结果如下
1
2
3
5
afterNodeInsertion 方法
关于afterNodeAccess()方法,在HashMap中依然没给具体实现,可以参考afterNodeAccess 中贴出的源代码
LinkedHashMap中还重写了afterNodeInsertion(boolean evict)方法,它的目的是移除链表中最老的节点对象,也就是当前在头部的节点对象,但实际上在JDK8中不会执行,因为removeEldestEntry方法始终返回false
removeEldestEntry 方法的源代码如下
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
afterNodeInsertion 方法的意义是什么呢
因为removeEldestEntry 固定返回false , 那这个方法的意义是什么呢 ?·
afterNodeInsertion方法的evict参数如果为false,表示哈希表处于创建模式。只有在使用Map集合作为构造器参数创建LinkedHashMap或HashMap时才会为false,使用其他构造器创建的LinkedHashMap,之后再调用put方法,该参数均为true。
下面给出了单独put 的情况
这里是使用Map集合作为构造器参数创建的时的情况
哈哈,其实到这里还是没有说这个方法的意义是什么,这里我们回过头来想一下,看一下HashMap 中有类似的操作吗,其实有的,就是这三个方法
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
很明显这三个方法,都在LinkedHashMap 中被重写了,所以下面的方法是因为是有返回值的,所以它不在是空方法体了,而是一个直接返回false 的方法体了
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
所以这里的意义就是让你去实现,为什么这么做呢?LinkedHashMap 就是用在记录顺序的场景下,一个典型应用就是LRU,也就是主要用在缓存上面,因为此时的设计虽然保留了LRU的基本特性,但是整个链表的大小是没有限制的
大小没有限制的缓存,哈哈 你懂了,后面肯定就是无限的GC了,因为这里都是强引用
实现一个大小确定的LRU(LinkedHashMap)
如果一个链表只能维持10个元素,那么当插入了第11个元素时,以如下方式重写removeEldestEntry的话,那么将会删除最老的一个元素
程序的运行结果如下
即将执行删除
不二map的大小:10
默认map的大小:11
afterNodeRemoval 方法
这个方法和上面的方法一样,在HashMap 中都是没有实现的,但是在LinkedHashMap的实现如下,其实这个方法是在删除节点后调用的,可以思考一下为甚么
因为LinkedHashMap 的双向链表连接了LinkedHashMap中的所有元素,HashMap中的删除方法可以使没有考虑这些的,它只考虑了如何存红黑树、链表中删除节点,所以它是不维护双向链表的,所以这里才有了这个方法的实现
debug 源码 插入元素的过程
有了HashMap 那一节和前面的铺垫,接下来我们看一下完整的插入过程,下面将和HashMap 不一样的地方标注出来
其他细节请参考上一篇HashMap,因为 DRY(don't repeat yourself)
获取元素的过程
首先 LinkedHashMap 重写了get 方法,getNode 方法依然使用的实HashMap 的,当元素存在的时候,判断是否要根据accessOrder 将元素移动到双向链表的尾部
删除元素的过程
Hashmap 一节,我们也没有讲remove 方法,所以这里我们来看一下,细节的一些东西都写在注释里了,首先remove 方法是有返回值的,存在则返回key 对应的value 不存在则返回null
matchValue 判断删除的时候,是否要求节点的值是相同的,True 则表示找到key相同的节点之后,还需要判断值是相等的才可以使删除。下面就是判断条件,可以自己分析一下
(!matchValue || (v = node.value) == value || (value != null && value.equals(v)))
LinkedHashMap重写了其中的afterNodeRemoval(Node e),该方法在HashMap中没有具体实现,通过此方法在删除节点的时候调整了双链表的结构。
总结
LinkedHashMap 的有序性
LinkedHashMap 指的是遍历的时候的有序性,而有序性是通过双向链表实现的,真实的存储之间是没有顺序的,和Hashmap 一样
如何实现一个固定大小的LinkedHashMap
继承LinkedHashMap实现removeEldestEntry 方法,当插入成功后,判断是否要删除最老节点
四个维护双向链表的方法
afterNodeAccess(Node<K,V> p)
访问元素之后维护
afterNodeInsertion(boolean evict)
插入元素之后维护
afterNodeRemoval(Node<K,V> p)
删除元素之后维护
linkNodeLast
也是插入元素之后维护,但是只用于桶上的第一个节点,后面的节点都是用afterNodeAccess或者afterNodeInsertion
你觉得LinkedHashMap 还有什么可以改进的地方吗,欢迎讨论
和上一节一样这里我依然给出这个思考题,虽然我们的说法可能不对,可能我们永远也站不到源代码作者当年的高度,但是我们依然积极思考,大胆讨论
虽然java 源代码的山很高,如果你想跨越,至少你得有登山的勇气,这里我给出自己的一点点愚见,希望各位不吝指教
afterNodeAccess() 方里面有几处判断个人觉得是不需要的,具体可以看afterNodeAccess方法里我写的注释,欢迎大佬讨论,谢谢!
作者:柯广的网络日志
微信公众号:Java大数据与数据仓库