JDK1.8源码(九)——java.util.LinkedHashMap 类

  前面咱们介绍了 Map 集合的一种典型实现  HashMap  ,关于 HashMap 的特性,咱们再来复习一遍:html

  ①、基于JDK1.8的HashMap是由数组+链表+红黑树组成,相对于早期版本的 JDK HashMap 实现,新增了红黑树做为底层数据结构,在数据量较大且哈希碰撞较多时,可以极大的增长检索的效率。java

  ②、容许 key 和 value 都为 null。key 重复会被覆盖,value 容许重复。node

  ③、非线程安全mysql

  ④、无序(遍历HashMap获得元素的顺序不是按照插入的顺序)sql

  HashMap 集合能够说是最重要的集合之一,上篇博客介绍的 HashSet 集合就是继承 HashMap 来实现的。而本篇博客咱们介绍 Map 集合的另外一种实现——LinkedHashMap,其实也是继承 HashMap 集合来实现的,并且咱们在介绍 HashMap 集合的 put 方法时,也指出了 put 方法中调用的部分方法在 HashMap 都是空实现,而在 LinkedHashMap 中进行了重写。因此想要完全了解 LinkedHashMap 的实现原理,HashMap 的实现原理必定不能不懂。数组

一、LinkedHashMap 定义

  LinkedHashMap 是基于 HashMap 实现的一种集合,具备 HashMap 集合上面所说的全部特色,除了 HashMap 无序的特色,LinkedHashMap 是有序的,由于 LinkedHashMap 在 HashMap 的基础上单独维护了一个具备全部数据的双向链表,该链表保证了元素迭代的顺序。安全

  因此咱们能够直接这样说:LinkedHashMap = HashMap + LinkedList。LinkedHashMap 就是在 HashMap 的基础上多维护了一个双向链表,用来保证元素迭代顺序。数据结构

  更形象化的图形展现能够直接移到文章末尾。mybatis

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

  

二、字段属性

   ①、Entry<K,V>app

    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);
        }
    }

  LinkedHashMap 的每一个元素都是一个 Entry,咱们看到对于 Entry 继承自 HashMap 的 Node 结构,相对于 Node 结构,LinkedHashMap 多了 before 和 after 结构。

  下面是Map类集合基本元素的实现演变。

  

  LinkedHashMap 中 Entry 相对于 HashMap 多出的 before 和 after 即是用来维护 LinkedHashMap  插入 Entry 的前后顺序的。

  ②、其它属性

//用来指向双向链表的头节点
transient LinkedHashMap.Entry<K,V> head;
//用来指向双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
//用来指定LinkedHashMap的迭代顺序
//true 表示按照访问顺序,会把访问过的元素放在链表后面,放置顺序是访问的顺序
//false 表示按照插入顺序遍历
final boolean accessOrder;

   注意:这里有五个属性别搞混淆的,对于 Node  next 属性,是用来维护整个集合中 Entry 的顺序。对于 Entry before,Entry after ,以及 Entry head,Entry tail,这四个属性都是用来维护保证集合顺序的链表,其中前两个before和after表示某个节点的上一个节点和下一个节点,这是一个双向链表。后两个属性 head 和 tail 分别表示这个链表的头节点和尾节点。

  PS:关于双向链表的介绍,能够看这篇博客

三、构造函数

  ①、无参构造

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 的值。

四、添加元素

   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      * @return
13      */
14     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
15             boolean evict) {
16          Node<K,V>[] tab; Node<K,V> p; 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和不为null
22          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<K,V> 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<K,V>)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 1st
39                              treeifyBin(tab, hash);
40                          break;
41                      }
42                      //key已经存在直接覆盖value
43                      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 key
50                  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行以及5三、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.Node<K,V> newNode(int hash, K key, V value, HashMap.Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
                new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        //用临时变量last记录尾节点tail
        LinkedHashMap.Entry<K,V> last = tail;
        //将尾节点设为当前插入的节点p
        tail = p;
        //若是原先尾节点为null,表示当前链表为空
        if (last == null)
            //头结点也为当前插入节点
            head = p;
        else {
            //原始链表不为空,那么将当前节点的上节点指向原始尾节点
            p.before = last;
            //原始尾节点的下一个节点指向当前插入节点
            last.after = p;
        }
    }

  也就是说将当前添加的元素设为原始链表的尾节点。

  ②、对于 putTreeVal 方法

  是在添加红黑树节点时的操做,LinkedHashMap 也重写了该方法的 newTreeNode 方法:

1     TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
2         TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
3         linkNodeLast(p);
4         return p;
5     }

  也就是说上面两个方法都是在将新添加的元素放置到链表的尾端,并维护链表节点之间的关系。 

  ③、对于 afterNodeAccess(e) 方法,在 putVal 方法中,是当添加数据键值对的 key 存在时,会对 value 进行替换。而后调用 afterNodeAccess(e) 方法:

 1     //把当前节点放到双向链表的尾部
 2     void afterNodeAccess(HashMap.Node<K,V> e) { // move node to last
 3         LinkedHashMap.Entry<K,V> last;
 4         //当 accessOrder = true 而且当前节点不等于尾节点tail。这里将last节点赋值为tail节点
 5         if (accessOrder && (last = tail) != e) {
 6             //记录当前节点的上一个节点b和下一个节点a
 7             LinkedHashMap.Entry<K,V> p =
 8                     (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
 9             //释放当前节点和后一个节点的关系
10             p.after = null;
11             //若是当前节点的前一个节点为null
12             if (b == null)
13                 //头节点=当前节点的下一个节点
14                 head = a;
15             else
16                 //不然b的后节点指向a
17                 b.after = a;
18             //若是a != null
19             if (a != null)
20                 //a的前一个节点指向b
21                 a.before = b;
22             else
23                 //b设为尾节点
24                 last = b;
25             //若是尾节点为null
26             if (last == null)
27                 //头节点设为p
28                 head = p;
29             else {
30                 //不然将p放到双向链表的最后
31                 p.before = last;
32                 last.after = p;
33             }
34             //将尾节点设为p
35             tail = p;
36             //LinkedHashMap对象操做次数+1,用于快速失败校验
37             ++modCount;
38         }
39     }

  该方法是在 accessOrder = true 而且 插入的当前节点不等于尾节点时,该方法才会生效。而且该方法的做用是将插入的节点变为尾节点,后面在get方法中也会调用。代码实现可能有点绕,能够借助下图来理解:

  

   ④、在看 afterNodeInsertion(evict) 方法

1     void afterNodeInsertion(boolean evict) { // possibly remove eldest
2         LinkedHashMap.Entry<K,V> 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<K,V> 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 LRUCache<K, V> extends LinkedHashMap<K, V> {
 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<K, V> eldest) {
16         return this.size() > this.maxElements;
17     }
18 }

  能够看到,它重写了 removeEldestEntry(Entry<K,V> eldest) 方法,当元素的个数大于设定的最大个数,便移除首元素。

五、删除元素

   同理也是调用 HashMap 的remove 方法,这里我不做过多的讲解,着重看LinkedHashMap 重写的第 46 行方法。

 1 public V remove(Object key) {
 2         Node<K,V> e;
 3         return (e = removeNode(hash(key), key, null, false, true)) == null ?
 4             null : e.value;
 5     }
 6     
 7     final Node<K,V> removeNode(int hash, Object key, Object value,
 8             boolean matchValue, boolean movable) {
 9         Node<K,V>[] tab; Node<K,V> p; 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         Node<K,V> node = 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<K,V>)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<K,V>)node).removeTreeNode(this, tab, movable);
40         else if (node == p)
41          tab[index] = node.next;
42         else
43          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.Node<K,V> e) { // unlink
 2         LinkedHashMap.Entry<K,V> p =
 3                 (LinkedHashMap.Entry<K,V>)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         else
12             a.before = b;
13     }

  该方法其实很好理解,就是当咱们删除某个节点时,为了保证链表仍是有序的,那么必须维护其先后节点。而该方法的做用就是维护删除节点的先后节点关系。

六、查找元素

1     public V get(Object key) {
2         Node<K,V> 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 小节 添加元素已经介绍过了,这就不在介绍。

七、遍历元素

  在介绍 HashMap 时,咱们介绍了 4 中遍历方式,同理,对于 LinkedHashMap 也有 4 种,这里咱们介绍效率较高的两种遍历方式:

  ①、获得 Entry 集合,而后遍历 Entry

1         LinkedHashMap<String,String> map = 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<String,String>> entrySet = map.entrySet();
7         for(Map.Entry<String,String> entry : entrySet ){
8             System.out.println(entry.getKey()+"---"+entry.getValue());
9         }

  ②、迭代

1         Iterator<Map.Entry<String,String>> iterator = map.entrySet().iterator();
2         while(iterator.hasNext()){
3             Map.Entry<String,String> entry = iterator.next();
4             System.out.println(entry.getKey()+"----"+entry.getValue());
5         }

  这两种效率都还不错,经过迭代的方式能够对一边遍历一边删除元素,而第一种删除元素会报错。

  打印结果:

  

八、迭代器

   咱们把上面遍历的LinkedHashMap 构造函数改为下面的:

LinkedHashMap<String,String> map = 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<K,V> next;
 3         LinkedHashMap.Entry<K,V> 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<K,V> nextNode() {
17             LinkedHashMap.Entry<K,V> e = 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.Node<K,V> p = 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 。

九、总结

  经过上面的介绍,关于 LinkedHashMap ,我想直接用下面一幅图来解释:

  

 

  去掉红色和蓝色的虚线指针,其实就是一个HashMap。

相关文章
相关标签/搜索