天天看點

Java LinkedHashMap類源碼解析

LinkedHashMap繼承了HashMap,他在HashMap的基礎上增加了一個雙向連結清單的結構,連結清單預設維持key插入的順序,重複的key值插入不會改變順序,适用于使用者需要傳回一個順序相同的map對象的情況。還可以生成access-order順序的版本,按照最近通路順序來存儲,剛被通路的結點處于連結清單的末尾,适合LRU,put get compute merge都算作一次通路,其中put key值相同的結點也算作一次通路,replace隻有在換掉一個鍵值對的時候才算一次通路,putAll産生的通路順序取決于原本map的疊代器實作。

在插入鍵值對時,可以通過對removeEldestEntry重寫來實作新鍵值對插入時自動删除最舊的鍵值對

擁有HashMap提供的方法,疊代器因為是通過周遊雙向連結清單,是以額外開銷與size成正比與capacity無關,是以選擇過大的初始大小對于周遊時間的增加沒有HashMap嚴重,後者的周遊時間依賴與capacity。

同樣是非線程安全方法,對于LinkedHashMap來說,修改結構的操作除了增加和删除鍵值對外,還有對于access-order時進行了access導緻疊代器順序改變,主要是get操作,對于插入順序的來說,僅僅修改一個已有key值的value值不是一個修改結構的操作,但對于通路順序,put和get已有的key值會改變順序。疊代器也是fail-fast設計,但是fail-fast隻是一個調試功能,一個設計良好的程式不應該出現這個錯誤

因為HashMap加入了TreeNode,是以現在LinkedHashMap也有這個功能

 以下描述中的連結清單,若無特别說明都是指LinkedHashMap的雙向連結清單

先來看一下基本結構,每個鍵值對加入了前後指針,集合加入了頭尾指針來形成雙向連結清單,accessOrder代表連結清單是以通路順序還是插入順序存儲

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

    /**
     * The head (eldest) of the doubly linked list.頭部
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.尾部
     */
    transient LinkedHashMap.Entry<K,V> tail;

    //true通路順序 false插入順序
    final boolean accessOrder;           

然後是幾個内部方法。linkNodeLast将p連接配接到連結清單尾部

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;//原本連結清單為空則p同時為頭部
        else {
            p.before = last;
            last.after = p;
        }
    }           

transferLinks用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;
    }           

reinitialize在調用HashMap方法的基礎上,将head和tail設為null

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

newNode生成一個LinkedHashMap結點,next指向e,插入到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);//建立一個鍵值對,next指向e
        linkNodeLast(p);//p插入到LinkedHashMap連結清單末端
        return p;
    }           

replacementNode根據原結點生成一個LinkedHashMap結點替換原結點

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);//生成一個新的鍵值對next是給出的next參數
        transferLinks(q, t);//用t替換q
        return t;
    }           

newTreeNode生成一個TreeNode結點,next指向next,插入到LinkedHashMap連結清單末端

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);//生成一個TreeNode,next指向參數next
        linkNodeLast(p);//p插入到LinkedHashMap連結清單末端
        return p;
    }           

replacementTreeNode根據結點p生成一個新的TreeNode,next設為給定的next,替換原本的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);//根據結點p生成一個新的TreeNode,next設為給定的next,替換原本的p
        return t;
    }           

afterNodeRemoval從LinkedHashMap的鍊上移除結點e

void afterNodeRemoval(Node<K,V> e) { 
        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;
    }           

afterNodeInsertion可能移除最舊的結點,需要evict為true同時連結清單不為空同時removeEldestEntry需要重寫

void afterNodeInsertion(boolean evict) { 
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {//removeEldestEntry需要重寫才從發揮作用,否則一定傳回false
            K key = first.key;//移除連結清單頭部的結點
            removeNode(hash(key), key, null, false, true);
        }
    }           

afterNodeAccess在通路過後将結點e移動到連結清單尾部,需要Map是access-order,若移動成功則增加modCount

void afterNodeAccess(Node<K,V> e) { 
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {//Map是access-order同時e不是連結清單的尾部
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)//将結點e從連結清單中剪下
                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;//結點e移動到連結清單尾部
            ++modCount;//因為有access-order下結點被移動,是以增加modCount
        }
    }           

構造函數方面,accessOrder預設是false插入順序,初始大小為16,負載因子為0.75,這裡是同HashMap。複制構造也是調用了HashMap.putMapEntries方法

containsValue周遊連結清單尋找相等的value值,這個操作一定不會造成結構改變

public boolean containsValue(Object value) {
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {//檢查同樣是根據LinkedHashMap提供的連結清單順序進行周遊
            V v = e.value;
            if (v == value || (value != null && value.equals(v)))
                return true;
        }
        return false;
    }           

get方法複用HashMap的getNode方法,若找到結點且Map是通路順序時,要将通路的結點放到連結清單最後,若沒找到則傳回null。而getOrDefault僅有的差別是沒找到時傳回defaultValue

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)//複用HashMap的getNode方法
            return null;
        if (accessOrder)
            afterNodeAccess(e);//access-order時将e放到隊尾
        return e.value;
    }

    public V getOrDefault(Object key, V defaultValue) {
       Node<K,V> e;
       if ((e = getNode(hash(key), key)) == null)
           return defaultValue;//複用HashMap的getNode方法,若沒有找到對應的結點則傳回defaultValue
       if (accessOrder)
           afterNodeAccess(e);//access-order時将e放到隊尾
       return e.value;
   }           

clear方法在HashMap的基礎上要把head和tail設為null

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

removeEldestEntry在put和putAll插入鍵值對時調用,原本是一定傳回false的,如果要自動删除最舊的鍵值對要傳回true,需要進行重寫。比如下面這個例子,控制size不能超過100

private static final int MAX_ENTRIES = 100;

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

下面兩個方法和HashMap相似,傳回key的Set和value的Collection還有傳回鍵值對的Set,這個是直接引用,是以對它們的remove之類的修改會直接回報到LinkedHashMap上

public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new LinkedKeySet();
            keySet = ks;
        }
        return ks;//傳回key值的set
    }

    public Collection<V> values() {
        Collection<V> vs = values;
        if (vs == null) {
            vs = new LinkedValues();
            values = vs;
        }
        return vs;//傳回一個包含所有value值的Collection
    }

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;//傳回一個含有所有鍵值對的Set
    }           

檢查HashMap的putVal方法,我們可以看到在找到了相同key值并修改value值時會調用afterNodeAccess,對于access-order會改變結點順序

if (e != null) { // 找到了相同的key則修改value值并傳回舊的value
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }