1. 程式人生 > 實用技巧 >java-map之LinkedHashMap

java-map之LinkedHashMap

1.1概述

在使用HashMap的時候,可能會遇到需要按照當時put的順序來進行雜湊表的遍歷。通過上篇對HashMap的瞭解,我們知道HashMap中不存在儲存順序的機制。本篇文章要介紹的LinkedHashMap專為此特性而生。在LinkedHashMap中可以保持兩種順序,分別是插入順序和訪問順序,這個是可以在LinkedHashMap的初始化方法中進行指定的。相對於訪問順序,按照插入順序進行編排被使用到的場景更多一些,所以預設是按照插入順序進行編排。

1.2結構


所以該結構其實就是用雙向連結串列加hashmap實現的,

1.3特點

  • key和value都允許為空
  • key重複會覆蓋,value可以重複
  • 有序的
  • LinkedHashMap是非執行緒安全的

1.4詳解

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

可以看到,LinkedHashMap繼承了HashMap,實現了Map介面。
在屬性上它比HashMap多了兩個屬性:

//連結串列的頭結點
private transient Entry<K,V> header;
//該屬性指取得鍵值對的方式,是個布林值,false表示插入順序,true表示訪問順序,也就是訪問次數.
private final boolean accessOrder;

LinkedHashMap有五個構造器:

   	//用預設的初始容量和負載因子構建一個LinkedHashMap,取出鍵值對的方式是插入順序
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    //構造一個指定初始容量的LinkedHashMap,取得鍵值對的順序
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    //構造一個指定初始容量和負載因子,按照插入順序的LinkedHashMap
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    //根據給定的初始容量,負載因子和鍵值對迭代順序構建一個LinkedHashMap
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

    //通過給定的map建立一個LinkedHashMap,負載因子是預設值,迭代方式是插入順序.
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
    }

其實主要區別還是在基本資料結構:

  private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;

        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }

LinkedHashMap的Entry類繼承了HashMap的Entry,並在此基礎上進行了擴充套件,它擁有以下屬性:

K key;
V value;
Entry<K, V> next;
int hash;
Entry<K, V> before;
NEtry<K, V> after;

LinkedHashMap的初始化實際上是先呼叫HashMap的初始化,最後呼叫LinkedHashMap的init()使得header初始化。

    void init() {
        header = new Entry<>(-1, null, null, null);
        header.before = header.after = header;
    }

在header中,hash值為-1,其他都為null,也就是說這個header不在陣列table中,其實它就是用來指示開源元素、標記結束元素的.header的目的就是為了記錄第一個插入的元素是誰,在遍歷的時候能夠找到第一個元素
LinkedHashMap儲存元素只是重寫了寫了父類put方法邏輯中呼叫的子方法addEntry()和createEntry()。

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //在這裡呼叫的是LinkedHashMap重寫後的addEntry方法,這之前的和HashMap一樣
        addEntry(hash, key, value, i);
        return null;
    }

在最後一步呼叫LinkedHashMap的addEntry方法:

    void addEntry(int hash, K key, V value, int bucketIndex) {		
        //呼叫HashMap的addEntry,在map部分新增元素。
        super.addEntry(hash, key, value, bucketIndex);

        // Remove eldest entry if instructed
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
    }

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
		//在這裡呼叫LinkedHashMap自己的createEntry方法.
        createEntry(hash, key, value, bucketIndex);
    }

LinkedHashMap自己的createEntry()方法:

    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
        Entry<K,V> e = new Entry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);
        size++;
    }

其實以上的操作和HashMap的操作沒有什麼不同,都是把新新增的節點放在了table[bucketIndex]位置上,差別在於LinkedHashMap還做了addBefore操作,而addBefore方法的目的就是讓新的Entry和原連結串列生成一個雙向連結串列.

        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

LinkedHashMap獲得元素:

    public V get(Object key) {
        //呼叫父類的getEntry方法獲取元素
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        //該方法用來記錄訪問順序.
        e.recordAccess(this);
        return e.value;
    }

可以看到,在get方法中,它是呼叫了父類的getEntry方法來獲取到元素,之後再呼叫自己的recordAccess()方法:

        void recordAccess(HashMap<K,V> m) {
            //因為在之前是轉型為父類物件來獲取entry的,所以這裡要轉回LinkedHashMap,判斷獲取資料的方式.
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            //當LinkedHashMap按照訪問來排序時
            if (lm.accessOrder) {
                lm.modCount++;
                //移除當前節點
                remove();
                //將當前節點插入到頭節點前面,即最後面.
                addBefore(lm.header);
            }
        }

1.5應用

實現LRU演算法:

public class LRUCache extends LinkedHashMap
{
    public LRUCache(int maxSize)
    {
        super(maxSize, 0.75F, true);
        maxElements = maxSize;
    }

    protected boolean removeEldestEntry(java.util.Map.Entry eldest)
    {
        return size() > maxElements;
    }

    private static final long serialVersionUID = 1L;
    protected int maxElements;
}

然後重寫一下recordAccess()方法:
``java
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);

```java
        private void remove() {
            before.after = after;
            after.before = before;
        }
        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

當然,也可以自定義實現該演算法,和上面這種繼承方式不一樣的的地方在於LRUCache類的節點中包含的是三個成員變數,一個最大容量,一個雙向連結串列,一個map集合。然後在LRUCache的增刪改查中封裝雙向連結串列和map集合的api函式。