LinkedHashMap都有哪些小秘密?同时给LruCache 提个建议

208 阅读28分钟
原文链接: mp.weixin.qq.com

码个蛋(codeegg)第 833 次推文

作者:刁儿郎当

博客:https://www.jianshu.com/p/494246f9ae51

码妞看世界

冲突情况

上一篇《 LruCache 缓存机制,深入浅出,发现了一个源码 bug》 中我们介绍了 LruCache 的使用和原理,同时也提到了 LruCache 本质就是在维护一个 LinkedHashMap,具体为什么是 LinkedHashMap 而不是其他对象,我们通过以下几个问题来解释原因。

1. LinkedHashMap 是个什么东西?

2. LinkedHashMap 是干什么用的?

3. LinkedHashMap 怎么用?

4. LinkedHashMap 的原理是什么?怎么实现的?

1. LinkedHashMap 是个什么东西?

LinkedHashMap 是一个继承于 HashMap,实现 Map 接口且有可预知迭代顺序的键值对存储列表。与 HashMap 的不同之处在于 LinkedHashMap 多维护了一个所有键值对的双向链表,而这个双向链表就定义了迭代顺序,和访问顺序。

简单来说就是:LinkedHashMap = HashMap + 双向链表。

2. LinkedHashMap 是干什么用的?

LinkedHashMap 主要用于需要知道访问顺序的场景,比如 LruCache 中,需要按 put 顺序或者访问时间顺序进行数据维护。又或者需要使用定长的缓存。

3. LinkedHashMap 怎么用?

我们先看一个 LinkedHashMap 普通使用的例子

例 1

public class LinkedHashMapActivity extends AppCompatActivity {  LinkedHashMap<Integer, Integer> linkedHashMap;  @Override  protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      setContentView(R.layout.activity_linked_hash_map);      linkedHashMap = new LinkedHashMap<Integer, Integer>(2);      linkedHashMap.put(1, 1);      linkedHashMap.put(2, 2);      linkedHashMap.put(3, 3);      for (Map.Entry<Integer, Integer> a : linkedHashMap.entrySet()) {          Log.e("TAG", "key->" + a.getKey() + "");          Log.e("TAG", "value->" + a.getValue() + "");      }  }}2019-11-21 14:32:17.332 3310-3310/com.we.we E/TAG: key->12019-11-21 14:32:17.333 3310-3310/com.we.we E/TAG: value->12019-11-21 14:32:17.333 3310-3310/com.we.we E/TAG: key->22019-11-21 14:32:17.333 3310-3310/com.we.we E/TAG: value->22019-11-21 14:32:17.333 3310-3310/com.we.we E/TAG: key->32019-11-21 14:32:17.333 3310-3310/com.we.we E/TAG: value->3
  • 其实普通的使用中 LinkedHashMap 和 HashMap 没有任何不同,都是实例化后进行 put 数据,而初始化时我们设置的初始容量是 2,但是 put 了三个数据,实际打印的结果也是 3 条数据,因此初始化的 2 并不代表 LinkedHashMap 的最大容量。

  • 那 LinkedHashMap 和 HashMap 到底有什么不同?我们再看一个例子

例 2

public class LinkedHashMapActivity extends AppCompatActivity {  LinkedHashMap<Integer, Integer> linkedHashMap;  @Override  protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      setContentView(R.layout.activity_linked_hash_map);            linkedHashMap = new LinkedHashMap<Integer, Integer>(2) {          @Override          protected boolean removeEldestEntry(Entry eldest) {                return linkedHashMap.size() > 2;          }    };    linkedHashMap.put(1, 1);    linkedHashMap.put(2, 2);    linkedHashMap.put(3, 3);    for (Map.Entry<Integer, Integer> a : linkedHashMap.entrySet()) {        Log.e("TAG", "key->" + a.getKey() + "");        Log.e("TAG", "value->" + a.getValue() + "");        }      }    }2019-11-21 14:34:17.708 3421-3421/com.we.we E/TAG: key->22019-11-21 14:34:17.708 3421-3421/com.we.we E/TAG: value->22019-11-21 14:34:17.708 3421-3421/com.we.we E/TAG: key->32019-11-21 14:34:17.708 3421-3421/com.we.we E/TAG: value->3
  • 和第一个例子相比,在 LinkedHashMap 实例化的过程中,重写了 removeEldestEntry () 方法,并根据当前 linkedHashMap.size () 和设置容量的判断结果返回数据,明明 put 了 3 条数据,打印却只打印了最后 put 的 2 条,这一结果证实了 2 点.

  • LinkedHashMap 的容量是可控的。

  • LinkedHashMap 是有插入顺序的。

那 LinkedHashMap 访问有序是怎么体现的呢?是直接调用 get () 方法就会自动排序么?我们写代码测试一下。

例 3

public class LinkedHashMapActivity extends AppCompatActivity {  LinkedHashMap<Integer, Integer> linkedHashMap;  @Override  protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      setContentView(R.layout.activity_linked_hash_map);      linkedHashMap = new LinkedHashMap<Integer, Integer>(2) {          @Override          protected boolean removeEldestEntry(Entry eldest) {                return linkedHashMap.size() > 2;             }          };        linkedHashMap.put(1, 1);        linkedHashMap.put(2, 2);        //调用get进行排序        linkedHashMap.get(1);       for (Map.Entry<Integer, Integer> a : linkedHashMap.entrySet()) {            Log.e("TAG", "key->" + a.getKey() + "");            Log.e("TAG", "value->" + a.getValue() + "");            }        }    }2019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: key->12019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: value->12019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: key->22019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: value->2

还是以上的例子,我们在 put 完以后调用了 get (1) 再打印日志发现并没有按我们预想的顺序将 1 放在最后,因为 LinkedHashMap 按访问顺序排序默认是关闭的,可通过构造函数实例化的时候打开。比如这样:

例 4

public class LinkedHashMapActivity extends AppCompatActivity {    LinkedHashMap<Integer, Integer> linkedHashMap;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_linked_hash_map);        //主要看最后这个参数,默认是false。        linkedHashMap = new LinkedHashMap<Integer, Integer>(2,0.75f,true) {             @Override             protected boolean removeEldestEntry(Entry eldest) {                 return linkedHashMap.size() > 2;           }        };        linkedHashMap.put(1, 1);        linkedHashMap.put(2, 2);        //调用get进行排序        linkedHashMap.get(1);        for (Map.Entry<Integer, Integer> a : linkedHashMap.entrySet()) {             Log.e("TAG", "key->" + a.getKey() + "");             Log.e("TAG", "value->" + a.getValue() + "");        }   }}2019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: key->22019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: value->22019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: key->12019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: value->1

在初始化过程开启访问排序后,在通过日志发现,1 已经排在的最后,达到了访问排序的效果。

以上的几个例子是 LinkedHashMap 的基本使用,但是具体为什么能达到这种效果,接下来我们深入源码了解 LinkedHashMap 的原理。

4. LinkedHashMap 的原理是什么?怎么实现的?

4.1. 首先我们了解 LinkedHashMap 的继承关系图

实线代表继承关系,虚线代表实现接口。

  • 以上的继承关系中可以发现 LinkedHashMap 继承于 HashMap,因此 LinkedHashMap 底层也是基于链表,如果 JDK1.8 以上就是链表 + 红黑树。

  • 而 LinkedHashMap 的不同之处就在于它又多维护了一个双向链表。

4.2. LinkedHashMap 的双向链表对象都包含什么属性?

LinkedHashMap 的双向链表对象是通过 LinkedHashMapEntry 对象来维护的,具体我们看下源码。

//LinkedHashMap.LinkedHashMapEntry//继承于 HashMap.Node,获得普通链表的能力,同时通过内部属性 before, after;维护双向链表。 static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {    //用来维护双向链表。    LinkedHashMapEntry<K,V> before, after;    LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {         super(hash, key, value, next);      }  }// HashMap.Node// 链表元素对象 static class Node<K,V> implements Map.Entry<K,V> {        final int hash;        //元素的key        final K key;        //值value        V value;        //下一个元素的数据        Node<K,V> next;       ...省略若干代码    }// HashMap.TreeNode//红黑树对象,主要看继承关系,如需了解内部逻辑可通过HashMap原理进行了解。static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {    TreeNode<K,V> parent;  // red-black tree links    TreeNode<K,V> left;    TreeNode<K,V> right;    TreeNode<K,V> prev;    // needed to unlink next upon deletion    boolean red;    TreeNode(int hash, K key, V val, Node<K,V> next) {        super(hash, key, val, next);    } }

以上源码中 LinkedHashMapEntry 就是 LinkedHashMap 中的每一个键值对对象,其中通过 before, after 两个属性维护了当前键值对的一前一后两个对象。以上源码的继承关系如下图所示:

补充一点小知识

  • HashMap.TreeNode 继承 LinkedHashMap.LinkedHashMapEntry,而 LinkedHashMap.LinkedHashMapEntry 又继承 HashMap.Node,那为什么 HashMap.TreeNode 不直接继承 HashMap.Node?绕这一圈为的是什么?

  • 其实 LinkedHashMapEntry 有组成双向链表的能力,继承了它就获得了这个能力,但是 TreeNode 就多了两个用不到的引用 (LinkedHashMapEntry<K,V> before, after;);同时内存占用也会比较大,这么做是为啥?

  • 官方解释是说 TreeNode 是 Node 内存占用的 2 倍左右,但是 TreeNode 是在 HashMap 的桶中的节点足够多的时候才会使用,(JDK1.8 当一个桶中的元素超过 8 才会转换为红黑树(TreeNode),<6 又转回链表(Node)) 如果 hash 算法不是很差的话 TreeNode 使用的几率是比较低的,因此浪费那一点空间换取了组成双向链表的能力是值得的。

  • 如果 TreeNode 直接 extends Node 则需要 Node extentds LinkedHashMapEntry,Node 使用是很频繁的,这样空间浪费的就太多了。

4.3. LinkedHashMap 对象是怎么维护每一个双向链表对象的?

  • 既然 LinkedHashMap 内的每一个元素都包含其前后两个元素对象,那必然在 put 对象的过程中就进行了维护,因此我们先看下 LinkedHashMap.put 方法。

  • 因为 LinkedHashMap 继承于 HashMap,并且 LinkedHashMap 没有重写 put (),因此调用的就是父类 HashMap 的 put,我们看下源码。

//HashMap的put()方法,同时也是LinkedHashMap的put方法public V put(K key, V value) {    return putVal(hash(key), key, value, false, true);}    //直接看关键部分的代码,看我注释的部分final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {    Node<K,V>[] tab; Node<K,V> p; int n, i;    if ((tab = table) == null || (n = tab.length) == 0)        n = (tab = resize()).length;    if ((p = tab[i = (n - 1) & hash]) == null)        //调用了newNode()方法,这里用到了一个面向对象里多态的特性,而LinkedHashMap重写了newNode()方法        //如果是LindedHashMap对象调用,触发的是LinkedHashMap重写的newNode()        tab[i] = newNode(hash, key, value, null);    else {         Node<K,V> e; K k;         if (p.hash == hash &&             ((k = p.key) == key || (key != null && key.equals(k))))              e = p;         else if (p instanceof TreeNode)              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);         else {            for (int binCount = 0; ; ++binCount) {                if ((e = p.next) == null) {                   p.next = newNode(hash, key, value, null);                   if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                       treeifyBin(tab, hash);                       break;                    }                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        break;                    p = e;                }            }        //有重复的key,则用待插入值进行覆盖,返回旧值。        if (e != null) { // existing mapping for key            V oldValue = e.value;            if (!onlyIfAbsent || oldValue == null)                e.value = value;                //HashMap中该方法没做任何操作,LinkedHashMap进行了重写实现,并调用。                //该方法主要将当前节点移动到双向链表尾部,后续进行详细分析                afterNodeAccess(e);               return oldValue;           }       }        ++modCount;        if (++size > threshold)            resize();        //每次put新元素都会调用(如果是更新已有元素则不调用,因为没有新增元素,不会导致size+1)        //但是HashMap中该方法没做任何操作, LinkedHashMap进行了重写实现.       //主要用于移除最早的元素,后续详细解析        afterNodeInsertion(evict);        return null;    }    //HashMap内的newNode    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {        return new Node<>(hash, key, value, next);    }    //LinkedHashMap重写的newNode    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {         //构造带双向链表属性的对象。        LinkedHashMapEntry<K,V> p =            new LinkedHashMapEntry<K,V>(hash, key, value, e);        //双向链表维护        linkNodeLast(p);        return p;    }  private void linkNodeLast(LinkedHashMapEntry<K,V> p) {     //首先获取当前链表的最后一个元素     LinkedHashMapEntry<K,V> last = tail;     //当前插入的元素定义为最后一个元素     tail = p;     if (last == null)         //如果之前的最后元素是null,说明之前的链表就是空的,所以当前的元素是一个元素。         head = p;      else {         //如果之前的链表不是null         //put前的最后一个元素设置为当前put元素的前一个。         p.before = last;         //当前put元素设置为put前最后一个元素的下一个         last.after = p;     } }

源码有点长,可以直接看我注释部分,注释部分完全可以描述新增键值对时双向链表的维护细节了。

其实以上的大段逻辑做的事很简单。

1. 先创建新的元素,通过 hash 计算位置并存放。

2. 每次 newNode 创建元素时都连带调用 linkNodeLast () 将新插入元素的双向链表关系维护起来(新插入的放末尾)。

3. 每次 put 新元素均通过 LinkedHashMap 中重写的 afterNodeInsertion () 判断是否删除头节点元素。

//这个方法是在HashMap的代码里调用的,在put方法中调用的时候参数evict传的是truevoid afterNodeInsertion(boolean evict) { // possibly remove eldest    LinkedHashMapEntry<K,V> first;    //evict是true,(first = head)=true,而removeEldestEntry()方法默认返回的是false,因此if内的逻辑默认是不执行的。    if (evict && (first = head) != null && removeEldestEntry(first)) {       K key = first.key;       //移除链表头部的元素       removeNode(hash(key), key, null, false, true);    }}//HashMap中的removeNode方法,主要用于删除某一个元素final Node<K,V> removeNode(int hash, Object key, Object value,                               boolean matchValue, boolean movable) {   Node<K,V>[] tab; Node<K,V> p; int n, index;      ...省略若干代码}

通过以上代码发现,其实每次调用 afterNodeInsertion () 方法,只是内部的 if 判断中 removeEldestEntry () 默认返回 false ,因此移除链表头部元素的逻辑没能执行,但是可通过重写 removeEldestEntry () 方法手动返回 true, 这样可以做到每添加一个元素都移除头部的元素。

4. 如果 put 的 key 已存在则用新值覆盖并调用 LinkedHashMap 重写的 afterNodeAccess () 将新值移动到链表尾部。

void afterNodeAccess(Node<K,V> e) { // move node to last  LinkedHashMapEntry<K,V> last;  //accessOrder 是LinkedHashMap实例化的时候的入参,需手动传true该方法的逻辑才会启用。  if (accessOrder && (last = tail) != e) {     LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;     //p为当前尾节点,因此p.after = null;     p.after = null;     //p.before==null,说明p以前是头节点,但是现在需要把p放在尾节点,则以前p.after就变成了头节点。     if (b == null)           head = a;     else           //原来的顺序是b <-> p <-> a...现在p需要移动到尾部,则就变成了b <-> a...... <->p           b.after = a;     if (a != null)           //原来a.before是p,现在p移动走了,那p.before就变成了a.before           a.before = b;     else            //如果之前p就是尾节点,则将last引用p.before           last = b;     if (last == null)           //如果原来尾节点是空,则说明当前链表只有一个节点,因此head引用p           head = p;     else {         //之前尾节点不为空,因为p移动到了最后,因此p.before引用原尾节点,原尾节点.after引用p         p.before = last;         last.after = p;     }       tail = p;       //计数器自增1       ++modCount;  }}

以上复杂的操作均是处理链表移动的各种异常情况,最终目的是将 put 的元素移动到链表末端。

注意:在 afterNodeAccess () 函数中,会修改 modCount 变量,迭代 LinkedHashMap 时,如果同时查询访问数据,也会导致 fail-fast,因为迭代的顺序已经改变。

4.4. LinkedHashMap 为什么调用 get () 就会触发排序?

以上例 4 已经展示了调用 get () 排序的场景,如不记得例 4 的逻辑,可上翻查看。

老规矩吧,点看源码一顿瞅....

//LinkedHashMap重写的get方法public V get(Object key) {  Node<K,V> e;  //getNode()调用是HashMap.getNode()  if ((e = getNode(hash(key), key)) == null)      return null;  if (accessOrder)      //哈哈哈,看到这我一下子就明白他怎么做的排序了。      afterNodeAccess(e);    return e.value;}

LinkedHashMap 每次调用 get 方法,在结果返回前就触发了 afterNodeAccess (),而 afterNodeAccess () 作用就是将当前元素移动到尾节点。

4.4. LinkedHashMap 调用 remove () 后链表怎么维护?

首先我们猜下,如果是我们自己实现这个逻辑需要怎么操作?比如现在调用 remove (),删了了链表中间的一个元素,应该怎么维护?

  1. 首先我们通过当前元素前后的元素获取前后节点,然后让前后节点做关联,但是需要注意首位节点。

  2. 如果 LinkedHashMap 重写了 remove () 则在重写的方法里实现逻辑,如果调用的是 HashMap.remove () 则需要有和 afterNodeAccess () 类似的 after...() 方法进行重写实现,且 after...() 方法需在 remove () 方法中调用。

我们验证下猜想是否正确。

//HashMap.remove()public V remove(Object key) {    Node<K,V> e;    return (e = removeNode(hash(key), key, null, false, true)) == null ?        null : e.value;}//HashMap.removeNode(),核心的删除方法final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {     Node<K,V>[] tab; Node<K,V> p; int n, index;        if ((tab = table) != null && (n = tab.length) > 0 &&            (p = tab[index = (n - 1) & hash]) != null) {            Node<K,V> node = null, e; K k; V v;                        if (p.hash == hash &&                ((k = p.key) == key || (key != null && key.equals(k))))                node = p;            else if ((e = p.next) != null) {                if (p instanceof TreeNode)                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);                else {                    do {                        if (e.hash == hash &&                            ((k = e.key) == key ||                             (key != null && key.equals(k)))) {                            node = e;                            break;                        }                        p = e;                  } while ((e = e.next) != null);            }       }     if (node != null && (!matchValue || (v = node.value) == value ||                                 (value != null && value.equals(v)))) {          if (node instanceof TreeNode)              ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);          else if (node == p)               tab[index] = node.next;          else               p.next = node.next;               ++modCount;               --size;               //维护双向链表的after...()方法,在HashMap中该方法是空方法。              afterNodeRemoval(node);           return node;         }      }    return null;}    //LinkedHashMap重写的afterNodeRemoval()方法    void afterNodeRemoval(Node<K,V> e) { // unlink        LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;        //首先删除元素的前后节点引用置空        p.before = p.after = null;    if (b == null)        //如果当前元素的前节点是null,则证明当前元素原来是头节点。        //因此删除当前元素后,当前元素的后节点就变成了头节点。        head = a;    else        //如果前节点不为null,则前节点的after引用后节点。        b.after = a;    if (a == null)        //如果当前元素的后节点为null,则说明当前元素为尾节点,删除当前元素后,尾节点变成当前元素的前节点。        tail = b;    else       //尾节点不为null,尾节点的前节点引用删除元素的前节点。       a.before = b;}

通过以上源码可见跟我们猜想一致,我们简单总结下。

  1. LinkedHashMap.remove () 其实调用的是 HashMap.remove ()

  2. HashMap.remove () 中调用了 HashMap.removeNode ()。

  3. HashMap.removeNode () 中调用了 afterNodeRemoval (), 而 LinkedHashMap 中重写了 afterNodeRemoval (),因此最终的维护链表逻辑是在 LinkedHashMap.afterNodeRemoval () 中。

至此,LinkedHashMap 的继承关系,添加,获取,移除三大主要方法,以及内部的排序处理逻辑已经分析完了,HashMap 凭借强大的设计模式,让 LinkedHashMap 重写了以下 3 个必要方法就基本实现了全部功能。

  • void afterNodeAccess(Node<K,V> p) { }

  • void afterNodeInsertion(boolean evict) { }

  • void afterNodeRemoval(Node<K,V> p) { }

接下来我们针对一些 LinkedHashMap 的特性补充一点小知识。

1. LinkedHashMap 的 containsValue ()进行了重写,相对 HashMap 大幅度提升了性能。

//LinkedHashMap.containsValue()public boolean containsValue(Object value) {  //循环查找所有键值对的中和value重复的数据  for (LinkedHashMapEntry<K,V> e = head; e != null; e = e.after) {       V v = e.value;       if (v == value || (value != null && value.equals(v)))           return true;        }        return false;    }    //HashMap.containsValue()    public boolean containsValue(Object value) {        Node<K,V>[] tab; V v;      if ((tab = table) != null && size > 0) {        //双重for循环查找数据        for (int i = 0; i < tab.length; ++i) {           for (Node<K,V> e = tab[i]; e != null; e = e.next) {              if ((v = e.value) == value ||                  (value != null && value.equals(v)))                  return true;              }          }     }  return false;}

以上两个 containsValue () 方法中,LinkedHashMap 针对的是链表循环,循环的是所有有效的数据,而 HashMap 中,是双重 for 循环,先循环桶,后循环每个桶的数据,双重 for 循环这一点效率就偏低,其次 HashMap 循环的是所有的桶,但并不是所有桶都有数据。

2. LinkedHashMap.LinkedHashIterator.nextNode () 相对 HashMap.HashIterator.nextNode () 大幅度提升了性能。

// LinkedHashMap.LinkedHashIterator.nextNode()final LinkedHashMapEntry<K,V> nextNode() {  LinkedHashMapEntry<K,V> e = next;  if (modCount != expectedModCount)       throw new ConcurrentModificationException();  if (e == null)        throw new NoSuchElementException();               current = e;        next = e.after;      return e;  }//HashMap.HashIterator.nextNode()final Node<K,V> nextNode() {  Node<K,V>[] t;  Node<K,V> e = next;  if (modCount != expectedModCount)      throw new ConcurrentModificationException();  if (e == null)       throw new NoSuchElementException();  if ((next = (current = e).next) == null && (t = table) != null) {        //循环HashMap中给每一个桶        do {} while (index < t.length && (next = t[index++]) == null);        }      return e;   }

同样的道理,LinkedHashMap 循环的是有效链表数据,HashMap 循环了所有的桶,不管桶里有没有数据。

总结和一点小建议

  • LinkedHashMap 的各个维度已经分析完了,篇幅较长,如果你已经看到了这里,那么对 LinkedHashMap 就有了相对全面的了解,但是还是建议多写代码多测试,实践才是检验原理的唯一标准。

  • 上篇 LruCache 缓存机制,深入浅出,发现了一个源码 bug 中提到 LruCache 本质就是在维护一个 LinkedHashMap,且 LruCache 具备控制缓存大小的能力。 其实上文提到的 removeEldestEntry () 方法就是移除最早元素的开关方法,只要重写该方法即可控制是否移除最早数据。但 LruCache 中并没有使用。而是通过每次 put () 连带调用 trimToSize () 方法进行了手动移除数据。 其实这块可以做下改进。

相关文章:

今日问题:

今天从LinkedHashMap学到了什么!

专属升级社区:《这件事情,我终于想明白了》