1. 程式人生 > >Java集合類原始碼解析:LinkedHashMap

Java集合類原始碼解析:LinkedHashMap

前言

今天繼續學習關於Map家族的另一個類 LinkedHashMap 。先說明一下,LinkedHashMap 是繼承於 HashMap 的,所以本文只針對 LinkedHashMap 的特性學習,跟HashMap 相關的一些特性就不做進一步的解析了,大家有疑惑的可以看之前的博文。

深入解析

LinkedHashMap的基本結構

首先,看一下LinkedHashMap類的定義結構:

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

它繼承了 HashMap

,並實現了Map介面,所以,LinkedHashMap的資料結構和HashMap非常相似,都是散列表的結構,同時繼承了很多HashMap的成員變數和方法,例如載入因子,容量,桶等,但在細節上卻有些許不同,比如:

  • HashMap中 ‘’桶“ 的連結串列結點是單向的結點,而LinkedHashMap 中的連結串列結點多出了前後的指向屬性,所以LinkedHashMap 中桶的連結串列是雙向連結串列;
  • HashMap中的連結串列只做資料儲存,LinkedHashMap 的連結串列控制儲存順序;
  • HashMap桶的連結串列產生是因為產生hash碰撞,所有資料形成連結串列 (紅黑樹) 儲存在一個桶中,LinkedHashMap 中雙向連結串列會串聯所有的資料,也就是說有桶中的資料都是會被這個雙向連結串列管理。

這些區別正是我們專門費勁學習LinkedHashMap 的原因,不然直接用HashMap完了,省事。

下面開始一一深入瞭解。

LinkedHashMap的實體類Entry

LinkedHashMap 類中有專門為雙向連結串列的結點作為載體的實體類Entry,它繼承了HashMap中的Entry,並加入了兩個屬性before, after ,用於指向前後結點的指標。

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

因為這兩個指標,桶中的連結串列才能實現雙向連結串列的功能,將所有的節點已連結串列的形式串聯一起來 ,這是LinkedHashMap 的結構圖 (摘自 Java集合之LinkedHashMap):

新的屬性

LinkedHashMap繼承了HashMap的所有非private屬性,同時也多了幾個新的屬性,分別是

//雙向連結的頭結點,最久的
transient LinkedHashMap.Entry<K,V> head;

//雙向連結的尾結點,最新的
transient LinkedHashMap.Entry<K,V> tail;

//true表示最近最少使用次序(LRU),false表示插入順序
final boolean accessOrder;

看完三個屬性後,我們再來看看預設的構造方法:

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

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

可以看到,LinkedHashMap的構造方法都是預設呼叫了父類的構造方法,並且幾乎都是把屬性accessOrder 賦值為false,除了第五個將其作為引數初始化,也就是說,預設情況下,LinkedHashMap建立物件都是採用插入順序的方式來維持鍵值對的次序的。

插入順序解析

下面通過具體的程式碼來展示 LinkedHashMap 的預設插入順序效果

public class Test {

    public static void main(String[] args) {
        LinkedHashMap map = new LinkedHashMap<Integer,Integer>();
        for (int i = 1; i<=5;i++){
            map.put(i,i);
        }
        System.out.println("正常輸出=="+map.toString());
        map.put(6,6);
        System.out.println("插入元素=="+map.toString());
    }
}
//結果
正常輸出=={1=1, 2=2, 3=3, 4=4, 5=5}
插入元素=={1=1, 2=2, 3=3, 4=4, 5=5, 6=6}

可以看出,當插入新元素時,容器會預設把元素放到最後,這是為什麼呢?

我們點開put原始碼進行檢視,發現直接跳到了HashMap的put方法,也就是說,LinkedHashMap 類中沒有對 put() 做具體的實現,直接複用了父類的方法,這是HashMap中的方法原始碼:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
  }
  
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

在HashMap的原始碼中,初始化容器時呼叫了newNode(),這個方法在 LinkedHashMap 中做了過載,也是其能實現插入順序保證的關鍵,下面看具體的原始碼:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    //祕密是這裡初始化的是自己的Entry類,然後呼叫linkNodeLast
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

跟蹤 linkNodeLast 方法,

// link at the end of list,把節點連線到連結串列尾處
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    //把new的Entry給tail
    tail = p;
    //若沒有last,說明p是第一個節點,head=p
    if (last == null)
        head = p;
    else {
    //否則把節點放到連結串列尾處
        p.before = last;
        last.after = p;
    }
}

到這裡就非常清晰了,LinkedHashMap 在插入元素時會呼叫自己過載的newNode() 方法,new一個自己的Entry方法並把節點放到連結串列結尾處,這也是它能實現插入元素順序放置的原因。

LRU演算法的實現

前面說到,LinkedHashMap 建立預設的例項可以實現插入順序的保證效果,它預設初始化的成員變數 accessOrder 的值是false的,如果傳入accessOrder 為true,那麼就啟用LRU演算法,下面給個例子演示下:

import java.util.LinkedHashMap;

public class Test {

    public static void main(String[] args) {
       Map map = new LinkedHashMap<Integer,Integer>(20,0.75f,true);
        for (int i = 1; i<=5;i++){
            map.put(i,i);
        }
        System.out.println("正常輸出=="+map.toString());
        map.get(3);
        System.out.println("讀取元素=="+map.toString());
        map.put(6,6);
        System.out.println("插入元素=="+map.toString());
    }
}
//輸出結果
正常輸出=={1=1, 2=2, 3=3, 4=4, 5=5}
讀取元素=={1=1, 2=2, 4=4, 5=5, 3=3}
插入元素=={1=1, 2=2, 4=4, 5=5, 3=3, 6=6}

可以看到,當初始化的例項時傳入值為 trueaccessOrder 時,不管是插入元素還是讀取元素,都是將最近用到的元素放到最後,這是因為 在put 和 get方法中都做了特定的處理。

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    //為true,呼叫afterNodeAccess方法
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

afterNodeAccess的原始碼解析:

//將最近使用的Node,放在連結串列的最末尾
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    //僅當按照LRU原則且e不在最末尾,才執行修改連結串列,將e移到連結串列最末尾的操作
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //將p的後一個結點置為null,因為執行後p在末尾,後一個結點肯定為null
        p.after = null;
        //p的前結點不存在,把頭結點設定為a
        if (b == null)
            head = a;
        else
        //如果b不為null,那麼b的後節點指向a
            b.after = a;
        //如果a節點不為空,a的後節點指向b
        if (a != null)
            a.before = b;
        else
        //如果a為空,那麼b就是尾節點
            last = b;
        //尾節點為null,p直接作為頭節點
        if (last == null)
            head = p;
        else {
        //否則就把p作為尾節點
            p.before = last;
            last.after = p;
        }
        //把p賦值給雙向連結串列的尾節點
        tail = p;
        ++modCount;
    }
}

所以,當呼叫這個方法的時候,就會將節點設定到連結串列的尾節點,從而也就達到了LRU的效果。

同理,在put方法中也呼叫了這個方法,不過LinkedHashMap 沒有自己的put方法,直接呼叫的是父類中的方法,在父類的方法中也呼叫了 afterNodeAccess() 方法。

不過,在HashMap中,afterNodeAccess方法並沒有任何實現,LinkedHashMap中過載了該方法,所以,當呼叫put插入元素時,其實也會呼叫LinkedHashMap 的afterNodeAccess方法。

除此之外,LinkedHashMap還有很多過載的方法,限於篇幅就不一一介紹了。

總結

最後說明一下,LinkedHashMap是HashMap的一個子類,其特殊實現的僅僅是儲存了記錄的插入順序,所以在Iterator迭代器遍歷LinkedHashMap時先得到的鍵值是先插入的,然而,由於其儲存沿用了HashMap結構外還多了一個雙向順序連結串列,所以在一般場景下遍歷時會比HashMap慢,此外具備HashMap的所有特性和缺點。

所以,除非是對插入順序讀取比較嚴格的情況,否則不建議用LinkedHashMap,一般情況下,HashMap足以滿足我們的日常使用。