zoukankan      html  css  js  c++  java
  • 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. 线程不安全。

  • 相关阅读:
    [置顶] windows player,wzplayerV2 for windows
    wzplayer 近期将会支持BlackBerry和WinPhone8
    wzplayerEx for android(真正硬解接口,支持加密的 player)
    ffmpeg for ios 交叉编译 (支持i686 armv7 armv7s) 包含lame支持
    ffmpeg for ios 交叉编译 (支持i686 armv7 armv7s) 包含lame支持
    编译cegcc 0.59.1
    wzplayer 近期将会支持BlackBerry和WinPhone8
    wzplayerEx for android(真正硬解接口,支持加密的 player)
    windows player,wzplayerV2 for windows(20140416)更新
    编译cegcc 0.59.1
  • 原文地址:https://www.cnblogs.com/niujifei/p/14750642.html
Copyright © 2011-2022 走看看