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. 线程不安全。

  • 相关阅读:
    【sqli-labs】 less23 Error based
    【sqli-labs】 less22 Cookie Injection- Error Based- Double Quotes
    【sqli-labs】 less21 Cookie Injection- Error Based- complex
    【sqli-labs】 less20 POST
    【sqli-labs】 less19 POST
    【sqli-labs】 less18 POST
    【sqli-labs】 less17 POST
    【sqli-labs】 less16 POST
    【sqli-labs】 less15 POST
    nginx.conf(centos7 1.14)主配置文件修改
  • 原文地址:https://www.cnblogs.com/niujifei/p/14750642.html
Copyright © 2011-2022 走看看