Java 集合:(二十三) LinkedHashMap 实现类

一、LinkedHashMap 类概述

  1、LinkedHashMap 是 HashMap 的子类。

  2、在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序。

  3、与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致。

  4、LinkedHashMap 不仅实现了HashMap的所有功能,更是维护了元素的存储顺序。LinkedHashMap维护元素顺序的方式有两种,一种是维护他的存入顺序,另一种则是维护元素的读取顺序

  5、LinkedHashMap的结构是HashMap+双向链表。他通过继承HashMap得到了用hash表存储数据的能力,同时他又维护了一个双向链表实现了对元素的排序功能。

  6、 HashMap 是无序的,即迭代器的顺序与插入顺序没什么关系。而 LinkedHashMap 在 HashMap 的基础上增加了顺序:分别为「插入顺序」和「访问顺序」。即遍历 LinkedHashMap 时,可以保持与插入顺序一致的顺序;或者与访问顺序一致的顺序。

 

二、LinkedHashMap 类结构

  1、LinkedHashMap 类继承结构

    

  2、LinkedHashMap 类签名

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

  

  3、LinkedHashMap 方法列表

    

三、LinkedHashMap 中Entry节点

  1、JDK7中节点

 1     /**
 2      * LinkedHashMap entry.
 3      */
 4     private static class Entry<K,V> extends HashMap.Entry<K,V> {
 5         // These fields comprise the doubly linked list used for iteration.
 6         Entry<K,V> before, after;
 7 
 8         Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
 9             super(hash, key, value, next);
10         }
11 
12         /**
13          * Removes this entry from the linked list.
14          */
15         private void remove() {
16             before.after = after;
17             after.before = before;
18         }
19 
20         /**
21          * Inserts this entry before the specified existing entry in the list.
22          */
23         private void addBefore(Entry<K,V> existingEntry) {
24             after  = existingEntry;
25             before = existingEntry.before;
26             before.after = this;
27             after.before = this;
28         }
29 
30         /**
31          * This method is invoked by the superclass whenever the value
32          * of a pre-existing entry is read by Map.get or modified by Map.set.
33          * If the enclosing Map is access-ordered, it moves the entry
34          * to the end of the list; otherwise, it does nothing.
35          */
36         void recordAccess(HashMap<K,V> m) {
37             LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
38             if (lm.accessOrder) {
39                 lm.modCount++;
40                 remove();
41                 addBefore(lm.header);
42             }
43         }
44 
45         void recordRemoval(HashMap<K,V> m) {
46             remove();
47         }
48     }

  2、JDK8 中节点

 1     /**
 2      * HashMap.Node subclass for normal LinkedHashMap entries.
 3      */
 4     static class Entry<K,V> extends HashMap.Node<K,V> {
 5         Entry<K,V> before, after;
 6         Entry(int hash, K key, V value, Node<K,V> next) {
 7             super(hash, key, value, next);
 8         }
 9     }
10 
11     // 指向eldest元素
12     transient LinkedHashMap.Entry<K,V> head;
13     // 指向youngest元素
14     transient LinkedHashMap.Entry<K,V> tail;

  LinkedHashMap 内部有一个嵌套类 Entry,它继承自 HashMap 中的 Node 类,如上。

  jdk1.8的链表结构和1.7的差异很大,可以看出来1.8中的实现简化了不是,只维护了两个指针,befor和after。在整个链表中维护了head(头指针)和tail(尾指针)。这两个指针是有讲究的,head所指向的是eldest元素,也就是最老的元素,tail指向youngest元素,也就是最年轻的元素。在这个链表中,都是在队尾添加元素,队头删除元素,这种方式很像队列,但是还是有点区别。

四、LinkedHashMap 成员变量

  LinkedHashMap 提供了以下四个成员变量

 1     private static final long serialVersionUID = 3801124242820219131L;
 2 
 3     /**
 4      * The head (eldest) of the doubly linked list.
 5      */
 6     transient LinkedHashMap.Entry<K,V> head;   //指向 eldest 最老的元素
 7 
 8     /**
 9      * The tail (youngest) of the doubly linked list.
10      */
11     transient LinkedHashMap.Entry<K,V> tail;  //指向 yongest 最新的元素
12 
13     /**
14      * The iteration ordering method for this linked hash map: <tt>true</tt>
15      * for access-order, <tt>false</tt> for insertion-order.
16      *
17      * @serial
18      */
19     final boolean accessOrder;   //此链接的哈希映射的迭代排序方法:true 用于访问顺序,false 用于插入顺序。

五、LinkedHashMap 构造器

  LinkedHashMap 提供了两类的构造器:

  1、无参或指定成员属性的构造器

 1     /**
 2      * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
 3      * with the default initial capacity (16) and load factor (0.75).
 4      */
 5     public LinkedHashMap() {
 6         super();
 7         accessOrder = false;
 8     }
 9 
10     public LinkedHashMap(int initialCapacity) {
11         super(initialCapacity);
12         accessOrder = false;
13     }
14 
15     public LinkedHashMap(int initialCapacity, float loadFactor) {
16         super(initialCapacity, loadFactor);
17         accessOrder = false;
18     }
19 
20     public LinkedHashMap(int initialCapacity,
21                          float loadFactor,
22                          boolean accessOrder) {
23         super(initialCapacity, loadFactor);
24         this.accessOrder = accessOrder;
25     }
26 
27     public LinkedHashMap(Map<? extends K, ? extends V> m) {
28         super();
29         accessOrder = false;
30         putMapEntries(m, false);
31     }

    这里的 super() 方法调用了 HashMap 的无参构造器。该构造器方法构造了一个容量为 16(默认初始容量)、负载因子为 0.75(默认负载因子)的空 LinkedHashMap,其顺序为插入顺序。

    倒数第二个稍微不一样,它的 accessOrder 可以在初始化时指定,即指定 LinkedHashMap 的顺序(插入或访问顺序)。

    LinkedHashMap的创建和HashMap没什么两样,就是这个构造方法中,加入了acessOrder的参数,告诉LinkedHashMap以哪种方式维护顺序。

    其中 accessOrder 元素遍历顺序,true维护元素的访问顺序,最新访问的放入队尾,false维护元素的插入顺序,最新插入的在队尾。

  2、传入一个Map的构造器

1 public LinkedHashMap(Map<? extends K, ? extends V> m) {
2     super();
3     accessOrder = false;
4     putMapEntries(m, false);
5 }

六、put 元素

  LinkedHashMap 本身没有实现 put 方法,它通过调用父类(HashMap)的方法来进行读写操作。这里再贴下 HashMap 的 put 方法:

 1 public V put(K key, V value) {
 2     return putVal(hash(key), key, value, false, true);
 3 }
 4 
 5 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 6                boolean evict) {
 7     Node<K,V>[] tab; Node<K,V> p; int n, i;
 8     if ((tab = table) == null || (n = tab.length) == 0)
 9         n = (tab = resize()).length;
10     if ((p = tab[i = (n - 1) & hash]) == null)
11         // 新的 bin 节点
12         tab[i] = newNode(hash, key, value, null);
13     else {
14         Node<K,V> e; K k;
15         // key 已存在
16         if (p.hash == hash &&
17             ((k = p.key) == key || (key != null && key.equals(k))))
18             e = p;
19         // 散列冲突
20         else if (p instanceof TreeNode)
21             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
22         else {
23             // 遍历链表
24             for (int binCount = 0; ; ++binCount) {
25                 // 将新节点插入到链表末尾
26                 if ((e = p.next) == null) {
27                     p.next = newNode(hash, key, value, null);
28                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
29                         treeifyBin(tab, hash);
30                     break;
31                 }
32                 if (e.hash == hash &&
33                     ((k = e.key) == key || (key != null && key.equals(k))))
34                     break;
35                 p = e;
36             }
37         }
38         if (e != null) { // existing mapping for key
39             V oldValue = e.value;
40             if (!onlyIfAbsent || oldValue == null)
41                 e.value = value;
42             afterNodeAccess(e);
43             return oldValue;
44         }
45     }
46     ++modCount;
47     if (++size > threshold)
48         resize();
49     afterNodeInsertion(evict);
50     return null;
51 }

  这个方法哪个地方跟 LinkedHashMap 有联系呢?如何能保持 LinkedHashMap 的顺序呢?且看其中的 newNode() 方法,它在 HashMap 中的代码如下:

1 Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
2     return new Node<>(hash, key, value, next);
3 }

  但是,LinkedHashMap 重写了该方法:

1 // 新建一个 LinkedHashMap.Entry 节点
2 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
3     LinkedHashMap.Entry<K,V> p =
4         new LinkedHashMap.Entry<K,V>(hash, key, value, e);
5     // 将新节点连接到列表末尾
6     linkNodeLast(p);
7     return p;
8 }
 1 // link at the end of list
 2 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
 3     LinkedHashMap.Entry<K,V> last = tail;
 4     tail = p;
 5     // list 为空
 6     if (last == null)
 7         head = p;
 8     else {
 9         // 将新节点插入到 list 末尾
10         p.before = last;
11         last.after = p;
12     }
13 }

  可以看到,每次插入新节点时,都会存到列表的末尾。原来如此,LinkedHashMap 的插入顺序就是在这里实现的。

  此外,上文分析 HashMap 时提到两个回调方法:afterNodeAccess 和 afterNodeInsertion。它们在 HashMap 中是空的:

  HashMap提供了两个回调方法作为他的扩展,LinkedHashMap只需要实现这两个方法即可,从这里也可以学到如何提供代码的扩展性,预先留出回调接口也是个不错的选择哦。

1 // Callbacks to allow LinkedHashMap post-actions
2 void afterNodeAccess(Node<K,V> p) { }
3 void afterNodeInsertion(boolean evict) { }

  在HashMap的put方法中,调用了两个回调方法,afterNodeAccess和afterNodeInsertion。下面介绍afterNodeInsertion,这个方法的主要目的就是在map添加元素以后,维护链表的顺序,同时也会控制了对链表头元素的删除与否。

  LinkedHashMap 中重写的 afterNodeInsertion 方法:

 1 // 在插入元素以后,判断当前容器的元素是否已满,如果是的话,就删除当前最老的元素,也就是队头元素。
 2 void afterNodeInsertion(boolean evict) { // possibly remove eldest
 3     LinkedHashMap.Entry<K,V> first;
 4     if (evict && (first = head) != null && removeEldestEntry(first)) {
 5         K key = first.key;
 6         removeNode(hash(key), key, null, false, true);
 7     }
 8 }
 9 
10 // 这是用户实现的回调方法,判断当前最老的元素是否需要删除,如果为true,就删除链表头元素
11 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
12     return false;
13 }

七、get 获取元素

  LinkedHashMap的get方法几乎就是复用了HashMap。唯一的区别就是多了一个accessOrder判断,如果accessOrder==true说明他需要维护元素的访问顺序,而afterNodeAccess是HashMap提供的回调方法,他也会在put元素的时候调用。

  afterNodeAccess方法的作用就是将当前访问的元素添加到队尾,因为这个链表都是从头部删除,因此这个元素会在最后才被删除。

  同样,LinkedHashMap 对它们进行了重写。下面来分析 afterNodeAccess 方法。

  这里的 getNode 方法是父类的(HashMap)。若 accessOrder 为 true(即指定为访问顺序),则将访问的节点移到列表末尾。

  LinkedHashMap 重写了 HashMap 的 get 方法,主要是为了维持访问顺序,代码如下:

 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  }
 9 
10  void afterNodeAccess(Node<K,V> e) { // 将访问元素添加到队尾
11      LinkedHashMap.Entry<K,V> last;
12       // accessOrder 为 true 表示访问顺序
13      if (accessOrder && (last = tail) != e) {
14          // p 为访问的节点,b 为其前驱,a 为其后继
15          LinkedHashMap.Entry<K,V> p =
16              (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
17          p.after = null;
18          // p 是头节点
19          // 如果当前元素是头元素,那么就将head指向他的下一个节点
20          if (b == null)
21              head = a;
22          else
23              b.after = a;
24          // 如果当前元素是尾元素,那么就将last指向他的上一个节点
25          if (a != null)
26              a.before = b;
27          else
28              last = b;
29          if (last == null)
30              head = p;
31          else {
32              p.before = last;
33              last.after = p;
34          }
35          tail = p;
36          ++modCount;
37      }
38  }

  为了便于分析和理解,这里画出了两个操作示意图: 

  

  

  这里描述了进行该操作前后的两种情况。可以看到,该方法执行后,节点 p 被移到了 list 的末尾。

八、删除元素

   removeNode 方法是父类 HashMap 中的。

 1 final Node<K,V> removeNode(int hash, Object key, Object value,
 2                            boolean matchValue, boolean movable
 3 ) {
 4     Node<K,V>[] tab; Node<K,V> p; int n, index;
 5     // table 不为空,且给的的 hash 值所在位置不为空
 6     if ((tab = table) != null && (n = tab.length) > 0 &&
 7         (p = tab[index = (n - 1) & hash]) != null) {
 8         Node<K,V> node = null, e; K k; V v;
 9         // 给定 key 对应的节点,在数组中第一个位置
10         if (p.hash == hash &&
11             ((k = p.key) == key || (key != null && key.equals(k))))
12             node = p;
13         // 给定的 key 所在位置为红黑树或链表
14         else if ((e = p.next) != null) {
15             if (p instanceof TreeNode)
16                 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
17             else {
18                 do {
19                     if (e.hash == hash &&
20                         ((k = e.key) == key ||
21                          (key != null && key.equals(k)))) {
22                         node = e;
23                         break;
24                     }
25                     p = e;
26                 } while ((e = e.next) != null);
27             }
28         }
29         // 删除节点
30         if (node != null && (!matchValue || (v = node.value) == value ||
31                              (value != null && value.equals(v)))) {
32             if (node instanceof TreeNode)
33                 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
34             else if (node == p)
35                 tab[index] = node.next;
36             else
37                 p.next = node.next;
38             ++modCount;
39             --size;
40             // 删除节点后的操作
41             afterNodeRemoval(node);
42             return node;
43         }
44     }
45     return null;
46 }

  afterNodeRemoval 方法在 HashMap 中的实现也是空的:

void afterNodeRemoval(Node<K,V> p) { }

  

  在删除元素以后,LinkedHashMap需要维护当前链表的指针,也就是双向链表的head和tail指针的指向问题。

  LinkedHashMap 重写了该方法:

 1 void afterNodeRemoval(Node<K,V> e) {
 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     // 如果当前元素是头元素,那么head指向他的下一个节点
 6     if (b == null)
 7         head = a;
 8     else
 9         b.after = a;
10     // 如果当前元素是尾元素,那么tail指向他的上一个节点
11     if (a == null)
12         tail = b;
13     else
14         a.before = b;
15 }

   该方法就是双链表删除一个节点的操作。

九、遍历操作

  容器的遍历是一个亘古不变的话题,然而LinkedHashMap的遍历方式有他的特殊性。因为他在hash表的基础之上又维护了一个双向链表,而这个链表维护这元素的遍历顺序,因为LinkedHashMap在遍历的时候,只能遍历这个链表,而不能像HashMap一样遍历hash表。

 1 abstract class LinkedHashIterator {
 2     LinkedHashMap.Entry<K,V> next;
 3     LinkedHashMap.Entry<K,V> current;
 4     int expectedModCount;
 5     LinkedHashIterator() {
 6         // 第一次从头开始遍历
 7         next = head;
 8         expectedModCount = modCount;
 9         current = null;
10     }
11     public final boolean hasNext() {
12         return next != null;
13     }
14     // 对链表从头到尾开始遍历,顺序遍历的方式很简单就是next = e.after
15     final LinkedHashMap.Entry<K,V> nextNode() {
16         LinkedHashMap.Entry<K,V> e = next;
17         if (modCount != expectedModCount)
18             throw new ConcurrentModificationException();
19         if (e == null)
20             throw new NoSuchElementException();
21         current = e;
22         next = e.after;
23         return e;
24     }
25     public final void remove() {
26         Node<K,V> p = current;
27         if (p == null)
28             throw new IllegalStateException();
29         if (modCount != expectedModCount)
30             throw new ConcurrentModificationException();
31         current = null;
32         K key = p.key;
33         removeNode(hash(key), key, null, false, false);
34         expectedModCount = modCount;
35     }
36 }

十、代码实验

  1、HashMap 是无序的

1 Map<String, String> map = new HashMap<>();
2 map.put("bush", "a");
3 map.put("obama", "b");
4 map.put("trump", "c");
5 map.put("lincoln", "d");
6 System.out.println(map);
7 
8 // 输出结果(无序):
9 // {obama=b, trump=c, lincoln=d, bush=a}

  2、 LinkedHashMap,则可以保持插入的顺序

1 Map<String, String> map = new LinkedHashMap<>();
2 map.put("bush", "a");
3 map.put("obama", "b");
4 map.put("trump", "c");
5 map.put("lincoln", "d");
6 System.out.println(map);
7 
8 // 输出结果(插入顺序):
9 // {bush=a, obama=b, trump=c, lincoln=d}

  3、指定 LinkedHashMap 的顺序为访问顺序:

 1 Map<String, String> map = new LinkedHashMap<>(2, 0.75f, true);
 2 map.put("bush", "a");
 3 map.put("obama", "b");
 4 map.put("trump", "c");
 5 map.put("lincoln", "d");
 6 System.out.println(map);
 7 
 8 map.get("obama");
 9 System.out.println(map);
10 
11 // 输出结果(插入顺序):
12 // {bush=a, obama=b, trump=c, lincoln=d}
13 
14 // 访问 obama 后,obama 移到了末尾
15 // {bush=a, trump=c, lincoln=d, obama=b}

  4、实现 LRU 缓存

private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
  private int capacity;
  
  public LRUCache(int capacity) {
    super(16, 0.75f, true);
    this.capacity = capacity;
  }
  
  @Override
  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return size() > capacity;
  }
}

    测试:

1 LRUCache<String, String> lruCache = new LRUCache<>(2);
2 lruCache.put("bush", "a");
3 lruCache.put("obama", "b");
4 lruCache.put("trump", "c");
5 System.out.println(lruCache);
6 
7 // 输出结果:
8 // {obama=b, trump=c}

这里定义的 LRUCache 类中,对 removeEldestEntry 方法进行了重写,当缓存中的容量大于 2,时会把最早插入的元素 "bush" 删除。因此只剩下两个值。

十一、总结

  1. LinkedHashMap 继承自 HashMap,其结构可以理解为「双链表 + 散列表」;

  2. 可以维护两种顺序:插入顺序或访问顺序;

  3. 可以方便的实现 LRU 缓存;

  4. 线程不安全。

原文地址:https://www.cnblogs.com/niujifei/p/14750642.html