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,因此 hash
,key
,value
,next
都是繼承自 HashMap 的 Entry 類的,而 before
,after
則是 LinkedHashMap 特有的
需要注意的是,next
用於指定位置的雜湊陣列對應的連結串列,而 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);
}
...
}
四、建構函式
需要注意這裡的 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,即那個查詢到的鍵值對。瞭解這一點後,就能看懂了