LruCache 之 LinkedHashMap 分析

LruCache是Android的一個(gè)內(nèi)部類,提供了基于內(nèi)存實(shí)現(xiàn)的緩存

雙向鏈表

LinkedHashMap 是 key 鍵有序的 HashMap 的一種實(shí)現(xiàn)。它除了使用哈希表這個(gè)數(shù)據(jù)結(jié)構(gòu),使用雙向鏈表來保證 key 的順序

雙向鏈表算是個(gè)很常見的數(shù)據(jù)結(jié)構(gòu),上圖中的頭節(jié)點(diǎn)的 prev、尾節(jié)點(diǎn)的 next 指向 null,雙向鏈表還有一種變種,見下圖

LinkedHashMap 就是采用的這種方式

源碼

先看源碼定義

public class LinkedHashMap<K, V> extends HashMap<K, V> {

其是繼承 HashMap,那么就會(huì)擁有 HashMap 的大部分特色,例如允許 null 鍵和 null 值,默認(rèn)容量是16,裝載因子是 0.75,非線程安全等等咯。另外,根據(jù)名字猜測(cè),也會(huì)帶有 LinkList 的特點(diǎn)。下面分析一下

定義的變量

/**
 * A dummy entry in the circular linked list of entries in the map.
 * The first real entry is header.nxt, and the last is header.prv.
 * If the map is empty, header.nxt == header && header.prv == header.
 */
transient LinkedEntry<K, V> header; // 內(nèi)部雙向鏈表的頭結(jié)點(diǎn)  

首先,請(qǐng)先看LinkedEntry這個(gè)數(shù)據(jù)結(jié)構(gòu)

LinkedEntry

/**
 * LinkedEntry adds nxt/prv double-links to plain HashMapEntry.
 */
static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
    LinkedEntry<K, V> nxt;
    LinkedEntry<K, V> prv;

    /** Create the header entry */
    LinkedEntry() {
        super(null, null, 0, null);
        nxt = prv = this;
    }

    /** Create a normal entry */
    LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
                LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
        super(key, value, hash, next);
        this.nxt = nxt;
        this.prv = prv;
    }
}

我們看到 LinkedEntry 繼承了 HashMapEntry,并且在其基礎(chǔ)上擴(kuò)展了兩個(gè)屬性:nxt 表示鏈表的下一個(gè)元素,prv表示鏈表的前一個(gè)元素。

而這個(gè) header,大家看注釋:The first real entry is header.nxt, and the last is header.prv.If the map is empty, header.nxt == header && header.prv == header. 第一個(gè)真正的 entry 是 header 的下一個(gè),最后一個(gè) entry 是 header 的前一個(gè);如果為 null,則他的前一個(gè)和后一個(gè)都等于 header,因?yàn)樗莻€(gè)環(huán)形鏈表,所以 header 的下一個(gè)也是最先加入的,header 的前一個(gè)是最后一個(gè)加入的,下面的圖可能大家就明了了

/**
 * True if access ordered, false if insertion ordered.
 */
private final boolean accessOrder;// 是否按照訪問順序

accessOrder 代表的是是否按照訪問順序,可以分為:按插入順序的鏈表,和按訪問順序的鏈表。true 表示按照訪問順序迭代,false 時(shí)表示按照插入順序。而如果要實(shí)現(xiàn) LRUCache 的 LRU 算法,就需要選擇 true。

構(gòu)造方法

/**
 * Constructs a new empty {@code LinkedHashMap} instance.
 */
public LinkedHashMap() {
    init();
    accessOrder = false;// 默認(rèn)為false,也就是插入順序
}

/**
 * Constructs a new {@code LinkedHashMap} instance with the specified
 * capacity.
 *
 * @param initialCapacity
 *            the initial capacity of this map.
 * @throws IllegalArgumentException
 *                when the capacity is less than zero.
 */
public LinkedHashMap(int initialCapacity) {//initialCapacity是初始化空間大小
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * Constructs a new {@code LinkedHashMap} instance with the specified
 * capacity and load factor.
 *
 * @param initialCapacity
 *            the initial capacity of this map.
 * @param loadFactor
 *            the initial load factor.
 * @throws IllegalArgumentException
 *             when the capacity is less than zero or the load factor is
 *             less or equal to zero.
 */
public LinkedHashMap(int initialCapacity, float loadFactor) {// loadFactor是加載因子,當(dāng)他的size大于initialCapacity*loadFactor的時(shí)候就會(huì)擴(kuò)容,他的默認(rèn)值是0.75
    this(initialCapacity, loadFactor, false);
}

/**
 * Constructs a new {@code LinkedHashMap} instance with the specified
 * capacity, load factor and a flag specifying the ordering behavior.
 *
 * @param initialCapacity
 *            the initial capacity of this hash map.
 * @param loadFactor
 *            the initial load factor.
 * @param accessOrder
 *            {@code true} if the ordering should be done based on the last
 *            access (from least-recently accessed to most-recently
 *            accessed), and {@code false} if the ordering should be the
 *            order in which the entries were inserted.
 * @throws IllegalArgumentException
 *             when the capacity is less than zero or the load factor is
 *             less or equal to zero.
 */
public LinkedHashMap(
        int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    init();
    this.accessOrder = accessOrder;
}

/**
 * Constructs a new {@code LinkedHashMap} instance containing the mappings
 * from the specified map. The order of the elements is preserved.
 *
 * @param map
 *            the mappings to add.
 */
public LinkedHashMap(Map<? extends K, ? extends V> map) {
    this(capacityForInitSize(map.size()));
    constructorPutAll(map);
}

下面看看init方法

init()

 @Override void init() {
    header = new LinkedEntry<K, V>();
}

這個(gè)方法初始化了一個(gè)header,也就是雙向環(huán)形鏈表的頭,并沒有存放任何的數(shù)據(jù)。

在HashMap的源碼中, init方法是一個(gè)空方法

 /**
 * This method is called from the pseudo-constructors (clone and readObject)
 * prior to invoking constructorPut/constructorPutAll, which invoke the
 * overridden constructorNewEntry method. Normally it is a VERY bad idea to
 * invoke an overridden method from a pseudo-constructor (Effective Java
 * Item 17). In this case it is unavoidable, and the init method provides a
 * workaround.
 */
void init() { }

LinkedHashMap 繼承 HashMap 后重寫了該方法。

初始變量和構(gòu)造方法講完后,下面按源文件順序看看它定義的一些方法

eldest()

 /**
 * Returns the eldest entry in the map, or {@code null} if the map is empty.
 * @hide
 */
public Entry<K, V> eldest() {
    LinkedEntry<K, V> eldest = header.nxt;
    return eldest != header ? eldest : null;
}

這個(gè)方法看注釋就明了:得到最老的元素,也是最先存放的元素或者如果 map 為空,就返回null。通過上面分析。我們知道最先存放的元素就是 header 的nxt。

addNewEntry()

/**
 * Evicts eldest entry if instructed, creates a new entry and links it in
 * as head of linked list. This method should call constructorNewEntry
 * (instead of duplicating code) if the performance of your VM permits.
 *
 * <p>It may seem strange that this method is tasked with adding the entry
 * to the hash table (which is properly the province of our superclass).
 * The alternative of passing the "next" link in to this method and
 * returning the newly created element does not work! If we remove an
 * (eldest) entry that happens to be the first entry in the same bucket
 * as the newly created entry, the "next" link would become invalid, and
 * the resulting hash table corrupt.
 */
@Override void addNewEntry(K key, V value, int hash, int index) {
    LinkedEntry<K, V> header = this.header;

    // Remove eldest entry if instructed to do so.
    LinkedEntry<K, V> eldest = header.nxt;
    if (eldest != header && removeEldestEntry(eldest)) {
        remove(eldest.key);
    }

    // Create new entry, link it on to list, and put it into table
    LinkedEntry<K, V> oldTail = header.prv;
    LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
            key, value, hash, table[index], header, oldTail);
    table[index] = oldTail.nxt = header.prv = newTail;
}

這個(gè)方法比 HashMap 的 addNewEntry 代碼行數(shù)要多了。看代碼可知,該方法是加入一個(gè)新的 entry,首先是找到 header,然后根據(jù)條件判斷是否移除最老元素,默認(rèn)的 removeEldestEntry() 方法返回 false

removeEldestEntry

 protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return false;
}

如果其返回了 true,就會(huì)移除最老元素。

接下來,可以看到,首先是新建了一個(gè)新的 LinkedEntry,然后在加入到 table 中,最后一行中header.prv = newTail,這回大家可能明白了 header 的前一個(gè)元素就是新元素,也是最后一個(gè)加入的。

還有一個(gè) addNewEntryForNullKey 方法,向 HashMap 看齊,是加入 null 的 entry

addNewEntryForNullKey

@Override void addNewEntryForNullKey(V value) {
    LinkedEntry<K, V> header = this.header;

    // Remove eldest entry if instructed to do so.
    LinkedEntry<K, V> eldest = header.nxt;
    if (eldest != header && removeEldestEntry(eldest)) {
        remove(eldest.key);
    }

    // Create new entry, link it on to list, and put it into table
    LinkedEntry<K, V> oldTail = header.prv;
    LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
            null, value, 0, null, header, oldTail);
    entryForNullKey = oldTail.nxt = header.prv = newTail;
}

差別就是最后的一句話

繼續(xù)看constructorNewEntry()方法

constructorNewEntry()

 /**
 * As above, but without eviction.
 */
@Override HashMapEntry<K, V> constructorNewEntry(
        K key, V value, int hash, HashMapEntry<K, V> next) {
    LinkedEntry<K, V> header = this.header;
    LinkedEntry<K, V> oldTail = header.prv;
    LinkedEntry<K, V> newTail
            = new LinkedEntry<K,V>(key, value, hash, next, header, oldTail);
    return oldTail.nxt = header.prv = newTail;
}

注釋很明了:只不過他沒有調(diào)用移除方法

下面就是get()方法

get()

/**
 * Returns the value of the mapping with the specified key.
 *
 * @param key
 *            the key.
 * @return the value of the mapping with the specified key, or {@code null}
 *         if no mapping for the specified key is found.
 */
@Override public V get(Object key) {
    /*
     * This method is overridden to eliminate the need for a polymorphic
     * invocation in superclass at the expense of code duplication.
     */
    if (key == null) {
        HashMapEntry<K, V> e = entryForNullKey;
        if (e == null)
            return null;
        if (accessOrder)//這個(gè)方法和HashMap的get方法差不多,只不過他多了一個(gè)判斷,因?yàn)長(zhǎng)inkedHashMap多了一個(gè)鏈表的維護(hù),所以他要判斷是否是按照訪問順序來維護(hù),
            makeTail((LinkedEntry<K, V>) e);
        return e.value;
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
            e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            if (accessOrder)
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }
    }
    return null;
}

該方法復(fù)寫了 HashMap 的 get 方法,并加入了自己的特色。

如果accessOrder為true(安裝訪問順序維護(hù)),就會(huì)調(diào)用下面的方法

makeTail()

/**
 * Relinks the given entry to the tail of the list. Under access ordering,
 * this method is invoked whenever the value of a  pre-existing entry is
 * read by Map.get or modified by Map.put.
 */
private void makeTail(LinkedEntry<K, V> e) {
    // Unlink e
    e.prv.nxt = e.nxt;
    e.nxt.prv = e.prv;

    // Relink e as tail
    LinkedEntry<K, V> header = this.header;
    LinkedEntry<K, V> oldTail = header.prv;
    e.nxt = header;
    e.prv = oldTail;
    oldTail.nxt = header.prv = e;
    modCount++;
}

這個(gè)方法是實(shí)現(xiàn) Lru 算法的關(guān)鍵,將元素插入到頭表前一個(gè)元素(離頭表最近的元素,也是最新的元素)。

頭兩行代碼的意思就是把元素 e 從雙向鏈表中刪除,然后將 e 添加到 header 的前面(就是 tail),相當(dāng)于把 e 又新添加到了雙向鏈表中,最后的幾行代碼就是確保整個(gè)雙向鏈表不能斷掉。

get 方法的剩余部分類似 HashMap 的 get 方法,只不過在循環(huán)查找過程中,會(huì)再次判斷訪問類型。

preModify()

@Override void preModify(HashMapEntry<K, V> e) {
    if (accessOrder) {
        makeTail((LinkedEntry<K, V>) e);
    }
}

該方法被 LinkedHashMap 重寫,還記得 HashMap 的 put 方法(LinkedHashMap 并沒有重寫該方法,所以還是用的 HashMap 的 put 方法)嗎,那里面涉及到了該方法。如果 accessOrder 為 true ,則 LinkedHashMap 的 put 效果如下

看上面這張圖咯,有沒有啥問題呢?

如果 accessOrder 為 true ,那么,在 put<k1,v1> 時(shí),就不應(yīng)該放在 header.nxt,而應(yīng)該是 entry_1 和 entry_0 顛倒一下就正確了。(為的就是保證每次 header 的 tail 是最近用到的,而 header.nxt 就是最久未使用的)

最新的元素總是屬于 header 的 pre (也就是 the last)

postRemove()

@Override void postRemove(HashMapEntry<K, V> e) {
    LinkedEntry<K, V> le = (LinkedEntry<K, V>) e;
    le.prv.nxt = le.nxt;
    le.nxt.prv = le.prv;
    le.nxt = le.prv = null; // Help the GC (for performance)
}

該方法就是去除結(jié)點(diǎn)的操作

containsValue()

/**
 * This override is done for LinkedHashMap performance: iteration is cheaper
 * via LinkedHashMap nxt links.
 */
@Override public boolean containsValue(Object value) {
    if (value == null) {
        for (LinkedEntry<K, V> header = this.header, e = header.nxt;
                e != header; e = e.nxt) {
            if (e.value == null) {
                return true;
            }
        }
        return false;
    }

    // value is non-null
    for (LinkedEntry<K, V> header = this.header, e = header.nxt;
            e != header; e = e.nxt) {//迭代鏈表
        if (value.equals(e.value)) {
            return true;
        }
    }
    return false;
}

LinkedHashMap 的 containsValue 方法是遍歷雙向鏈表,從鏈表的 header 開始查找,因?yàn)槭黔h(huán)形的,如果查找一圈還沒找到則返回false。 containsValue 從鏈表中查詢,而 HashMap 從 table 數(shù)組中查詢,進(jìn)行該操作時(shí),沒有 HashMap 快(數(shù)組比鏈表迭代快)。

clear()

public void clear() {
    super.clear();

    // Clear all links to help GC
    LinkedEntry<K, V> header = this.header;
    for (LinkedEntry<K, V> e = header.nxt; e != header; ) {
        LinkedEntry<K, V> nxt = e.nxt;
        e.nxt = e.prv = null;
        e = nxt;
    }

    header.nxt = header.prv = header;
}

清空鏈表咯

剩下的就是迭代器類了,就不貼了,感興趣的可以自己看源碼,注意:HashMap:是迭代數(shù)組,LinkedHashMap 迭代鏈表。

參考鏈接

LRUCache原理及HashMap LinkedHashMap內(nèi)部實(shí)現(xiàn)原理

Android LinkedHashMap源碼詳解

理解LinkedHashMap

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容