手撕LinkHashMap

LinkedHashMap 繼承自 HashMap ,在瞭解了 HashMap 之後,再來看看 LinkedHashMap 是如何維護它的順序的。

初始化

LinkedHashMap 的構造函數比 HashMap 的構造函數多維護了一個成員變量 accessOrder 。該成員變量如果爲 true ,則按訪問順序,否則按插入順序(默認爲 false)。

看下面這個例子:

        Map<String, String> insertOrderMap = new LinkedHashMap<>();
        for (int i = 0; i < 5; i++){
            insertOrderMap.put(String.valueOf(i), String.valueOf(i));
        }
        assert insertOrderMap.keySet().toString().equals("[0, 1, 2, 3, 4]");

        Map<String, String> accessOrderMap = new LinkedHashMap<>(16, 0.75f,true);
        for (int i = 0; i < 5; i++){
            accessOrderMap.put(String.valueOf(i), String.valueOf(i));
        }
        accessOrderMap.get(String.valueOf(3));
        accessOrderMap.get(String.valueOf(1));
        assert accessOrderMap.keySet().toString().equals("[0, 2, 4, 3, 1]");

斷言最終都會正確執行!在訪問順序模式下,調用 get 方法,改變了遍歷的順序。我們知道的是 LinkedHashMap 底層採用了雙向鏈表來維護順序,查看 get 方法實現:

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

accessOrder 爲 true 時,會重新維護鏈表。

    void afterNodeAccess(Node<K,V> e) { // 移動節點到最後
        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;
        }
    }

上面的邏輯還是蠻多的,不過也是因爲要考慮的情況比較多,理解起來也不算太複雜。


存取操作

將 key 重新插入到 Map 中,插入順序不受影響。那如果是訪問順序模式呢?

查看 put 源碼,最終發現:

			if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 更改鏈表順序
                afterNodeAccess(e);
                return oldValue;
            }

上面的代碼是在 HashMapputVal 方法的一部分。afterNodeAccess 由子類 LinkedHashMap 實現(模板方法模式)。僅僅只有在重新插入已經存在映射的 key 時,纔會調整訪問順序模式下的順序。

可能比較疑惑的是 在插入順序模式下,也沒有看見 LinkedHashMap 維護雙向鏈表啊

其實不然,在 putVal 方法中有發現這樣一行代碼吧?

tab[i] = newNode(hash, key, value, null);

子類 LinkedHashMap 重寫了該方法,這種方式是真的漂亮!重寫後的方法如下:

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return 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;
        }
    }

也就是說,LinkedHashMap 在 put 值的時候,其實,一直在維護鏈表,這裏需要注意。


刪除操作

LinkedHashMap 並沒有重寫 remove 方法,那麼 ,它是如何實現在刪除元素後進行雙向鏈表的處理的呢?查看 remove 方法,最終找到 afterNodeRemoval(node) 方法(模板方法模式),LinkedHashMap 實現了該方法 :

    // 將當前節點從雙向鏈表當中剔除
	void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        // 如果當前節點爲頭節點,則重新設置頭節點
        if (b == null)
            head = a;
        else
            b.after = a;
        // 如果當前節點爲尾節點,則重新設置尾節點
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

迭代器

重要的一步來了,在進行迭代器遍歷時,如何保證按照一定的順序(插入或者訪問)來遍歷呢?

    abstract class LinkedHashIterator {
        LinkedHashMap.Entry<K,V> next;
        LinkedHashMap.Entry<K,V> current;
        int expectedModCount;

        LinkedHashIterator() {
            // 從頭節點開始遍歷
            next = head;
            expectedModCount = modCount;
            current = null;
        }

        public final boolean hasNext() {
            return next != null;
        }

        final LinkedHashMap.Entry<K,V> nextNode() {
            LinkedHashMap.Entry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

迭代器的代碼比較容易理解,需要注意的是針對不同的集合視圖(keySetentrySetvalues),訪問不同的迭代器,僅僅是 next 方法中獲取節點的值(keynodevalue)不同,所以在不同集合視圖下的不同迭代器,只要繼承 LinkedHashIterator 類,新增 next 方法,實現 Iterator 接口。

	final class LinkedKeyIterator extends LinkedHashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().getKey(); }
    }

    final class LinkedValueIterator extends LinkedHashIterator
        implements Iterator<V> {
        public final V next() { return nextNode().value; }
    }

    final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

除了以上通過迭代器遍歷之外,還有一種方法,這是在 1.8 新加的 forEach 方法:

	public void forEach(BiConsumer<? super K, ? super V> action) {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e.key, e.value);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }

代碼實現來看,這種遍歷方法仍然是有序的!

在使用構造函數傳入的Map 爲LinkedHashMap 時, 重新構造的 LinkedHashMap 仍然維持傳入的 LinkedHashMap的順序。


構建 LRU 緩存

這一點是從文檔中發現的,百度了一下 LRU 緩存

LRU 是Least Recently Used的縮寫,即最近最少使用。

研究了下具體的實現,重載方法 removeEldestEntry(Map.Entry) ,實現移除最老的 Entry 策略即可。具體的原理是因爲 HashMap 在實現 putVal 方法時,調用了回調函數 afterNodeInsertion(evict) ,該函數由子類 LinkedHashMap 做了具體實現:

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

正如上述代碼,我們只要實現 removeEldestEntry 方法,就能夠自定義移除最老的 Entry 策略了。

:實現一個 容量爲 10 的 LRU 緩存:

	static class LruCache<K,V> extends LinkedHashMap<K,V>{

        private static int CAPACITY = 10;

        public LruCache(){
            super(16, 0.75f, true);
        }

        /**
         * 緩存計數
         */
        private AtomicInteger count = new AtomicInteger(0);

        @Override
        protected boolean removeEldestEntry(Map.Entry eldest) {

            if(count.getAndIncrement() >= CAPACITY ){
                return true;
            }
            return false;
        }
    }

當實際存儲的元素數量大於 10 時,將會踢出最老的元素,爲了達到 LRU ,需要在訪問順序的模式下構造 LinkedHashMap;這種實現最終會導致 int 類型溢出,可以使用如下這個類來代替:

public class AtomicPositiveInteger extends Number {


    private static final long serialVersionUID = -3038533876489105940L;

    private static final AtomicIntegerFieldUpdater<AtomicPositiveInteger> indexUpdater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicPositiveInteger.class, "index");

    private volatile int index = 0;

    public AtomicPositiveInteger() {
    }


    public final int getAndIncrement() {
        // 溢出時做 與 運算,
        return indexUpdater.getAndIncrement(this) & Integer.MAX_VALUE;
    }

    public final int get() {
        return indexUpdater.get(this) & Integer.MAX_VALUE;
    }

    public final void set(int newValue) {
        if (newValue < 0) {
            throw new IllegalArgumentException("new value " + newValue + " < 0");
        }
        indexUpdater.set(this, newValue);
    }

    @Override
    public byte byteValue() {
        return (byte) get();
    }

    @Override
    public short shortValue() {
        return (short) get();
    }

    @Override
    public int intValue() {
        return get();
    }

    @Override
    public long longValue() {
        return (long) get();
    }

    @Override
    public float floatValue() {
        return (float) get();
    }

    @Override
    public double doubleValue() {
        return (double) get();
    }

    @Override
    public String toString() {
        return Integer.toString(get());
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + get();
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof AtomicPositiveInteger)) return false;
        AtomicPositiveInteger other = (AtomicPositiveInteger) obj;
        return intValue() == other.intValue();
    }
}

上面這種方式,能夠保證不溢出(重新歸爲0),但是無法恢復到該有的容量大小,比如 10,如果要做到這步,應該需要同步了。


總結

從實際問題入手分析源碼,思路將會清晰很多,並就新的問題再次分析,直到分析完所有問題。這種類似遞歸般的學習方法還是很不錯的。

LinkedHashMap 的排序是由雙向鏈表維護的,鏈表的元素類型爲 LinkedHashMap.Entry<K,V>,底層維護了一個 head 和 一個 tail 記錄這個雙向鏈表。

LinkedHashMap 的排序分爲:

  1. 插入排序:按照插入時的順序遍歷
  2. 訪問排序:在不進行任何訪問操作前(即未更改雙向鏈表),按照插入的順序遍歷;在進行訪問操作,例如 get 或者 put 一個已經存在的值 的時候,會調整雙向鏈表,最近訪問的會放在鏈表的末尾。

可以發現,HashMapLinkedHashMap 的設計實現採用了大量模板方法模式,也就是父類在方法實現中調用未實現方法(或者說留給子類拓展的),子類實現方法,以此來改變行爲。

具體總結如下:

  • 在插入數據時,LinkedHashMap 重寫 newNode 方法,來修改頭尾節點,維護雙向鏈表。
  • 在存數據時(put 操作),
    • 當存放已經存在的 key 值時,會通過實現 afterNodeAccess 方法來調整訪問順序模式下的雙向鏈表。
    • HashMap 預留 afterNodeInsertion 回調方法,LinkedHashMap 實現了該方法, 插入數據時,判斷是否需要移除最老的元素(雙向鏈表頭)。這有點像個驚喜,我覺得 LinkedHashMap 完全可以不做這個。
  • 在刪除數據時,LinkedHashMap 實現 afterNodeRemoval 方法,來調整雙向鏈表。

推薦博文


手撕Java類HashMap

手撕HashMap迭代器

手撕HashMap紅黑樹


手撕系列讓我很有食慾啊!!!


我與風來


認認真真學習,做思想的產出者,而不是文字的搬運工
錯誤之處,還望指出

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