1. 程式人生 > >LinkedHashMap原始碼分析及實現LRU演算法

LinkedHashMap原始碼分析及實現LRU演算法

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

一、簡單介紹

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

LinkedHashMap

可以看到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);
    }
}

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