1. 程式人生 > >Java 集合 | LinkedHashMap原始碼分析(JDK 1.7)

Java 集合 | LinkedHashMap原始碼分析(JDK 1.7)

一、基本圖示



二、基本介紹

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
結構
  • LinkedHashMap 繼承了 HashMap
  • LinkedHashMap 實現了 Map 介面
特性
  • 底層使用拉鍊法處理衝突的散列表
  • 元素是有序的,即遍歷得到的元素順序和放進去的元素屬性一樣
  • 是執行緒不安全的
  • key 和 value 都允許為 null
  • 1 個 key 只能對應 1 個 value,1 個 value 對應多個 key

三、基本結構

// 指向雙向連結串列的頭節點
transient LinkedHashMap.Entry<K,V> head;

// 判斷是否進行重排序
final boolean accessOrder;

LinkedList 的基本結構是繼承 HashMap 的 Entry,因此 hashkeyvaluenext 都是繼承自 HashMap 的 Entry 類的,而 beforeafter 則是 LinkedHashMap 特有的

需要注意的是,next 用於指定位置的雜湊陣列對應的連結串列,而 beforeafter 則用於迴圈雙向連結串列中元素的指向

private static
class Entry<K,V> extends HashMap.Entry<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); } ... }

四、建構函式

需要注意這裡的 accessOrder 變數,該變數是用來判斷是按照插入順序進行排序(false),還是按照訪問順序進行排序(false),具體的後面會介紹。如果不特別指定該變數為 true,預設都是 false,即元素放進去什麼順序,遍歷出來就是什麼順序;如果指定為 true,則情況就不同了

注意這裡的建構函式,都是呼叫父類的建構函式

public LinkedHashMap() {
	// 呼叫 HashMap 的 建構函式
    super();
    accessOrder = false;
}

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

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

//  需要注意這個建構函式,可以自己定義 accessOrder
public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

五、增加函式

整體還是繼承的 HashMap 的 put 方法,但是裡面的 addEntry 方法進行了重寫

// HashMap 的 put 方法
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    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;
        }
    }

    modCount++;
    // LinkedHashMap 重寫的方法
    addEntry(hash, key, value, i);
    return null;
}

LinkedHashMap 中重寫了的 addEntry 方法,裡面還是呼叫了父類 HashMap 的 addEntry 方法。在 HashMap 的 addEntry 方法中,createEntry 方法已經被子類重寫了,因此這裡的 createEntry 方法呼叫的是子類中重寫的方法

// LinkedHashMap 中重寫了的方法
void addEntry(int hash, K key, V value, int bucketIndex) {
    super.addEntry(hash, key, value, bucketIndex);

    // Remove eldest entry if instructed
    Entry<K,V> eldest = header.after;
    if (removeEldestEntry(eldest)) {
        removeEntryForKey(eldest.key);
    }
}

// 父類 HashMap 的中的方法
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    
    // 這裡呼叫的是子類 LinkedHashMap 的重寫後的方法
    createEntry(hash, key, value, bucketIndex);
}

LinkedHashMap 中重寫後的方法,其中前面三步和 HashMap 中該方法的實現一樣,唯一的區別在於 LinkedHashMap 中的 addBefore 方法

void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMap.Entry<K,V> old = table[bucketIndex];
    Entry<K,V> e = new Entry<>(hash, key, value, old);
    table[bucketIndex] = e;
    // 將新的鍵值對 e,放到 header 節點之前,即在迴圈雙向連結串列最後一個元素後插入
    e.addBefore(header);
    size++;
}

LinkedHashMap 的 Entry 繼承 HashMap 的 Entry 類,因此除了之前的特性,還加上了 before 和 after 的屬性,用於指向迴圈連結串列中元素

private static class Entry<K,V> extends HashMap.Entry<K,V> {
    Entry<K,V> before, after;

    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
        super(hash, key, value, next);
    }

    private void remove() {
        before.after = after;
        after.before = before;
    }

    // 將新的 Entry 鍵值對插入到 existingEntry 鍵值對之前
    private void addBefore(Entry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }
    ...
}

直接看圖示就明白了



接下來再看 LinkedHashMap 中的擴容方法。同樣是呼叫父類 HashMap 中的擴容方法 resize,但是具體的實現就有些不同了

先看 HashMap 中的 resize 方法,其中的 transfer 方法在 LinkedHashMap 中重寫了,因此這裡呼叫 LinkedHashMap 的此方法

// 當鍵值對的個數 ≥ 閥值,並且雜湊陣列在所放入的位置不為 null,則對雜湊表進行擴容
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    // 子類重寫了該方法,因此呼叫子類的這個方法
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

HashMap 中的 transfer 方法是先遍歷舊的雜湊陣列 table,然後再遍歷 table 中對應位置的單鏈表,計算每個元素在新的雜湊陣列中的位置,然後將元素進行轉移

LinkedHashMap 中的 transfer 方法,則是直接遍歷迴圈雙向連結串列,計算每個元素在新的雜湊陣列中的位置,然後將元素進行轉移

void transfer(HashMap.Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    // 遍歷雙向連結串列,e 是雙向連結串列的第一個元素
    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 中重寫了 get 方法,但是在根據 key 找到 Entry 鍵值對的過程中,是通過呼叫 HashMap 的 getEntry 方法實現的,而在該方法中,先找到雜湊陣列的位置,然後在遍歷對應位置的單鏈表。可見,LinkedHashMap 的 get 方法不是遍歷雙向連結串列,依然還是遍歷雜湊表來獲取元素的

// LinkedHashMap 中的 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;
}

// HashMap 中的方法,先找到指定陣列下標,然後遍歷對應位置的單鏈表
final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

七、雙向連結串列的重新排序

成員變數中,有這麼一個變數 accessOrder,它有兩個作用

  • false,按照元素插入的順序排序
  • true,按照元素訪問的順序排序
private final boolean accessOrder;

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

而在 LinkedHashMap 的 get 和 put 方法中,都呼叫了同一個方法 recordAccess

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    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;
            // 如果需要覆蓋 key 和 hash 相同的元素,則會呼叫該方法
            // this 呼叫 put 方法的物件,因此 this 就是 linkedList 物件
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

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

該方法的作用,是把訪問過的元素,這裡的訪問,有兩層含義:

  • put,根據 key 修改對應元素的 value
  • get,根據 key 獲取對應元素的 value

每訪問一次,都會通過 recordAccess 方法把訪問過的元素放到雙向連結串列的最後一位。該方法在 HashMap 中是空實現,但是在 LinkedHashMap 中,是在 Entry 類中的方法,每次是通過 Entry 物件來呼叫該方法的

private static class Entry<K,V> extends HashMap.Entry<K,V> {
    Entry<K,V> before, after;

    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
        super(hash, key, value, next);
    }

    private void remove() {
        before.after = after;
        after.before = before;
    }

    private void addBefore(Entry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }

    void recordAccess(HashMap<K,V> m) {
        // 獲取傳入的 HashMap 物件,向下轉型為 LinkedHashMap 物件
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        // 如果 accessOrder 為 true
        if (lm.accessOrder) {
            lm.modCount++;
            // 將訪問後的元素刪除
            remove();
            // 將訪問後的元素移到最後一位
            addBefore(lm.header);
        }
    }
    ...
}

我們可以通過例子和圖示來看一下整個過程

@Test
public void test3() {
    LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>(16,0.75f, true);
    linkedHashMap.put("語文", 1);
    linkedHashMap.put("英語", 2);
    linkedHashMap.put("數學", 3);
    print(linkedHashMap);

    linkedHashMap.get("語文");
    print(linkedHashMap);

    linkedHashMap.put("英語", 4);
    print(linkedHashMap);
}

public static void print(LinkedHashMap<String, Integer> linkedHashMap) {
    for (Map.Entry entry: linkedHashMap.entrySet()) {
        System.out.print(entry + " ");
    }

    System.out.println();
}

結果是

語文=1 英語=2 數學=3 
英語=2 數學=3 語文=1 
數學=3 語文=1 英語=4 

可以看到,每訪問一個元素,該元素就被放到了最後一位,通過圖示在來看一下把

通過程式碼,可以看到,recordAccess 方法先呼叫查詢到鍵值對的 remove 方法將查詢到的元素刪除

然後在呼叫該鍵值對的 addBefore 方法將刪除的元素置於連結串列的最後一位

這裡我就畫了 get 方法呼叫 recordAccess 的例子,put方法也是一樣的。其實就做了兩個步驟,將查詢到的元素刪除,然後將其置於最後一位

需要注意的是,無論是 get 還是 put,都是使用物件 e 來指向查詢到的元素的,物件 e 也是 Entry 的物件,因此在呼叫 recordAccess 方法的時候,其實是用例項過後的 e 來呼叫的。之後 recordAccess 方法裡的 remove 和 addBefore 方法,呼叫他們的物件都是物件 e,即那個查詢到的鍵值對。瞭解這一點後,就能看懂了

八、參考