LinkedHashMap源碼分析及實現LRU算法

PS: 要先了解HashMap的實現原理HashMap源碼分析

一、簡單介紹

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

LinkedHashMap

可以看到LinkedHashMap繼承了HashMap,其實際是在HashMap基礎上,把數據節點連成一個雙向鏈表,遍歷的時候按鏈表順序遍歷。

小總結

默認的LinkedHashMap 的遍歷會按照插入的順序遍歷出來,HashMap則不保證順序。

注意上面是默認的情況,LinkedHashMap中還有個accessorder成員標誌,默認是false,當爲true時,每get一個元素,都會把這個元素放在鏈表最後,即遍歷的時候就變成最後被遍歷出來。

二、源碼分析

//通過繼承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);
     }
 }

這裏繼承了父類HashMap的Node類,新增了兩個成員before,after,用於實現鏈表結構。

類成員

//指向鏈表的頭結點
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方法中,如果是一個當前不存在的key,就會根據不同結構分別調用上面兩個方法來創建新節點,這樣就能把父類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算法(leetcode的一道題)

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

這裏只展示了實現插入整數,可改進爲泛型使其更加通用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章