1. 程式人生 > >Java進階之----LinkedHashMap原始碼分析

Java進階之----LinkedHashMap原始碼分析

最近事情有點多,今天抽出時間來看看LinkedHashMap的原始碼,其實一開始是想分析TreeMap來這,但是看了看原始碼之後,決定還是等過幾天再分析,原因是TreeMap涉及到了樹的操作。。而之前沒有接觸過樹的這種資料結構,只是在學校學一點皮毛而已。。所以我還是打算過幾天先惡補一下相關的知識再來對TreeMap做分析。

言歸正傳,我們今天來看LinkedHashMap。從名字上我們可以看出來,這個對插入的值是保持順序的,即我們插入的順序就是我們輸出的順序,如果不相信,我們可以用HashMap和LinkedHashMap,按相同的順序插入相同的值,最後看輸出的結果,就可以知道他們的區別了。

我們首先來看LinkedHashMap的繼承結構

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

我們可以看到,LinkedHashMap是直接繼承了HashMap的,所以在一定程度上來說,他們兩個是一樣的。只不過LinkedHashMap重寫了HashMap的一些方法。從而達到了輸出有順序的目的。

看我之前的一篇博文http://blog.csdn.net/zw0283/article/details/51177547  大家應該對HashMap有一個大致的認識。而LinkedHashMap與HashMap在主要邏輯實現上並無差異,最大的不同,就是LinkedHashMap比HashMap多維護了一個連結串列,這個多出來的連結串列,就是存放我們插入順序資訊的。

這裡我們在看一下LinkedHashMap的內部Entry例項的結構


有了上邊的結構圖,對下邊原始碼的理解也更容易一些,好,我們開始分析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;
}

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

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



5個構造方法,也是夠多的。。不過我們看到,大部分都是呼叫父類的構造方法,也就是HashMap的構造方法,這裡我就不在贅述了,大家可以參考我上一篇博文。

我們還看到,在構造方法裡多了一個boolean變數accessOrder,這是什麼鬼?

看原始碼中的註釋我們可以知道,這個變數是控制輸出的順序的,一共有兩種順序:

1、按插入順序輸出,類似於佇列,先進去的先出來

2、按LRU順序,何為LRU,就是最近最少使用,打個比方,我們插入值A、B、C、D,如果這樣插入的話,那輸出的時候就是A、B、C、D,看起來好像跟第一種沒什麼分別。那我們在測試,插入A、B、C、D、A,我們在輸出的時候,發現輸出變成了B、C、D、A。A為什麼跑到後邊去了?這是因為,A被插入了2次,而LRU最近最少使用,所以A的使用頻率要高於BCD,要將使用頻率高的放到後邊,使用頻率小的放到前邊。

還有一個不得不提的問題就是,在HashMap中,我們看到有一個空實現的init方法,這個方法在HashMap中沒什麼用,它的作用是留給子類覆蓋的,也就是說,在LinkedhashMap構造方法中,呼叫super的構造方法後,還會呼叫自身的重寫後的init方法,體現了Java的多型性。

我們來看看被重寫後的init方法

void init() {
        header = new Entry<>(-1, null, null, null);
        header.before = header.after = header;
}
大致就是構建了一個頭結點,然後將改節點的前繼和後繼都指向自己,構成了一個單節點的雙向連結串列。

我們再來看看Entry,即map的內部資料結構

private static class Entry<K,V> extends HashMap.Entry<K,V>{
    // 增加了前繼和後繼,構成雙向連結串列,按插入順序儲存
    Entry<K,V> before, after;
}


根據上邊的Entry結構圖來看,應該更容易理解一些。

我們在前邊已經知道了LinkedHashMap多了一個連結串列來保證輸出順序,我們就來看看LinkedHashMap是怎麼實現的。

在看程式碼之前,我們先猜測一下這個LinkedHashMap的資料結構,有了資料結構圖之後,相信理解程式碼也會十分容易。既然繼承了HashMap,那整體結構肯定和HashMap一樣了,只是細節上有變動,我們大膽猜測一下


上邊的圖是我參照網上的答案,並結合自己的想法畫出來的,網上都是把上邊這個圖拆成了2個,畫是好畫了,但是很容易產生誤導(反正對我來說有誤導)我完全看不懂拆成2個之後的對應關係,索性我就直接自己用Visio畫了一個,畫的不好看,大家見諒。。

很容易看出來,細的藍色箭頭,是原來HashMap裡本身就有的,而粗的箭頭,則是LinkedHashMap新增了,每個Entry例項旁邊的數字代表的是插入的順序。我們可以想象一下,如果我們用左手捏住head節點,右手捏住任意節點,用力拽開,會發現這些Entry例項會變成一個環狀結構(注意:其實在第6個Entry例項和Head節點處還有一個雙向箭頭,這裡為了不引起混淆就沒有畫,但實際上是有的)就像這樣。(大家注意,我下邊這個其實是雙向箭頭,為了方便我就弄成了單向的。。)


配合上邊幾個圖,相信大家應該對LinkedHashMap的結構有一個瞭解了,那我們現在注意來分析一下被LinkedHashMap重寫的幾個方法。

recordAccess方法分析

我們通過原始碼可以知道,LinkedHashMap並沒有直接重寫put方法,而是重寫了put方法裡呼叫的一些方法,笨一點的方法就是,在HashMap裡put呼叫的每一個方法,都去LinkedhashMap裡看一看有沒有重寫。。。(話說我就是這麼找的。。)

// LinkedHashMap重寫的方法,HashMap裡為空實現
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            // 判斷一下要用哪種順序,LRU還是正常的順序,LRU要做特別操作,而正常順序留不需要操作了
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
}
我們跟進去看看remove方法
private void remove() {
       before.after = after;
       after.before = before;
}
這個不難理解,假設現在有ABC,B為當前物件,則第一句為將A的後繼指向為B的後繼,即C。第二句將B的後繼C的前繼設為B的前繼,即A。所以最後變成了AC,當前物件B就被刪除掉了。

為什麼要刪除呢?我們在來看看addBefore方法

private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }
結合上邊的圖我們在來看這個程式碼,recordAccess呼叫addBefore傳入的引數是當前Map的head節點,所以從這個方法名我們可以看出它要做的是將當前節點(this)加入到head之前,但是,別忘了,LinkedHashMap內部維護的是迴圈雙向連結串列,所以加入到head之前,意思就是加到連結串列的結尾。(不知道有沒有表述明白,反正我看的時候在這繞了好半天。。)

我用一張圖為大家說明一下,在展示圖之前,各位要先明白一些問題。就是這個recordAccess是在什麼地方呼叫的?我們看HashMap的put方法可知,是在有重複key的時候才呼叫的。

/****************HashMap**************/
// 遍歷該位置的連結串列,如果有重複的key,則將value覆蓋
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

所以,現在很清楚了,當有重複的key時,相當於“使用頻率”增加了,若使用普通的順序,則不需要做什麼,若使用LRU演算法的話,就需要把使用頻率高的放到後邊,自然,使用頻率小的就到前邊了。所以才會有上邊的recordAccess方法。

我們來看看圖就明白了






transfer方法分析

transfer方法是當Entry陣列需要擴容時呼叫的。我們來看原始碼中transfer方法的註釋:

 /**
     * Transfers all entries to new table array.  This method is called
     * by superclass resize.  It is overridden for performance, as it is
     * faster to iterate using our linked list.
     */
    @Override
    void transfer(HashMap.Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e = header.after; e != header; e = e.after) {
            if (rehash)
                e.hash = (e.key == null) ? 0 : hash(e.key);
            int index = indexFor(e.hash, newCapacity);
            e.next = newTable[index];
            newTable[index] = e;
        }
    }

重寫這個方法的原因主要是為了優化,因為LinkedHashMap內部有一個連結串列,做查詢的時候,相對於HashMap的遍歷方式,重寫後的遍歷連結串列在效率上要高於原來的處理。不過做的事情都是一樣的。將原來的資料轉存到一個新的數組裡。只不過遍歷的方式不一樣而已。

get方法分析

get方法相對來說就簡單了許多,這裡把原始碼列出,不在過多贅述,注意的是,取出操作也會觸發連結串列位置的調整。

public V get(Object key) {
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }