LinkedHashMap原始碼分析及實現LRU演算法
PS: 要先了解HashMap的實現原理HashMap原始碼分析
一、簡單介紹
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
可以看到LinkedHashMap繼承了HashMap,其實際是在HashMap基礎上,把資料節點連成一個雙向連結串列。具體做法是,給節點新增兩個成員欄位before、after,遍歷的時候按連結串列順序遍歷。
小總結
預設的LinkedHashMap 的遍歷會按照插入的順序遍歷出來,HashMap則不保證順序。
注意上面是預設的情況,LinkedHashMap中還有個accessorder成員標誌,預設是false,當為true時,每get一個元素,都會把這個元素放在連結串列最後,即遍歷的時候就變成最後被遍歷出來。
二、原始碼分析
把map裡面的node練成一個連結串列(雙向的),把第一個插入的資料作為連結串列頭,遍歷從表頭開始。
//通過繼承HashMap.Node類,新增兩個成員,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);
}
}
類成員
//指向連結串列的頭結點
transient LinkedHashMap.Entry<K,V> head;
//指向連結串列的尾節點
transient LinkedHashMap.Entry<K,V> tail;
//為false時,遍歷會按插入的順序遍歷
//為true時,每一次get操作,都會把獲得的節點放到連結串列尾
//預設為false
final boolean accessOrder;
接下來就是分析從增刪查來讀LinkedHashMap原始碼。
void afterNodeAccess (Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
這三個方法是父類HashMap留給子類實現的方法,分別在get、put、remove方法完成後呼叫
1. 新增操作
一開始筆者想找put方法,但發現其並沒有重寫父類的put方法,轉去找Entry類在哪裡使用到。找到以下兩個方法。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);//連線到連結串列尾部
return p;
}
//紅黑樹結構時用到
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);//連線到連結串列尾部
return p;
}
上面兩個方法是重寫父類的,父類HashMap的put方法中,如果是一個當前不存在的新的節點,就會根據不同結構分別呼叫上面兩個方法來建立新節點,這樣就能把父類HashMap裡的節點換成LinkHashMap中的通過“改裝”的節點了。
接下來看linkNodeLast()方法操作實現連結串列
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;//舊的尾節點
tail = p;//賦值新的尾節點
if (last == null)
head = p;//這時候說明連結串列為空
else {
p.before = last;//指向舊的尾節點
last.after = p;//舊的尾節點下一個節點指向新的尾節點
}
}
就是把新增的節點放在連結串列最後,如果連結串列一開始為空,那就賦值給頭結點
再看看afterNodeInsertion()方法
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
由於removeEldestEntry()方法總是返回false,所以該方法等於沒有做任何操作。
小總結
LinkedHashMap的新增操作通過父類HashMap來完成,它則是通過定以義一個Entry繼承父類Node的,然後利用Entry實現了連結串列的結構。
2. 查操作
2.1 get操作
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);//調整連結串列
return e.value;
}
getNode()方法在父類HashMap中實現了,LinkedHashMap重用其來查詢資料,然後根據accessOrder的值來決定是否需要調整連結串列.
下面看afterNodeAccess()方法的做法
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
//進入條件:accessOrder為true,且尾節點不等於e
LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e,
b = p.before, a = p.after;
//p就是節點e,b是節點的前節點,a是e的後節點
p.after = null;//先把p的節點賦值為null
if (b == null)//說明p是頭結點
head = a;
else
b.after = a;//把p的前節點連上p的後節點
if (a != null)//這裡肯定不為null吧??
a.before = b;//把p的後節點連上p的前節點
else
last = b;
if (last == null)
head = p;
else {
p.before = last;//把p的前節點指向尾節點
last.after = p;//把舊尾節點的後節點指向p
}
tail = p;//尾節點賦值為p
++modCount;
}
}
afterNodeAccess()方法做的就是,把傳入節點e,移動到連結串列尾部。
2.2 遍歷操作
來看看其遍歷如何實現,挑entrySet方法來說,先回顧下遍歷寫法
Iterator it = map.entrySet().iterator();
while(it.hasNext())
it.next();
追蹤其原始碼如下
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() {
return nextNode();
}
}
可以看到map.entrySet().iterator()這一句程式碼返回的是LinkedEntryIterator物件。而其next()方法,則是呼叫了nextNode()方法,該方法在父類LinkedHashIterator中實現了
LinkedHashIterator() {
next = head;//next一開始指向連結串列頭結點
expectedModCount = modCount;
current = null;
}
final LinkedHashMap.Entry<K,V> nextNode() {
//next一開始是指向head(即指向連結串列頭)
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)//快速失敗
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;//next指向下一個節點
return e;
}
可以看到遍歷如前文所說,是遍歷連結串列。
3. 刪除操作
LinkedHashMap沒有重寫remove方法,而是重寫了afterNodeRemoval方法
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e,
b = p.before, a = p.after;
//把p的前後指向賦值為null
p.before = p.after = null;
if (b == null)//說明是頭結點
head = a;
else
b.after = a;//p原來的前節點指向p原來的後節點
if (a == null)//說明p是尾節點
tail = b;
else
a.before = b;//p原來的後節點指向p原來的前節點
}
LinkedHashMap重用了HashMap的remove方法,然後在afterNodeRemoval方法中刪除連結串列中相應的節點
三、實現LRU演算法
利用LinkedHashMap的資料結構特性,可以簡便地實現LRU演算法。
class LRUCache {
private LinkedHashMap<Integer,Integer> data;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
//這裡要指定第三個引數為true
data=new LinkedHashMap<>(capacity,1,true);
}
public int get(int key) {
//get()方法會自動調整連結串列
Integer o = data.get(key);
return o!=null?o:-1;
}
public void put(int key, int value) {
//這裡要呼叫一次get方法,一來可以檢查當前key是否存在
//二來可以調整其在連結串列位置的位置
if(data.get(key)==null&&data.size()==capacity){
data.remove(data.keySet().iterator().next());
}
data.put(key,value);
}
}
這裡只展示了實現插入整數,可改進為泛型使其更加通用。