【源碼面經】Java源碼系列-LinkedHashMap

面試題

  1. LinkedHashMap如何實現有序的
  2. 如何用LinkedHashMap實現LRU

源碼解析

LinkedHashMap在Map的基礎上進行了擴展,提供了按序訪問的能力。這個順序經過accessOrder控制,能夠是結點的插入順序,也能夠是結點的訪問時間順序。面試

LinkedHashMap還提供了removeEldestEntry方法,能夠用來刪除最老訪問結點。算法

經過accessOrder和removeEldestEntry能夠用來實現LRU緩存。緩存

LinkedHashMap

如圖所示,LinkedHashMap實現順序訪問的方法比較簡單,在HashMap實現以外,還維護了一個雙向鏈表。每當插入結點時,不只要在Map中維護,還須要在鏈表中進行維護。HashMap中的put, get等方法都提供了一些鉤子方法,如afterNodeAccessafterNodeInsertionafterNodeRemoval等。經過這些方法,LinkedHashMap能夠對這些結點進行一些特性化的維護。數據結構

當遍歷LinkedHashMap時經過遍歷鏈表代替遍歷Map中的各個槽,從而實現按序訪問。工具

底層數據結構

/** * LinkedHashMap普通的鏈表結點,繼承了HashMap的Node,在此基礎上 * 對每一個Node添加了before和after指針。LinkedHashMap在HashMap的 * 基礎上,還維護了一個雙向鏈表,鏈表中的結點就是Map中的每一個結點, * 經過此鏈表,LinkedHashMap就實現了維護結點順序的目的 */
    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);
        }
    }

    /** * 雙向鏈表的頭結點 */
    transient LinkedHashMap.Entry<K, V> head;

    /** * 雙向鏈表的尾結點 */
    transient LinkedHashMap.Entry<K, V> tail;

    /** * true-按訪問順序(最先操做過的結點靠前) * false-按插入順序遍歷(最先插入的結點靠前) * * @serial */
    final boolean accessOrder;
複製代碼

Node結點

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);
        // 建立一個key-value對時,不只要放入map中,還有放入LinkedHashMap
        // 內置的雙向鏈表中,用來維護插入順序
        linkNodeLast(p);
        return p;
    }

    Node<K, V> replacementNode(Node<K, V> p, Node<K, V> next) {
        LinkedHashMap.Entry<K, V> q = (LinkedHashMap.Entry<K, V>) p;
        LinkedHashMap.Entry<K, V> t =
                new LinkedHashMap.Entry<K, V>(q.hash, q.key, q.value, next);
        // 用t結點代替q結點在雙向鏈表中的位置
        transferLinks(q, t);
        return t;
    }

    TreeNode<K, V> newTreeNode(int hash, K key, V value, Node<K, V> next) {
        TreeNode<K, V> p = new TreeNode<K, V>(hash, key, value, next);
        linkNodeLast(p);
        return p;
    }

    TreeNode<K, V> replacementTreeNode(Node<K, V> p, Node<K, V> next) {
        LinkedHashMap.Entry<K, V> q = (LinkedHashMap.Entry<K, V>) p;
        TreeNode<K, V> t = new TreeNode<K, V>(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }
複製代碼

工具方法

// 在雙向鏈表尾部添加結點
    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;
        }
    }

    // 使用dst結點覆蓋src結點在雙向鏈表中的位置
    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;
    }
    
    /** * 每次插入新Node時,是否須要刪除最老的結點。 * * @return true-刪除最老結點,false-不刪除 */
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return false;
    }
複製代碼

構造方法

public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }


    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }


    /** * 能夠指定遍歷結點的順序 * * @param accessOrder true-按訪問順序(最先操做過的結點靠前) * false-按插入順序遍歷(最先插入的結點靠前) */
    public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
複製代碼

鉤子方法

// 重寫HashMap中提供給LinkedHashMap的鉤子方法

    /** * HashMap 調用remove方法後,會調用這個鉤子方法,e爲刪除的結點 */
    void afterNodeRemoval(Node<K, V> e) {
        // p = e; b = p.before; a = p.after;
        LinkedHashMap.Entry<K, V> p =
                (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;

        // 從雙向鏈表中刪除p結點
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

    /** * HashMap 調用put等方法後,會調用這個鉤子方法 * * @param evict false-table處於建立模式(即經過構造方法調用) */
    void afterNodeInsertion(boolean evict) {
        LinkedHashMap.Entry<K, V> first;

        // 若是map中存在元素,且須要刪除eldest元素,則從鏈表和Map中
        // 刪除雙向鏈表頭結點。removeEldestEntry在LinkedHashMap默認返回
        // false。該方法能夠用來實現LRU緩存
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

    /** * HashMap 調用put, get等方法後,會調用這個鉤子方法,更改最新訪問時間。 * 能夠用來實現LRU緩存 * * @param e 最近操做過的結點 */
    void afterNodeAccess(Node<K, V> e) {
        LinkedHashMap.Entry<K, V> last;

        // 若是accessOrder爲true,表明按最新遍歷時間維護鏈表
        // 則將e移至鏈表尾部
        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;
        }
    }
複製代碼

其餘

public boolean containsValue(Object value) {
        // 由於LinkedHashMap中使用雙向鏈表維護了全部Node,因此只須要遍歷
        // 雙向鏈表便可遍歷全部Node。而不用遍歷Map。

        for (LinkedHashMap.Entry<K, V> e = head; e != null; e = e.after) {
            V v = e.value;
            if (v == value || (value != null && value.equals(v)))
                return true;
        }
        return false;
    }

    public V get(Object key) {
        Node<K, V> e;
        // 尋找key對應結點
        if ((e = getNode(hash(key), key)) == null)
            return null;
        // 若是須要按訪問時間排序,則更新結點在雙向鏈表中的位置
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

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

    public void clear() {
        super.clear();
        head = tail = null;
    }

    public void forEach(BiConsumer<? super K, ? super V> action) {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        // 覆寫了遍歷方法,用遍歷雙向鏈表代替遍歷map,從而實現了按序遍歷。

        for (LinkedHashMap.Entry<K, V> e = head; e != null; e = e.after)
            action.accept(e.key, e.value);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
複製代碼

迭代器

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;
        }
    }
複製代碼

面試題解答

  1. LinkedHashMap如何實現有序的this

    LinkedHashMap在HashMap的基礎上,還將每一個key-value對應的Node維護在了一個額外的雙向鏈表中。spa

    LinkedHashMap經過accessOrder能夠支持按插入的順序訪問,或者按遍歷的順序訪問指針

    accessOrdercode

    • false: 按插入順序排序,map中每插入一個結點時,將這個結點同時放置在雙向鏈表的結尾
    • true: 按訪問順序排序,當操做map中的一個結點時,經過HashMap提供的鉤子方法(afterNodeAccessafterNodeInsertionafterNodeRemoval)找到這個結點在鏈表中的位置,並移動到鏈表結尾。這樣鏈表的頭結點就是鏈表最久沒有訪問過的結點

    遍歷的時候,經過便利雙向鏈表代替遍歷map的每一個槽,來實現順序訪問。cdn

  2. 如何用LinkedHashMap實現LRU

    首先分析LRU算法有哪些特性

    1. 新數據插入到鏈表尾部(表明最新訪問);
    2. 每當緩存命中(即緩存數據被訪問)則將數據移到鏈表尾部(表明最新訪問);
    3. 當鏈表滿的時候,將鏈表頭部的數據丟棄(刪除最久未訪問結點);

    在LinkedHashMap保證結點有序的狀況下,經過設置accessOrder爲true,採用按遍歷順序維護結點。

    1. put方法將結點插入到雙向鏈表尾部實現LRU特性 1;
    2. 鉤子方法afterNodeAccess實現LRU特性 2;
    3. 實現removeEldestEntry方法,刪除最久未訪問結點。實現LRU特性 3;
相關文章
相關標籤/搜索