LinkedHashMap 學習筆記

LinkedHashMap 在 Map 集合框架的位置

LinkedHashMap

LinkedHashMap簡介

LinkedHashMap是Map 接口的 哈希表(hash table) 和 鏈表(linked list) 的實現,具有可預測的迭代順序。此實現與 HashMap 的不同之處在於,它維護一個雙鏈接列表,該列表貫穿其所有條目。這個鏈表定義了迭代的順序,通常是鍵值插入到 map 中的插入順序(insertion-order). 請注意,如果鍵重新插入到 map 中,則插入順序不受影響。

在遍歷LinkedHashMap時,不會像遍歷 HashMap 和 Hashtable 所得到的遍歷順序是不確定的。而且LinkedHashMap在遍歷有順序保證的同時,不會像TreeMap那樣招致額外的開銷。它可以用來拷貝已有的map, 不用考慮這個被拷貝的map對象原來的實現方式。拷貝出來的map對象和原始的map對象的順序是一致的。

     void foo(Map m) {
         Map copy = new LinkedHashMap(m);
         ...
     }

寫一個測試方法跑一下看下結果:

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapTest {
  public static void main(String args[]) {
      Map<Integer, Integer> map = new HashMap();
      for(int i=1; i<5; i++) {
          map.put(i, i*i);
      }
      for(int i=1; i>-5; i--) {
          map.put(i, i*i);
      }
      System.out.println(map);
      System.out.println(map);
      System.out.println(map);
      System.out.println("-----------------copy from map -----------------------");
      LinkedHashMap<Integer,Integer> copy = new LinkedHashMap<Integer, Integer>(map);
      System.out.println(copy);
      Map<Integer, Integer> linkedHashMap = new LinkedHashMap();
      for(int i=1; i<5; i++) {
          linkedHashMap.put(i, i*i);
      }
      for(int i=1; i>-5; i--) {
          linkedHashMap.put(i, i*i);
      }
      System.out.println("-----------------------------------------");
      System.out.println(linkedHashMap);
      System.out.println("---------- put exist key again ---------------------");
      for(int i=1; i<5; i++) {
          linkedHashMap.put(i, i*i);
      }
      System.out.println(linkedHashMap);
      System.out.println("--- remove exist key and put again, keep insert order ----");
      linkedHashMap.remove(3);
      linkedHashMap.put(3, 9);
      System.out.println(linkedHashMap);
  }
}

輸出內容:

{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
-----------------copy from map -----------------------
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
-----------------------------------------
{1=1, 2=4, 3=9, 4=16, 0=0, -1=1, -2=4, -3=9, -4=16}
---------- put exist key again ---------------------
{1=1, 2=4, 3=9, 4=16, 0=0, -1=1, -2=4, -3=9, -4=16}
--- remove exist key and put again, keep insert order ----
{1=1, 2=4, 4=16, 0=0, -1=1, -2=4, -3=9, -4=16, 3=9}

如果模塊在輸入上獲取映射,將其複製並隨後返回結果(其順序由副本的順序確定),則此技術特別有用。(客戶通常喜歡按插入的順序返回。)

構造函數的 accessOrder 參數

LinkedHashMap 有一個特殊的構造函數:

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

這個構造函數的參數除了有初始化容量 initialCapacity 和加載因子 loadFactor 之外,還有一個布爾參數:accessOrder
accessOrder指定了維護順序的模式。

  • accessOrder = false. 按照插入順序進行排序。
  • accessOrder = true, 按照訪問順序排序。也就是根據上一次訪問截止時。按照最近最少到最近最多的訪問順序排列。
    寫一個方法測試一下:
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapTest {
    public static void main(String args[]) {
        Map<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(10, 0.75f, true);
        for(int i=0; i<10; i++) {
            map.put(i, i*i);
        }
        System.out.println("------------origin order---------");
        System.out.println(map);
        System.out.println(map.values());

        for(int i=1; i<10; i++) {
            map.get(3);
        }
        for(int i=1; i<30; i++) {
            map.get(6);
        }
        for(int i=1; i<10; i++) {
            map.get(4);
        }
        for(int i=1; i<9; i++) {
            map.get(2);
        }
        System.out.println("------------ after access ---------");
        System.out.println(map);
        System.out.println(map.values());
    }
}

輸出內容:

------------origin order---------
{0=0, 1=1, 2=4, 3=9, 4=16, 5=25, 6=36, 7=49, 8=64, 9=81}
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
------------ after access ---------
{0=0, 1=1, 5=25, 7=49, 8=64, 9=81, 3=9, 6=36, 4=16, 2=4}
[0, 1, 25, 49, 64, 81, 9, 36, 16, 4]

它的移動算法如下:

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

重寫 removeEldestEntry(Map.Entry<K,V> eldest) 方法

如果此 Map 應刪除其老的條目,則返回 true。 此方法在將新條目插入映射後,通過 put 和 putAll 調用。 它爲實現者提供了每次添加新條目時刪除最老條目的機會。 如果映射表示緩存,這非常有用:它允許Map通過刪除陳舊條目來減少內存消耗。
寫一個代碼測試一下:

import java.util.LinkedHashMap;
import java.util.Map;

public class CacheMap<K, V> extends LinkedHashMap<K, V> {
    public static final int MAX_SIZE = 10;

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> entry) {
        return size() > MAX_SIZE;
    }

    public CacheMap(int i, float v, boolean b) {
        super(i, v, b);
    }

    public static void main(String[] args) {
        CacheMap<Integer, Integer> cacheMap = new CacheMap<Integer, Integer>(40, 0.75f, true);
        for (int i=0; i<40; i++) {
            cacheMap.put(i, i*i);
        }
        for (int i=1; i<20; i++) {
            cacheMap.get(3);
        }
        System.out.println(cacheMap);
        System.out.println("cacheMap.size() = "+cacheMap.size());
        cacheMap.put(20, 400);
        System.out.println("-----------after insert new element---------------");
        System.out.println(cacheMap);
        System.out.println("cacheMap.size() = "+cacheMap.size());
    }
}

輸出內容:

{30=900, 31=961, 32=1024, 33=1089, 34=1156, 35=1225, 36=1296, 37=1369, 38=1444, 39=1521}
cacheMap.size() = 10
-----------after insert new element---------------
{31=961, 32=1024, 33=1089, 34=1156, 35=1225, 36=1296, 37=1369, 38=1444, 39=1521, 20=400}
cacheMap.size() = 10

看到沒有,在CacheMap裏面,我設置最大容量是10.通過重寫removeEldestEntry方法,在裏面判斷size()和MAX_SIZE的大小決定要不要移除最老的元素。以達到插入新元素時移除老元素的目的。

此方法通常不會以任何方式修改map,而是允許map按照其返回值的指示進行自身修改。此方法允許直接修改map,但如果這樣做,則必須返回 false(指示map不應嘗試任何進一步修改)。未指定在此方法內修改映map返回 true 的效果。

在LinkedHashMap中,此實現僅返回 false(因此此映射的行爲類似於普通地圖 - 最長的元素永遠不會被刪除)。

LinkedHashMap 的數據結構

    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    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);
        }
    }
 //-------------------------------------------------------------------------------------------------
  //------------------------------------------- HashMap.Node ----------------------------------------
   //-------------------------------------------------------------------------------------------------
  /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

可以看到,Entry 和Map.Node相比,多了兩個屬性:before 和 after. 用於維護雙向隊列。

如何在隊尾插入元素

  • 先把 tail 備份爲 last
  • 把tail賦值爲新entry p
  • 如果last爲null.表示空map.直接把新 entry p 賦值給 head
  • 如果last不爲null,表示非空map. 把p的前驅節點賦值爲之前的last.把之前的last的後去賦值爲新 entry p.
    // link at the end of list
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    ```

### 如何替換Entry
```java
    // apply src's links to dst
    private void transferLinks(LinkedHashMap.Entry<K,V> src,
                               LinkedHashMap.Entry<K,V> dst) {
        LinkedHashMap.Entry<K,V> b = dst.before = src.before;
        LinkedHashMap.Entry<K,V> a = dst.after = src.after;
        if (b == null)
            head = dst;
        else
            b.after = dst;
        if (a == null)
            tail = dst;
        else
            a.before = dst;
    }
發佈了76 篇原創文章 · 獲贊 9 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章