LruCache 之 LinkedHashMap 分析

LruCache是Android的一个内部类,提供了基于内存实现的缓存

双向链表

LinkedHashMap 是 key 键有序的 HashMap 的一种实现。它除了使用哈希表这个数据结构,使用双向链表来保证 key 的顺序

双向链表算是个很常见的数据结构,上图中的头节点的 prev、尾节点的 next 指向 null,双向链表还有一种变种,见下图

LinkedHashMap 就是采用的这种方式

源码

先看源码定义

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

其是继承 HashMap,那么就会拥有 HashMap 的大部分特色,例如允许 null 键和 null 值,默认容量是16,装载因子是 0.75,非线程安全等等咯。另外,根据名字猜测,也会带有 LinkList 的特点。下面分析一下

定义的变量

/**
 * 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; // 内部双向链表的头结点  

首先,请先看LinkedEntry这个数据结构

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,并且在其基础上扩展了两个属性:nxt 表示链表的下一个元素,prv表示链表的前一个元素。

而这个 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. 第一个真正的 entry 是 header 的下一个,最后一个 entry 是 header 的前一个;如果为 null,则他的前一个和后一个都等于 header,因为他是个环形链表,所以 header 的下一个也是最先加入的,header 的前一个是最后一个加入的,下面的图可能大家就明了了

/**
 * True if access ordered, false if insertion ordered.
 */
private final boolean accessOrder;// 是否按照访问顺序

accessOrder 代表的是是否按照访问顺序,可以分为:按插入顺序的链表,和按访问顺序的链表。true 表示按照访问顺序迭代,false 时表示按照插入顺序。而如果要实现 LRUCache 的 LRU 算法,就需要选择 true。

构造方法

/**
 * Constructs a new empty {@code LinkedHashMap} instance.
 */
public LinkedHashMap() {
    init();
    accessOrder = false;// 默认为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是加载因子,当他的size大于initialCapacity*loadFactor的时候就会扩容,他的默认值是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>();
}

这个方法初始化了一个header,也就是双向环形链表的头,并没有存放任何的数据。

在HashMap的源码中, init方法是一个空方法

 /**
 * 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 后重写了该方法。

初始变量和构造方法讲完后,下面按源文件顺序看看它定义的一些方法

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;
}

这个方法看注释就明了:得到最老的元素,也是最先存放的元素或者如果 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;
}

这个方法比 HashMap 的 addNewEntry 代码行数要多了。看代码可知,该方法是加入一个新的 entry,首先是找到 header,然后根据条件判断是否移除最老元素,默认的 removeEldestEntry() 方法返回 false

removeEldestEntry

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

如果其返回了 true,就会移除最老元素。

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

还有一个 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;
}

差别就是最后的一句话

继续看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;
}

注释很明了:只不过他没有调用移除方法

下面就是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)//这个方法和HashMap的get方法差不多,只不过他多了一个判断,因为LinkedHashMap多了一个链表的维护,所以他要判断是否是按照访问顺序来维护,
            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;
}

该方法复写了 HashMap 的 get 方法,并加入了自己的特色。

如果accessOrder为true(安装访问顺序维护),就会调用下面的方法

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++;
}

这个方法是实现 Lru 算法的关键,将元素插入到头表前一个元素(离头表最近的元素,也是最新的元素)。

头两行代码的意思就是把元素 e 从双向链表中删除,然后将 e 添加到 header 的前面(就是 tail),相当于把 e 又新添加到了双向链表中,最后的几行代码就是确保整个双向链表不能断掉。

get 方法的剩余部分类似 HashMap 的 get 方法,只不过在循环查找过程中,会再次判断访问类型。

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> 时,就不应该放在 header.nxt,而应该是 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)
}

该方法就是去除结点的操作

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 开始查找,因为是环形的,如果查找一圈还没找到则返回false。 containsValue 从链表中查询,而 HashMap 从 table 数组中查询,进行该操作时,没有 HashMap 快(数组比链表迭代快)。

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:是迭代数组,LinkedHashMap 迭代链表。

参考链接

LRUCache原理及HashMap LinkedHashMap内部实现原理

Android LinkedHashMap源码详解

理解LinkedHashMap

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容