JDK1.8源码(九)——java.util.LinkedHashMap 类
前面我们介绍了 Map 集合的一种典型实现 HashMap ,关于 HashMap 的特性,我们再来复习一遍:
①、基于JDK1.8的HashMap是由数组+链表+红黑树组成,相对于早期版本的 JDK HashMap 实现,新增了红黑树作为底层数据结构,在数据量较大且哈希碰撞较多时,能够极大的增加检索的效率。
②、允许 key 和 value 都为 null。key 重复会被覆盖,value 允许重复。
③、非线程安全
④、无序(遍历HashMap得到元素的顺序不是按照插入的顺序)
HashMap 集合可以说是最重要的集合之一,上篇博客介绍的 HashSet 集合就是继承 HashMap 来实现的。而本篇博客我们介绍 Map 集合的另一种实现——LinkedHashMap,其实也是继承 HashMap 集合来实现的,而且我们在介绍 HashMap 集合的 put 方法时,也指出了 put 方法中调用的部分方法在 HashMap 都是空实现,而在 LinkedHashMap 中进行了重写。所以想要彻底了解 LinkedHashMap 的实现原理,HashMap 的实现原理一定不能不懂。
1、LinkedHashMap 定义
LinkedHashMap 是基于 HashMap 实现的一种集合,具有 HashMap 集合上面所说的所有特点,除了 HashMap 无序的特点,LinkedHashMap 是有序的,因为 LinkedHashMap 在 HashMap 的基础上单独维护了一个具有所有数据的双向链表,该链表保证了元素迭代的顺序。
所以我们可以直接这样说:LinkedHashMap = HashMap + LinkedList。LinkedHashMap 就是在 HashMap 的基础上多维护了一个双向链表,用来保证元素迭代顺序。
更形象化的图形展示可以直接移到文章末尾。
public class LinkedHashMapextends HashMapimplements Map
2、字段属性
①、Entry
static class Entryextends HashMap.Node { Entry before, after; Entry(int hash, K key, V value, Node next) { super(hash, key, value, next); } }
LinkedHashMap 的每个元素都是一个 Entry,我们看到对于 Entry 继承自 HashMap 的 Node 结构,相对于 Node 结构,LinkedHashMap 多了 before 和 after 结构。
下面是Map类集合基本元素的实现演变。
LinkedHashMap 中 Entry 相对于 HashMap 多出的 before 和 after 便是用来维护 LinkedHashMap 插入 Entry 的先后顺序的。
②、其它属性
//用来指向双向链表的头节点transient LinkedHashMap.Entry head;//用来指向双向链表的尾节点transient LinkedHashMap.Entry tail;//用来指定LinkedHashMap的迭代顺序//true 表示按照访问顺序,会把访问过的元素放在链表后面,放置顺序是访问的顺序//false 表示按照插入顺序遍历final boolean accessOrder;
注意:这里有五个属性别搞混淆的,对于 Node next 属性,是用来维护整个集合中 Entry 的顺序。对于 Entry before,Entry after ,以及 Entry head,Entry tail,这四个属性都是用来维护保证集合顺序的链表,其中前两个before和after表示某个节点的上一个节点和下一个节点,这是一个双向链表。后两个属性 head 和 tail 分别表示这个链表的头节点和尾节点。
PS:关于双向链表的介绍,可以看这篇博客。
3、构造函数
①、无参构造
1 public LinkedHashMap() {2 super();3 accessOrder = false;4 }
调用无参的 HashMap 构造函数,具有默认初始容量(16)和加载因子(0.75)。并且设定了 accessOrder = false,表示默认按照插入顺序进行遍历。
②、指定初始容量
1 public LinkedHashMap(int initialCapacity) {2 super(initialCapacity);3 accessOrder = false;4 }
③、指定初始容量和加载因子
1 public LinkedHashMap(int initialCapacity, float loadFactor) {2 super(initialCapacity, loadFactor);3 accessOrder = false;4 }
④、指定初始容量和加载因子,以及迭代规则
1 public LinkedHashMap(int initialCapacity,2 float loadFactor,3 boolean accessOrder) {4 super(initialCapacity, loadFactor);5 this.accessOrder = accessOrder;6 }
⑤、构造包含指定集合中的元素
1 public LinkedHashMap(Map<? extends K, ? extends V> m) {2 super();3 accessOrder = false;4 putMapEntries(m, false);5 }
上面所有的构造函数默认 accessOrder = false,除了第四个构造函数能够指定 accessOrder 的值。
4、添加元素
LinkedHashMap 中是没有 put 方法的,直接调用父类 HashMap 的 put 方法。关于 HashMap 的put 方法,可以参看我对于 HashMap 的介绍。
我将方法介绍复制到下面:
1 //hash(key)就是上面讲的hash方法,对其进行了第一步和第二步处理 2 public V put(K key, V value) { 3 return putVal(hash(key), key, value, false, true); 4 } 5 /** 6 * 7 * @param hash 索引的位置 8 * @param key 键 9 * @param value 值10 * @param onlyIfAbsent true 表示不要更改现有值11 * @param evict false表示table处于创建模式12 * @return13 */14 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,15 boolean evict) {16 Node[] tab; Nodep; int n, i;17 //如果table为null或者长度为0,则进行初始化18 //resize()方法本来是用于扩容,由于初始化没有实际分配空间,这里用该方法进行空间分配,后面会详细讲解该方法19 if ((tab = table) == null || (n = tab.length) == 0)20 n = (tab = resize()).length;21 //注意:这里用到了前面讲解获得key的hash码的第三步,取模运算,下面的if-else分别是 tab[i] 为null和不为null22 if ((p = tab[i = (n - 1) & hash]) == null)23 tab[i] = newNode(hash, key, value, null);//tab[i] 为null,直接将新的key-value插入到计算的索引i位置24 else {//tab[i] 不为null,表示该位置已经有值了25 Node e; K k;26 if (p.hash == hash &&27 ((k = p.key) == key || (key != null && key.equals(k))))28 e = p;//节点key已经有值了,直接用新值覆盖29 //该链是红黑树30 else if (p instanceof TreeNode)31 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);32 //该链是链表33 else {34 for (int binCount = 0; ; ++binCount) {35 if ((e = p.next) == null) {36 p.next = newNode(hash, key, value, null);37 //链表长度大于8,转换成红黑树38 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st39 treeifyBin(tab, hash);40 break;41 }42 //key已经存在直接覆盖value43 if (e.hash == hash &&44 ((k = e.key) == key || (key != null && key.equals(k))))45 break;46 p = e;47 }48 }49 if (e != null) { // existing mapping for key50 V oldValue = e.value;51 if (!onlyIfAbsent || oldValue == null)52 e.value = value;53 afterNodeAccess(e);54 return oldValue;55 }56 }57 ++modCount;//用作修改和新增快速失败58 if (++size > threshold)//超过最大容量,进行扩容59 resize();60 afterNodeInsertion(evict);61 return null;62 }
View Code
这里主要介绍上面方法中,为了保证 LinkedHashMap 的迭代顺序,在添加元素时重写了的4个方法,分别是第23行、31行以及53、60行代码:
1 newNode(hash, key, value, null);2 putTreeVal(this, tab, hash, key, value)//newTreeNode(h, k, v, xpn)3 afterNodeAccess(e);4 afterNodeInsertion(evict);
①、对于 newNode(hash,key,value,null) 方法
HashMap.NodenewNode(int hash, K key, V value, HashMap.Node e) { LinkedHashMap.Entryp = new LinkedHashMap.Entry(hash, key, value, e); linkNodeLast(p); return p; } private void linkNodeLast(LinkedHashMap.Entry p) { //用临时变量last记录尾节点tail LinkedHashMap.Entrylast = tail; //将尾节点设为当前插入的节点p tail = p; //如果原先尾节点为null,表示当前链表为空 if (last == null) //头结点也为当前插入节点 head = p; else { //原始链表不为空,那么将当前节点的上节点指向原始尾节点 p.before = last; //原始尾节点的下一个节点指向当前插入节点 last.after = p; } }
也就是说将当前添加的元素设为原始链表的尾节点。
②、对于 putTreeVal 方法
是在添加红黑树节点时的操作,LinkedHashMap 也重写了该方法的 newTreeNode 方法:
1 TreeNodenewTreeNode(int hash, K key, V value, Node next) {2 TreeNodep = new TreeNode(hash, key, value, next);3 linkNodeLast(p);4 return p;5 }
也就是说上面两个方法都是在将新添加的元素放置到链表的尾端,并维护链表节点之间的关系。
③、对于 afterNodeAccess(e) 方法,在 putVal 方法中,是当添加数据键值对的 key 存在时,会对 value 进行替换。然后调用 afterNodeAccess(e) 方法:
1 //把当前节点放到双向链表的尾部 2 void afterNodeAccess(HashMap.Nodee) { // move node to last 3 LinkedHashMap.Entry last; 4 //当 accessOrder = true 并且当前节点不等于尾节点tail。这里将last节点赋值为tail节点 5 if (accessOrder && (last = tail) != e) { 6 //记录当前节点的上一个节点b和下一个节点a 7 LinkedHashMap.Entryp = 8 (LinkedHashMap.Entry)e, b = p.before, a = p.after; 9 //释放当前节点和后一个节点的关系10 p.after = null;11 //如果当前节点的前一个节点为null12 if (b == null)13 //头节点=当前节点的下一个节点14 head = a;15 else16 //否则b的后节点指向a17 b.after = a;18 //如果a != null19 if (a != null)20 //a的前一个节点指向b21 a.before = b;22 else23 //b设为尾节点24 last = b;25 //如果尾节点为null26 if (last == null)27 //头节点设为p28 head = p;29 else {30 //否则将p放到双向链表的最后31 p.before = last;32 last.after = p;33 }34 //将尾节点设为p35 tail = p;36 //LinkedHashMap对象操作次数+1,用于快速失败校验37 ++modCount;38 }39 }
该方法是在 accessOrder = true 并且 插入的当前节点不等于尾节点时,该方法才会生效。并且该方法的作用是将插入的节点变为尾节点,后面在get方法中也会调用。代码实现可能有点绕,可以借助下图来理解:
④、在看 afterNodeInsertion(evict) 方法
1 void afterNodeInsertion(boolean evict) { // possibly remove eldest2 LinkedHashMap.Entry first;3 if (evict && (first = head) != null && removeEldestEntry(first)) {4 K key = first.key;5 removeNode(hash(key), key, null, false, true);6 }7 }
该方法用来移除最老的首节点,首先方法要能执行到if语句里面,必须 evict = true,并且 头节点不为null,并且 removeEldestEntry(first) 返回true,这三个条件必须同时满足,前面两个好理解,我们看最后这个方法条件:
1 protected boolean removeEldestEntry(Map.Entry eldest) {2 return false;3 }
这就奇怪了,该方法直接返回的是 false,也就是说怎么都不会进入到 if 方法体内了,那这是这么回事呢?
这其实是用来实现 LRU(Least Recently Used,最近最少使用)Cache 时,重写的一个方法。比如在 mybatis-connector 包中,有这样一个类:
1 package com.mysql.jdbc.util; 2 3 import java.util.LinkedHashMap; 4 import java.util.Map.Entry; 5 6 public class LRUCacheextends LinkedHashMap { 7 private static final long serialVersionUID = 1L; 8 protected int maxElements; 9 10 public LRUCache(int maxSize) {11 super(maxSize, 0.75F, true);12 this.maxElements = maxSize;13 }14 15 protected boolean removeEldestEntry(Entry eldest) {16 return this.size() > this.maxElements;17 }18 }
可以看到,它重写了 removeEldestEntry(Entry
5、删除元素
同理也是调用 HashMap 的remove 方法,这里我不作过多的讲解,着重看LinkedHashMap 重写的第 46 行方法。
1 public V remove(Object key) { 2 Node e; 3 return (e = removeNode(hash(key), key, null, false, true)) == null ? 4 null : e.value; 5 } 6 7 final NoderemoveNode(int hash, Object key, Object value, 8 boolean matchValue, boolean movable) { 9 Node[] tab; Nodep; int n, index;10 //(n - 1) & hash找到桶的位置11 if ((tab = table) != null && (n = tab.length) > 0 &&12 (p = tab[index = (n - 1) & hash]) != null) {13 Nodenode = null, e; K k; V v;14 //如果键的值与链表第一个节点相等,则将 node 指向该节点15 if (p.hash == hash &&16 ((k = p.key) == key || (key != null && key.equals(k))))17 node = p;18 //如果桶节点存在下一个节点19 else if ((e = p.next) != null) {20 //节点为红黑树21 if (p instanceof TreeNode)22 node = ((TreeNode)p).getTreeNode(hash, key);//找到需要删除的红黑树节点23 else {24 do {//遍历链表,找到待删除的节点25 if (e.hash == hash &&26 ((k = e.key) == key ||27 (key != null && key.equals(k)))) {28 node = e;29 break;30 }31 p = e;32 } while ((e = e.next) != null);33 }34 }35 //删除节点,并进行调节红黑树平衡36 if (node != null && (!matchValue || (v = node.value) == value ||37 (value != null && value.equals(v)))) {38 if (node instanceof TreeNode)39 ((TreeNode)node).removeTreeNode(this, tab, movable);40 else if (node == p)41 tab[index] = node.next;42 else43 p.next = node.next;44 ++modCount;45 --size;46 afterNodeRemoval(node);47 return node;48 }49 }50 return null;51 }
View Code
我们看第 46 行代码实现:
1 void afterNodeRemoval(HashMap.Nodee) { // unlink 2 LinkedHashMap.Entryp = 3 (LinkedHashMap.Entry)e, b = p.before, a = p.after; 4 p.before = p.after = null; 5 if (b == null) 6 head = a; 7 else 8 b.after = a; 9 if (a == null)10 tail = b;11 else12 a.before = b;13 }
该方法其实很好理解,就是当我们删除某个节点时,为了保证链表还是有序的,那么必须维护其前后节点。而该方法的作用就是维护删除节点的前后节点关系。
6、查找元素
1 public V get(Object key) {2 Node e;3 if ((e = getNode(hash(key), key)) == null)4 return null;5 if (accessOrder)6 afterNodeAccess(e);7 return e.value;8 }
相比于 HashMap 的 get 方法,这里多出了第 5,6行代码,当 accessOrder = true 时,即表示按照最近访问的迭代顺序,会将访问过的元素放在链表后面。
对于 afterNodeAccess(e) 方法,在前面第 4 小节 添加元素已经介绍过了,这就不在介绍。
7、遍历元素
在介绍 HashMap 时,我们介绍了 4 中遍历方式,同理,对于 LinkedHashMap 也有 4 种,这里我们介绍效率较高的两种遍历方式:
①、得到 Entry 集合,然后遍历 Entry
1 LinkedHashMapmap = new LinkedHashMap<>();2 map.put("A","1");3 map.put("B","2");4 map.put("C","3");5 map.get("B");6 Set<Map.Entry> entrySet = map.entrySet();7 for(Map.Entry entry : entrySet ){8 System.out.println(entry.getKey()+"---"+entry.getValue());9 }
②、迭代
1 Iterator<Map.Entry> iterator = map.entrySet().iterator();2 while(iterator.hasNext()){3 Map.Entryentry = iterator.next();4 System.out.println(entry.getKey()+"----"+entry.getValue());5 }
这两种效率都还不错,通过迭代的方式可以对一边遍历一边删除元素,而第一种删除元素会报错。
打印结果:
8、迭代器
我们把上面遍历的LinkedHashMap 构造函数改成下面的:
LinkedHashMapmap = new LinkedHashMap<>(16,0.75F,true);
也就是说将 accessOrder = true,表示按照访问顺序来遍历,注意看上面的 第 5 行代码:map.get("B)。也就是说设置 accessOrder = true 之后,那么 B---2 应该是最后输出,我们看一下打印结果:
结果跟预期一致。那么在遍历的过程中,LinkedHashMap 是如何进行的呢?
我们追溯源码:首先进入到 map.entrySet() 方法里面:
发现 entrySet = new LinkedEntrySet() ,接下来我们查看 LinkedEntrySet 类。
这是一个内部类,我们查看其 iterator() 方法,发现又new 了一个新对象 LinkedEntryIterator,接着看这个类:
这个类继承 LinkedHashIterator。
1 abstract class LinkedHashIterator { 2 LinkedHashMap.Entry next; 3 LinkedHashMap.Entry current; 4 int expectedModCount; 5 6 LinkedHashIterator() { 7 next = head; 8 expectedModCount = modCount; 9 current = null;10 }11 12 public final boolean hasNext() {13 return next != null;14 }15 16 final LinkedHashMap.Entry nextNode() {17 LinkedHashMap.Entrye = next;18 if (modCount != expectedModCount)19 throw new ConcurrentModificationException();20 if (e == null)21 throw new NoSuchElementException();22 current = e;23 next = e.after;24 return e;25 }26 27 public final void remove() {28 HashMap.Nodep = current;29 if (p == null)30 throw new IllegalStateException();31 if (modCount != expectedModCount)32 throw new ConcurrentModificationException();33 current = null;34 K key = p.key;35 removeNode(hash(key), key, null, false, false);36 expectedModCount = modCount;37 }38 }
看到 nextNode() 方法,很显然是通过遍历链表的方式来遍历整个 LinkedHashMap 。
9、总结
通过上面的介绍,关于 LinkedHashMap ,我想直接用下面一幅图来解释:
去掉红色和蓝色的虚线指针,其实就是一个HashMap。