LinkedHashMap 源碼分析

前言

前面對 HashMap 的源碼作了分析,咱們知道 HashMap 內部的數據結構是數組+單鏈表/紅黑樹實現的,這種數據結構是不能保證數據插入的有序性的,由於會對傳入的 key 作 hash 運算,而後再作取模運算,經過鏈表指向的方法去存儲數據,這樣就致使了遍歷數據的時候沒法根據咱們存入的順序來讀取。css

而 LinkedHashMap 從新實現了內部鏈表節點,爲每一個節點增長了前置節點和後置節點,從而實現一個雙向鏈表解決了 HashMap 無序的問題,而且能夠在建立 LinkedHashMap 的時候設置 accessOrder 這個 final 類型的常量來控制訪問內部元素的時候是根據插入順序(值爲false)仍是根據訪問順序(值爲 true)的,若是是 true 就會在每次調用 get() 方法的時候移動數據,最近訪問的元素下次優先訪問。java

LinkedHashMap 的介紹

先看下介紹: node

能夠看到 LinkedHashMap 繼承於 HashMap 的,他繼承了 HashMap 的方法和特性,好比內部的默認初始容量、默認負載因子、擴容機制等。可是 LinkedHashMap 在此基礎上又進行了擴展,主要提供了元素的訪問順序上進行了增強。數組

官方的描述:緩存

Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map. (A key k is reinserted into a map m if m.put(k, v) is invoked when m.containsKey(k) would return true immediately prior to the invocation.)安全

使用哈希表和鏈表實現了 Map 接口,具有有序的迭代特性,這種實現和 HashMap 不一樣的是內部使用了一個貫穿全部條目的雙向鏈表,這個鏈表定義了迭代排序,一般是按照插入 Map 時的順序,若是在調用 put 方法插入值的時候,若是 Map 中存在值,則覆蓋原來的值,若是不存在則執行插入。數據結構

這裏列下 LinkedHashMap 的特性:框架

  1. 容許key 爲 null ,value 爲 null
  2. key 不能夠重複,value 能夠重複
  3. 內部是以數組+雙向鏈表實現的,JDK8 之後加入了紅黑樹
  4. 內部鍵值對的存儲是有序的(須要注意初始化的時候 accessOrder 屬性的設置)。
  5. 初始容量爲 16,負載因子爲 0.75,也就是當容量達到 容量*負載因子 的時候會擴容,一次擴容增長一倍。
  6. 內部的鍵值對要重寫 key 對象重寫 hashCode 方法、equals 方法。
  7. 線程不安全的,若是想要線程安全,使用
    1. SynchronizedMap 使用 Map<String, String> map1 = Collections.synchronizedMap(new LinkedHashMap<String, String>(16,0.75f,true)); 來得到一個線程安全的 LinkedHashMap,SynchronizedMap內部也是使用的 Synchronized 實現線程安全的

LinkedHashMap 分析

前面講了 LinkedHashMap 是幾繼承於 HashMap 的,實現了雙向鏈表,而且經過accessOrder 能夠控制訪問順序,來看下是怎麼實現的:函數

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
    // 雙向鏈表的頭結點
    transient LinkedHashMapEntry<K,V> head;
    //雙向鏈表尾結點
    transient LinkedHashMapEntry<K,V> tail;
    // 控制訪問順序,若是爲 true 順序爲訪問順序,若是爲 false,爲插入順序
    final boolean accessOrder;
}
複製代碼

這裏在 HashMap 基礎上多擴展了三個字段,分別雙向鏈表的頭結點 head,尾節點 tail,和控制訪問順序的 accessOrder ,先看下頭節點和尾節點的類 LinkedHashMapEntry:源碼分析

/** * 繼承於 HashMap Node 節點的 LinkedHashMap 的條目 * HashMap.Node subclass for normal LinkedHashMap entries. */
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
    // 定義了當前節點的前置節點 before 和後置節點 after
    LinkedHashMapEntry<K,V> before, after;
    LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
複製代碼

能夠看到 LinkedHashMapEntry 在 HashMap 的 Node 節點上多增長了 兩個屬性:

  • 節點前置節點 指向上一個節點
  • 節點後置節點 指向下個節點

這裏可能會有疑問,HashMap.Node 節點已經有了 next 屬性,這裏爲何還要定義 after 屬性?

這是由於 next 屬性裏面保存的是一個哈希桶位置的當前鏈表的當前節點的下個節點應用,是在一個鏈表內的,咱們能夠經過哈希桶和單鏈表的 next 快速的找出要查找的元素。

而 after 也是執行下個節點的,不一樣的是,這裏的 after 能夠經過不一樣的哈希桶位置將不一樣的節點關聯起來。

這個圖理解下:

黑色 before 指向 藍色 after 指向 紅色 next 指向

before 和 after 將每一個節點所有串聯了起來,是保證 LinkedHashMap 順序的關鍵。

構造方法

// 定義初始化容量和負載因子,默認按照插入順序排序
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    //定義初始化容量、使用默認負載因子,默認按照插入順序排序
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    //使用默認初始化容量、使用默認負載因子,默認按照插入順序排序
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    //傳入一個 Map 初始化,默認按照插入順序排序
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

    // 定義初始化容量和負載因子,使用訪問順序排序
    public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
複製代碼

能夠看到,每一個構造方法都初始化了 accessOrder ,這個 accessOrder 是final 類型的,一經賦值不能修改,而且是個關鍵屬性,因此只能在構造函數才能保證必定能初始化。

存入數據

LinkedHashMap 裏面沒有重寫 put 方法,繼續使用的 HashMap 的 put 方法。

那 LinkedHashMap 是怎麼在插入新數據的時候保證插入的順序的呢?

咱們前面講了,LinkedHashMap 內部的元素多了兩個屬性來保證順序訪問的,那麼確定是在新建節點的時候對不一樣節點間進行了關聯。在 HashMap 的 putval 方法裏面建立新節點是使用 newNode 方法的,天然 LinkedHashMap 只須要重寫下 newNode 方法便可。

// LinkedHashMapEntry 是繼承於Node 的
 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
     LinkedHashMapEntry<K,V> p =
         new LinkedHashMapEntry<K,V>(hash, key, value, e);
     linkNodeLast(p);
     return p;
 }
複製代碼

主要分爲三步:建立 p 節點,關聯節點,返回節點 建立 p 節點卻是沒什麼特別的操做,依舊和 HashMap 裏面的同樣,主要的操做在關聯節點 linkNodeLast 中:

// 把節點關聯到鏈表尾部
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    // 拿到尾節點賦值給 臨時變量 last
    LinkedHashMapEntry<K,V> last = tail;
    // 把建立的節點賦值給尾節點。
    tail = p;
    // 若是以前的尾節點爲空,說明鏈表爲空,頭節點也賦值爲 p
    if (last == null)
        head = p;
    // 不爲空,說明有值,新節點的 before 指向之前的尾節點
    // 之前的尾節點的 after 指向 新節點
    else {
        p.before = last;
        last.after = p;
    }
}
複製代碼

能夠看到,在 linkNodeLast() 函數的內部給節點的 before 和 after屬性賦值,也就把節點給串聯了起來。

因此 LinkedHashMap 保證插入順序的關鍵就是在 newNode 方法裏面,也是比 HashMap 多的一些操做都在裏面。

你可能在 HashMap 的 putVal 方法裏面發現了這個方法 afterNodeInsertion:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
複製代碼

這裏的 afterNodeInsertion 在 HashMap 裏面是空方法,專門爲 LinkedHashMap 準備的,與之相似的還有兩個方法:

// Callbacks to allow LinkedHashMap post-actions
// 在新增、替換、查找的時候,若是 accessOrder 爲 true 會執行。
void afterNodeAccess(Node<K,V> p) { }
// 新增元素後會在 LinkedHashMap 中調用
void afterNodeInsertion(boolean evict) { }
// 刪除元素後會在 LinkedHashMap 中調用
void afterNodeRemoval(Node<K,V> p) { }
複製代碼

afterNodeInsertion 方法

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
    //若是頭節點不爲 null,根據removeEldestEntry返回值判斷是否移除最近最少被訪問的節點
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

// 移除最近最少被訪問條件之一,經過覆蓋此方法可實現不一樣策略的緩存
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}
複製代碼

刪除數據

和 put 方法同樣,LinkedHashMap 沒有實現 remove 方法,依舊調用的就是 HashMap 的 remove 方法。

咱們知道, LinkedHashMap 不只在新增的時候改變節點的 before 和 after 的值,也要在刪除的時候也要改變節點的 before 和 after 的值。那是怎麼實現的呢?

在 HashMap 源碼裏面有:

final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
                // 省略部分代碼
                afterNodeRemoval(node);
                return node;
                // 省略部分代碼
}
複製代碼

這裏的 afterNodeRemoval 前面講了,在 HashMap 裏面是空方法,專門爲 LinkedHashMap 準備的。

afterNodeRemoval 方法

// HashMap 的 remove 方法中調用,傳入刪除的節點
void afterNodeRemoval(Node<K,V> e) { // unlink
    // 把刪除節點 e 賦值給 p 
    // b 刪除節點的前置節點
    // a 刪除節點的後置節點
    LinkedHashMapEntry<K,V> p =
        (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
    // 首先將刪除節點的引用置爲 null
    p.before = p.after = null;
    // 若是刪除節點前置節點是空,說明是頭節點,刪除後後置節點設置爲頭節點
    if (b == null)
        head = a;
    // 若是刪除節點前置節點不是空,將刪除節點的前置節點的 after 指向 刪除節點的後置節點 
    else
        b.after = a;
    // 若是是尾節點,則將刪除節點的前一個指向節點置爲 尾節點
    if (a == null)
        tail = b;
    // 若是是尾節點,將刪除節點的後置節點的前指向節點指向 刪除節點的前置節點
    else
        a.before = b;
}
複製代碼

這樣就完成了一次刪除操做。

獲取操做

LinkedHashMap 重寫了 get 方法:

public V get(Object key) {
    Node<K,V> e;
    // 經過 getNode 查找節點
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
複製代碼

首先經過,getNode 查找節點,可是在 LinkedHashMap 方法中並無這個 getNode 方法,可見是調用的 HashMap 的 getNode 方法,能夠去HashMap 源碼分析 裏面去看下 getNode 方法的實現,這裏再也不介紹。

須要注意的是,在調用 getNode 方法之後,若是 accessOrder 爲 true ,會接着調用 afterNodeAccess 方法,這個方法前面講了,是在 HashMap 中定義,在 LinkedHashMap 中實現的:

afterNodeAccess 方法

void afterNodeAccess(Node<K,V> e) { // move node to last
    // 最後一個節點
    LinkedHashMapEntry<K,V> last;
    // 若是 accessOrder 爲 true,而且 尾節點不是 e,進入 if 代碼塊
    if (accessOrder && (last = tail) != e) {
        // 把刪除節點 e 賦值給 p 
        // b 刪除節點的前置節點
        // a 刪除節點的後置節點
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        // 將當前節點的後置節點引用置爲 null,由於要把 當前節點做爲尾節點,尾節點的後置節點爲 null
        p.after = null;
        // 若是當前節點是頭節點,將 a 做爲頭節點。
        if (b == null)
            head = a;
        // 將當前節點的前置和後置節點關聯
        else
            b.after = a;
        // 若是 當前節點的後置節點不爲尾節點,將當前節點的後置節點和前置節點關聯 
        if (a != null)
            a.before = b;
        //當前節點是尾節點 ,將 b 做爲尾節點
        else
            last = b;
        // 若是尾節點爲 null,頭節點也設置爲當前節點 ,由於鏈表就一個節點 
        if (last == null)
            head = p;
        //不然 更新當前節點p的前置節點爲 原尾節點last, last的後置節點是p
        else {
            p.before = last;
            last.after = p;
        }
        // 將當前節點設置爲尾節點,
        tail = p;
        ++modCount;
    }
}
複製代碼

一句話描述就是:把當前操做的節點移到最後,做爲尾節點。

這樣的話,就會改變咱們插入時候的元素的順序,也就實現了按照訪問和插入實現元素的排序。

遍歷

看過源碼的可能注意到了,在 LinkedHashMap 內部定義了一些內部類,分別爲:

能夠看到 LinkedKeyIterator、 LinkedValueIterator、LinkedEntryIterator 都是繼承於 LinkedHashIterator 迭代器,在 next 方法返回對應迭代器的值。

來看下被是哪一個類繼承的 LinkedHashIterator 幹了些啥:

abstract class LinkedHashIterator {
    // 下個節點
    LinkedHashMapEntry<K, V> next;
    //當前節點
    LinkedHashMapEntry<K, V> current;
    //期待的修改次數
    int expectedModCount;

    LinkedHashIterator() {
        //初始化 next 爲頭節點,默認從頭節點開始
        next = head;
        //使用 HashMap 的 modCOunt
        expectedModCount = modCount;
        //當前爲 null
        current = null;
    }

    // 若是 next 頭節點 不爲空,就說明 LinkedHashMap 內部有值.返回 true
    public final boolean hasNext() {
        return next != null;
    }

    // 查找下個節點
    final LinkedHashMapEntry<K, V> nextNode() {
        LinkedHashMapEntry<K, V> e = next;
        //判斷fail-fast
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        //若是要返回的節點是null,異常
        if (e == null)
            throw new NoSuchElementException();
        // 就是一個使用臨時變量 e 把next 賦值給 current,而後 next 表明current 的下個節點.
        current = e;
        next = e.after;
        return e;
    }

    //移除遍歷的節點,調用 HashMap 中的方法。
    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;
    }
}
複製代碼

那這些是作什麼的呢?

其實這裏的幾個迭代器都是爲 LinkedHashMap 裏面的keySet()、values()、entrySet()方法準備的,就拿 entrySet 來說:

public Set<Map.Entry<K, V>> entrySet() {
    Set<Map.Entry<K, V>> es;
    return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
複製代碼

在 entrySet() 方法裏面,若是 entrySet 爲空,就建立 LinkedEntrySet 對象,來看下 LinkedEntrySet 類作了些什麼:

final class LinkedEntrySet extends AbstractSet<Map.Entry<K, V>> {
    public final int size() {
        return size;
    }
    public final void clear() {
        LinkedHashMap.this.clear();
    }
    public final Iterator<Map.Entry<K, V>> iterator() {
        return new LinkedEntryIterator();
    }
    public final boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
        Object key = e.getKey();
        Node<K, V> candidate = getNode(hash(key), key);
        return candidate != null && candidate.equals(e);
    }
    public final boolean remove(Object o) {
        if (o instanceof Map.Entry) {
            Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
            Object key = e.getKey();
            Object value = e.getValue();
            return removeNode(hash(key), key, value, true, true) != null;
        }
        return false;
    }
    public final Spliterator<Map.Entry<K, V>> spliterator() {
        return Spliterators.spliterator(this, Spliterator.SIZED |
                Spliterator.ORDERED |
                Spliterator.DISTINCT);
    }
    public final void forEach(Consumer<? super Map.Entry<K, V>> action) {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        // Android-changed: Detect changes to modCount early.
        for (LinkedHashMapEntry<K, V> e = head; (e != null && mc == modCount); e = e.after)
            action.accept(e);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}
複製代碼

能夠看到 iterator() 方法 返回了

public final Iterator<Map.Entry<K, V>> iterator() {
    return new LinkedEntryIterator();
}
複製代碼

LinkedEntryIterator 就是繼承於前面分析的 LinkedHashIterator,只是在 next 方法裏面使用調用 nextNode 方法返回 Entry 而已:

public final Map.Entry<K, V> next() {
    return nextNode();
}
複製代碼

其餘的兩種遍歷 key 或者 value 的原理相似,就不在分析。

最後

關於 LinkedHashMap 的分析就先到這裏,最後再來寫一下 LinkedHashMap 的特色:

  1. 容許key 爲 null ,value 爲 null
  2. key 不能夠重複,value 能夠重複
  3. 內部是以數組+雙向鏈表實現的,JDK8 之後加入了紅黑樹
  4. 內部鍵值對的存儲是有序的(須要注意初始化的時候 accessOrder 屬性的設置)。
  5. accessOrder 爲 true,那麼內部元素的順序會根據最近訪問方式排序,若是爲 false,就會按照元素插入的順序排序
  6. 初始容量爲 16,負載因子爲 0.75,也就是當容量達到 容量*負載因子 的時候會擴容,一次擴容增長一倍。
  7. 內部的鍵值對要重寫 key 對象重寫 hashCode 方法、equals 方法。
  8. 線程不安全的,若是想要線程安全,使用
    1. SynchronizedMap 使用 Map<String, String> map1 = Collections.synchronizedMap(new LinkedHashMap<String, String>(16,0.75f,true)); 來得到一個線程安全的 LinkedHashMap,SynchronizedMap內部也是使用的 Synchronized 實現線程安全的

有興趣的能夠去看下另外的幾篇文章:

Java經常使用集合框架

ArrayList 源碼分析

LinkedList 源碼分析

HashMap 源碼分析

相關文章
相關標籤/搜索