java 集合框架-LinkedHashMap

一、概述

1、繼承擴展HashMap,實現Map接口,基於雙向鏈表實現有序,支持插入有序和訪問順序

2、允許NULL元素,基本操作(add、contrains、remove)與HashMap一樣有穩定性能(hash分佈均勻情況下)

3、由於需要維護鏈表,性能較HashMap差,而迭代可能不一定,LinkedHashMap的迭代所需時間與 大小 成比例,HashMap迭代所需時間與 容量 成比例

4、初始容量和負載因子對性能有影響,這與HashMap相似,但初始容量非常大的影響比HashMap要小,因爲迭代不受容量影響

5、非同步,非線程安全,可使用 Collections.synchronizedMap 封裝爲同步

6、迭代同樣是 快速失敗 機制

二、源碼分析

1、變量

    /**
     * The head of the doubly linked list.
     */
    private transient Entry<K,V> header;

    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    private final boolean accessOrder;

header 保持鏈表頭的引用, accessOrder 表示改對象的有序性是訪問順序還是插入順序,默認爲false:插入順序

2、構造函數

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the specified initial capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the specified initial capacity and a default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity
     * @throws IllegalArgumentException if the initial capacity is negative
     */
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the default initial capacity (16) and load factor (0.75).
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    /**
     * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
     * the same mappings as the specified map.  The <tt>LinkedHashMap</tt>
     * instance is created with a default load factor (0.75) and an initial
     * capacity sufficient to hold the mappings in the specified map.
     *
     * @param  m the map whose mappings are to be placed in this map
     * @throws NullPointerException if the specified map is null
     */
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
    }

    /**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

基本上與HashMap的構造函數對應上,每個構造函數都是super先構造父類,從構造函數看,只增加了一個 accessOrder 變量

3、核心實現

從構造函數看不到LinkedHashMap有啥不一樣,往下看發現它重寫實現了HashMap定義的許多鉤子方法,如 init():

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

這個init是HashMap定義留給子類的鉤子,方便子類擴展,LinkedHashMap便是利用這些擴展點;初始化中,header 指向一個空的Entry,並且頭尾指針都是自身;這個初始化方法在HashMap的構造方法調用,能保證此時還沒有元素插入;後面的方法有很多關於這個雙向鏈表的指針操作,先看看這個鏈表Node的定義:

    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);
        }

        /**
         * Removes this entry from the linked list.
         */
        private void remove() {
            before.after = after;
            after.before = before;
        }

        /**
         * Inserts this entry before the specified existing entry in the list.
         */
        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

        /**
         * This method is invoked by the superclass whenever the value
         * of a pre-existing entry is read by Map.get or modified by Map.set.
         * If the enclosing Map is access-ordered, it moves the entry
         * to the end of the list; otherwise, it does nothing.
         */
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

        void recordRemoval(HashMap<K,V> m) {
            remove();
        }
    }

可以看到,LinkedHashMap的Entry也是繼承HashMap的Entry,並且增加了兩個變量:前後指針,重寫實現了HashMap定義的鉤子方法:recordAccess和recordRemoval,這兩個方法在HashMap.Entry定義但爲空實現;recordAccess在已存在元素被get或者put的時候被調用,作用是維護元素的’訪問有序’,如果變量accessOrder爲true(訪問順序),那麼該方法將當期訪問元素移動到鏈表最後(header的前面)以保持訪問順序,否則不做任何操作;而recordRemoval方法在元素被移除remove時調用,這裏調用了一個私有方法remove,實際是將刪除的元素從雙向鏈表移除掉;

接下來我們看看,一個元素正常put進來時的操作,LinkedHashMap並沒有重寫HashMap的put方法,而只是重寫了addEntry和createEntry方法:

    /**
     * This override alters behavior of superclass put method. It causes newly
     * allocated entry to get inserted at the end of the linked list and
     * removes the eldest entry if appropriate.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        super.addEntry(hash, key, value, bucketIndex);

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

    /**
     * This override differs from addEntry in that it doesn't resize the
     * table or remove the eldest entry.
     */
    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的正常put流程(詳情可看之前文章):計算hash、獲取下標、查找是否已存在key-value(如是則更新後結束)、新增Entry(addEntry)、判斷擴容、創建新Entry(createEntry);即 put 會調用addEntry,而addEntry會調用createEntry;LinkedHashMap 重寫的createEntry方法中,實現與HashMap只多了一句邏輯:e.addBefore(header),將當期新增的元素加到鏈表的最後一位,這裏就不需要判斷或者調用recordAccess,因爲無論何種排序,新增都是排最後(最新位置);

而重寫的addEntry方法,調用父類的實現後,進行了一個判斷:是否刪除最舊的元素,這個判斷方法removeEldestEntry在LinkedHashMap中直接返回false,即不刪除最舊元素;那麼這個設計有什麼用了?我們知道LinkedHashMap支持維持訪問順序,也就是最近訪問的排序在前,在此排序方式下,最老的一個元素eldest也就是最舊沒有訪問過的,這就非常像LRU(Least recently used)緩存了,加入新元素時,將最少使用的元素刪除掉;我們只需要簡單繼承下LinkedHashMap,設置accessorder 爲true,重寫改方法:

     private static final int MAX_ENTRIES = 100;

     protected boolean removeEldestEntry(Map.Entry eldest) {
          return size() > MAX_ENTRIES;
     }

這樣就基於LinkedHashMap簡單實現了一個LRU緩存;
而事實上真的是有這麼實現的,或者大家都已經使用過而不知道而已,在工程代碼中通過Eclipse看這個方法的繼承實現:
image
可以看到mysql jdbc裏面的一個LRU緩存就是這麼實現:

/**
 * @author Mark Matthews
 * @version $Id$
 */
public class LRUCache extends LinkedHashMap<Object, Object> {
    private static final long serialVersionUID = 1L;
    protected int maxElements;

    public LRUCache(int maxSize) {
        super(maxSize, 0.75F, true);
        this.maxElements = maxSize;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)
     */
    @Override
    protected boolean removeEldestEntry(Entry<Object, Object> eldest) {
        return (size() > this.maxElements);
    }
}

LinkedHashMap還重寫了HashMap兩個方法:transfer和containsValue,邏輯都是一樣的,只是利用了鏈表來迭代提高性能:

    /**
     * Transfers all entries to new table array.  This method is called
     * by superclass resize.  It is overridden for performance, as it is
     * faster to iterate using our linked list.
     */
    @Override
    void transfer(HashMap.Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e = header.after; e != header; e = e.after) {
            if (rehash)
                e.hash = (e.key == null) ? 0 : hash(e.key);
            int index = indexFor(e.hash, newCapacity);
            e.next = newTable[index];
            newTable[index] = e;
        }
    }


    /**
     * Returns <tt>true</tt> if this map maps one or more keys to the
     * specified value.
     *
     * @param value value whose presence in this map is to be tested
     * @return <tt>true</tt> if this map maps one or more keys to the
     *         specified value
     */
    public boolean containsValue(Object value) {
        // Overridden to take advantage of faster iterator
        if (value==null) {
            for (Entry e = header.after; e != header; e = e.after)
                if (e.value==null)
                    return true;
        } else {
            for (Entry e = header.after; e != header; e = e.after)
                if (value.equals(e.value))
                    return true;
        }
        return false;
    }

但是我覺得,不是說使用LinkedHashMap就性能比HashMap好,只能說這個迭地上性能稍微好一點,但是卻要增加頭尾指針的空間和維護雙向鏈表的操作;

最後來看看迭代器,應該可以猜到,肯定是基於雙向鏈表進行迭代而不再是底層table數組

    private abstract class LinkedHashIterator<T> implements Iterator<T> {
        Entry<K,V> nextEntry    = header.after;
        Entry<K,V> lastReturned = null;

        /**
         * The modCount value that the iterator believes that the backing
         * List should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
        int expectedModCount = modCount;

        public boolean hasNext() {
            return nextEntry != header;
        }

        public void remove() {
            if (lastReturned == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            LinkedHashMap.this.remove(lastReturned.key);
            lastReturned = null;
            expectedModCount = modCount;
        }

        Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (nextEntry == header)
                throw new NoSuchElementException();

            Entry<K,V> e = lastReturned = nextEntry;
            nextEntry = e.after;
            return e;
        }
    }

其他迭代器與HashMap一樣,繼承父類LinkedHashIterator,重寫next方法返回需要的對象

    private class KeyIterator extends LinkedHashIterator<K> {
        public K next() { return nextEntry().getKey(); }
    }

    private class ValueIterator extends LinkedHashIterator<V> {
        public V next() { return nextEntry().value; }
    }

    private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() { return nextEntry(); }
    }

三、總結

有幾個點是比較重要的:
1、雙向鏈表及維護,在Entry中增加了before、after指針
2、accessorder 控制順序方式:訪問順序、插入順序(默認)
3、可以繼承實現LRU緩存
4、迭代方式使用鏈表,性能有提升,增加了鏈表空間和維護

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章