引言
前面已对java.util.HashMap
的源码做了详细的分析。HashMap
底层采用哈希表,保证了put和get操作的高效。但也导致了遍历操作是无序的。java.util.LinkedHashMap
则是在HashMap
的基础上,维护了一条双向链表,记录了节点的插入顺序。使遍历操作可以按插入顺序进行。本文通过源码解析的方式,对LinkedHashMap
的有序性进行说明。
数据结构
LinkedHashMap
继承自HashMap
,在保持数组+链表+红黑树的结构下,还维护着一条双向链表,可以记录节点插入顺序。
LinkedHashMap
数据结构的最小单元是java.util.LinkedHashMap.Entry
,LinkedHashMap.Entry
在HashMap.Node
的基础上,增加了指向前后节点的引用before和after。LinkedHashMap.Entry
源码如下:
1 | static class Entry<K,V> extends HashMap.Node<K,V> { |
成员变量
指向双向链表头节点
1 | transient LinkedHashMap.Entry<K,V> head; |
指向双向链表尾节点
1 | transient LinkedHashMap.Entry<K,V> tail; |
是否改变访问顺序。开启这个,每次更新或查询节点都会将该节点移至双向链表末尾
1 | final boolean accessOrder; |
构造方法
所有LinkedHashMap
构造方法内部都显示调用了父类HashMap
构造方法。
构造一
默认accessOrder=false
1 | public LinkedHashMap() { |
构造二
指定数组初始容量,默认accessOrder=false
1 | public LinkedHashMap(int initialCapacity) { |
构造三
指定数组初始容量和加载参数,默认accessOrder=false
1 | public LinkedHashMap(int initialCapacity, float loadFactor) { |
构造四
指定数组初始容量、加载参数和accessOrder
1 | public LinkedHashMap(int initialCapacity, |
构造五
通过Map
构建LinkedHashMap
,默认accessOrder=false
1 | public LinkedHashMap(Map<? extends K, ? extends V> m) { |
成员方法
LinkedHashMap
大部分还是沿用了HashMap
的方法逻辑,通过对部分方法的改造,巧妙加入了链表的思想。
newNode方法
在HashMap#putVal
方法中,通过newNode
方法创建节点。
LinkedHashMap
重写了newNode
方法,使创建的节点类型为LinkedHashMap.Entry
1 | Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { |
newNode
内部调用了linkNodeLast
方法。
linkNodeLast方法
给head,tail,before,after引用赋值,形成双向链表。通过这种方式,使添加的节点有了顺序
1 | private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { |
afterNodeAccess方法
在putVal
方法中,如果是更新value的情况,会调用一次afterNodeAccess
方法。
LinkedHashMap
实现了afterNodeAccess
方法,作用是:
每次更新节点后,都会将该节点移动到双向链表的末尾。
源码如下:
1 | void afterNodeAccess(Node<K,V> e) { |
注意:
accessOrder默认为false,需要手动开启。
总结起来,该方法处理了四种情况:
- e为尾节点,则不需要任何操作
- e为头节点,则b为null,需要将a设为头节点,e设为尾结点
- e为中间节点,则需要连接b和a,e设为尾结点
- 链表未初始化,则b,a都为null,e既是头节点也是尾结点
afterNodeInsertion方法
在putVal
方法中,插入新的节点后,调用了afterNodeInsertion
方法
LinkedHashMap
实现了该方法,作用是:
在插入新的节点后,移除双向链表的头节点。
源码如下:
1 | void afterNodeInsertion(boolean evict) { |
removeEldestEntry方法
注意removeEldestEntry
方法默认返回false,需要重新实现。
1 | protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { |
示例
重写removeEldestEntry
方法,以满足:使map中节点数不超过3,超过自动移除
1 | public static void main(String[] args) { |
结果:
{1=1}
{1=1, 2=2}
{1=1, 2=2, 3=3}
{2=2, 3=3, 4=4}
{3=3, 4=4, 5=5}
再配合afterNodeAccess
方法,将更新或查询的节点移动到链表末尾,就可以实现按使用频率淘汰旧数据的逻辑
afterNodeRemoval方法
在HashMap#removeNode
方法中,删除节点后,会调用一次afterNodeRemoval
方法。
LinkedHashMap
实现了该方法,作用是:
在删除节点后,调整双向链表相关的引用。具体表现为:
首先移除e的前后引用
如果e是中间节点,则直接连接a、b节点
- 如果e是头节点,则使head指向a节点
- 如果e是尾节点,则使after指向b节点
1 | void afterNodeRemoval(Node<K,V> e) { |
get方法
get
方法与 HashMap#get
方法基本相同,区别是如果accessOrder=true,会调用afterNodeAccess
将该节点移动到双向链表末尾。
1 | public V get(Object key) { |
keySet方法
LinkedHashMap
重写了keySet
方法,使遍历按节点插入顺序进行。源码如下:
keySet
方法返回LinkedHashMap.LinkedKeySet
对象
1 | // 初次调用,会创建一个LinkedKeySet对象 |
LinkedKeySet类
iterator
方法返回一个LinkedHashMap.LinkedKeyIterator
迭代器对象
1 | final class LinkedKeySet extends AbstractSet<K> { |
LinkedKeyIterator类
next
方法内部调用了LinkedHashMap.LinkedHashIterator#nextNode
方法
1 | final class LinkedKeyIterator extends LinkedHashIterator implements Iterator<K> { |
LinkedHashIterator类
LinkedHashIterator
初始化时会使next指向head节点,每调用一次nextNode
方法,会使next在双向链表上移动一次。
可以看到,与HashMap.HashIterator#nextNode
相比,LinkedHashMap.LinkedHashIterator#nextNode
的指针是在双向链表上移动,既保留了数据插入的顺序,也提升了遍历效率。
1 | abstract class LinkedHashIterator { |
values
和entrySet
方法与keySet
类似,这里不再展开