Java基于LRU算法机制写一个缓存容器(哈希Map结合双链表的快乐编程)

 

1、前序

 

       废话就不多了,博主默认大家都已经知道什么是LRU算法了,且都知道了JDK中是有一个LinkedHashMap容器,可以稍加继承改造下就会很容易的实现一个LRU机制的缓存容器;

       本篇的重点其实不在JDK自带的LinkedHashMap容器上进行扩展,而是重点讲它实现LRU算法的思路(这个功能很隐蔽,一般不看源码不跟代码,根本就不知道它除了节点的插入和访问有序外,还可以实现满容后,再put元素,移除头节点的功能,注意是移除头节点,不是网上大多数人想当然的写的是移除尾节点!!!!,一看就是照搬过来,自己没有亲自跟源码的,简直就是误导人!),然后借鉴它的思路,我们通过手写代码的方式,来实现一个我们自己的LRUCache。

 

2、LinkedHashMap

        2.1 构造函数

 

       总过5个构造器,我们常用的就是无参默认构造器和带容量参数的构造器,即图中黄色圈框的从上到下数,2和3,最不常用的就是5,没说错吧。

        接下来,我们重点看下第5个构造器:

 

 public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
 }

        重点看下第三个参数,accessOrder如果你知道这个参数的意思,也调过这个参数设置为true时(默认false,按插入的顺序访问,也就是你put进去数据是什么顺序,你最终访问的就是什么顺序,不会因为你访问了某个数据而导致数据的顺序发生了改变,这个有点不太直白,晦涩难懂,没关系,往下看,相信你自己,你会恍然大悟的!)对LinkedHashMap添加数据的影响时,可以跳过下面的内容,如果没有用过这个参数的话,那就跟着我一起认识下,这个参数还是很关键的,我把它比作是LinkedHashMap开启LRU算法的开关,不相信是吧,来,往下看。


    2.2 看下面一段代码,请说出输出结果

 

    public static void main(String[] args) {
        Map<String,Integer> link = new LinkedHashMap<>(6);
        link.put("a",1);
        link.put("b",2);
        link.put("c",3);
        link.put("d",4);
        System.out.println(link);
        link.get("b");
        System.out.println(link);
    }

A.  两个都是 {a=1, b=2, c=3, d=4}

B.  第一个是{a=1, b=2, c=3, d=4},第二个是{a=1, c=3, d=4, b=2}


答案选:A


    2.3 再看下面一段代码,请说出输出结果

  public static void main(String[] args) {
        /**第三个参数 accessOrder,访问顺序,如果true的话, 基于访问顺序来,否则,基于插入顺序*/
        Map<String,Integer> link = new LinkedHashMap<>(6,0.75f,true);
        link.put("a",1);
        link.put("b",2);
        link.put("c",3);
        link.put("d",4);
        System.out.println(link);
        link.get("b");
        System.out.println(link);
    }

A.  两个都是 {a=1, b=2, c=3, d=4}

B.  第一个是{a=1, b=2, c=3, d=4},第二个是{a=1, c=3, d=4, b=2}


答案选:B


  2.4 分析一下accessOrder这个参数会在哪里使用

 

       上面两种方式虽然运行一下代码就知道答案了,但是,but!如果你仅停留在IDEA给你输出的答案, 自己不去想为什么这样的话,显然是说过不去的,作为一个程序猿,一个敢于说我精通Java的程序猿(吹一下还是允许的),是不允许这种一探究竟,"手撕源码"的机会从手中划走的。

         来,定位下,在哪使用的,看下面:

 

 


 

     圈出来的,如果大家看不懂是怎么实现的话,我来补个图说明下:

 

假设连续put了4个节点后,链表结构如图所示,注意是双向的!

 


 

不继续put节点了,开始访问节点b了

 


 

get(Object key)方法中,会根据accessOrder的值判断是否调用afterNodeAccess(e)方法

 


 

按顺序访问时,是如何改变链表节点顺序的(尾移法,即将访问的节点移动到链表的尾部)

 


 

       除了访问key的时候会牵扯到节点的尾移法,当然put、merage等只要是节点的操作动作都会涉及到节点的尾移法。

 

put元素时,会触发当前节点的尾移,注意put操作是发生在LinedHashMap的父类HashMap类中的

 


 

merge操作时,有可能会触发节点的尾移

 

 

       其他略


  2.5 分析完accessOrder参数的影响后,思考下

        LRU算法的理念就是将数据缓存容器中最近最少使用的元素给移除掉,想想我们刚才分析了LinkedHashMap的accessOrder参数的作用和对当前链表的节点的顺序影响后,简直细思极恐啊,LinkedHashMap这不就是把最近有在访问和使用到的节点移动到链表的尾部吗,反过来,那些待在链表头的节点不就是LRU(Least Recently Used)节点吗?如果LinkedHashMap提供当容器的容量(capacity)满了后,再put节点会触发remove头节点的方法,那不就是彻彻底底实现了一个基于LRU机制的且有序的容器了吗?

       有这个方法吗?还真有,很隐秘,不看源码的话,你都不知道还有这个方法!!!

 

 

 

移动年长(最久未使用)的Entry

 

    先不要管这个返回值是true还是false,我们先来看一下,它是在哪被调用的


 

  2.6 分析removeEldestEntry(Map.Entry<K,V> eldest)方法

 

在LinkedHashMap类中的afterNodeInsertion方法中可能被调用

 

      很明显,  当evict=true,头结点不为空,且removeEldestEntry头节点成功时(注意移除的是头节点(first),移不移除头结点,该方法的返回值参与了决策),会触发removeNode方法,这个方法想都不要想,就是干掉头结点,头结点是什么,上面说过了,就是LRU节点!!!


 

        我们再来看下,这个afterNodeInsertion(boolean evict)会在哪里调用,

        当你在LinkedHashMap中搜索它是在哪调用时,你会发现,只有方法的实现,没有方法的调用的入口处。

 

 


   

     既然LinkedHashMap中没有,我们就去他的父类HashMap中找

 

Callbacks to allow LinkedHashMap post-actions

 

      我们追踪到这里,赫然发现下面这段注释,大意就是这些方法是为了LinkedHashMap专门定义的,为了其进行相关动作的操作时回调用的

 

Callbacks to allow LinkedHashMap post-actions

   


      我们直奔主题,找到afterNodeInserting方法的调用处(共有5处)

 

总过能搜索5处地方调用了该方法,我们这里只说put操作

 


 

        其实到这里,我们已经知道如何基于LinkedHashMap来实现一个我们自己的LRU缓存了,如果你还没想明白要怎么实现的话,那上面两点(accessOrder参数和removeEldestEntry方法)我算是白讲那么多了,废话不多说,比葫芦画瓢,我们先看一下其他我们常用的第三方包里面有没有实现LRU算法的缓存容器:

 

 


       

         随便整开一个,还是spring模块中带的瞧一瞧

 

 


 

3、基于JDK现有LinkedHashMap实现LRU缓存

 

     我们模仿一个还不行吗,来,上代码

     精简版:

public class LRUCache extends LinkedHashMap<String,Integer>{

    private int maxSize;

    public LRUCache(int maxSize){
        super(maxSize,0.75f,true);
        this.maxSize = maxSize;
    }

    @Override
    public Integer get(Object key) {
        return super.getOrDefault(key,-1);
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
        return size() > maxSize;
    }
    
}

 


     注释+测试版:

package com.appleyk.leetcode.LRU算法实现;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * <p>基于JDK现有的LinkedHashMap类,写一个LRU(最近最少使用)机制的缓存容器</p>
 *
 * @author appleyk
 * @version V.0.1.1
 * @blob https://blog.csdn.net/appleyk
 * @date created on 13:03 2020/10/13
 */
public class LRUCache extends LinkedHashMap<String,Integer>{

    private int maxSize;

    public LRUCache(int maxSize){
        super(maxSize,0.75f,true);
        // 注意,这个赋值一定不要落下,否则你会发现put的时候,一直put空(因为,插入的节点总是会移除,卖个关子)
        this.maxSize = maxSize;
    }

    @Override
    public Integer get(Object key) {
        // 当取不到key对应的节点时,返回默认值-1
        return super.getOrDefault(key,-1);
    }


    /**
     * 这个方法在执行put操作时,会触发链表中的头结点的删除
     * 调用链:
     * 1、HashMap#put(k,v)
     * 2、HashMap#putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
     * 3、HashMap#afterNodeInsertion(evict);
     * 4、HashMap中为LinkedHashMap预留了三个方法
     * 备注:下面的注释很明确了,就是专门预留给LinkedHashMap进行回调的
     * // Callbacks to allow LinkedHashMap post-actions
     * void afterNodeAccess(Node<K,V> p) { // 在节点操作之后调用,方法作用:move node to last}
     * void afterNodeInsertion(boolean evict) { // 在节点插入之后调用,方法作用:possibly remove eldest}
     * void afterNodeRemoval(Node<K,V> p) { // 在节点移除之后调用,方法作用:消除当前节点的链接}
     * 其中afterNodeInsertion(evict)在LinkedHashMap中被实现,实现方式如下:
     * LinkedHashMap#void afterNodeInsertion(boolean evict) {// possibly remove eldest}
     * 注释很明确,就是可能会移除最近最少使用的节点(Entry)
     * 为什么说是可能呢,因为不确定是否要移除,是有条件的,接着往下看
     * LinkedHashMap#
     *  if (evict && (first = head) != null && removeEldestEntry(first)) {
     *        K key = first.key;
     *        removeNode(hash(key), key, null, false, true);
     *  }
     *  evict已经肯定是true(传过来就是),其次如果链表中头结点不为空的话,则
     *  如果LinkedHashMap#removeEldestEntry这个方法返回的是true的话,那就removeNode掉当前的头节点
     *  所以,我们要想基于LinkedHashMap实现一个LRU算法的话,就要复写该方法,当缓存容量满的时候要清除
     *  那些最近使用最少的entry了,对于LinkedHashMap来说,是移除头结点c
     *  这里有个细节需要注意,那就是一定要设置accessOrder = true,否则get的时候,无法将访问的当前节点放到链表的尾部
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
        return size() > maxSize;
    }

    public static void main(String[] args) {

        /**第三个参数 accessOrder,访问顺序,如果true的话, 基于访问顺序来,否则,基于插入顺序*/
        Map<String,Integer> link = new LinkedHashMap<>(6,0.75f,true);
        link.put("a",1);
        link.put("b",2);
        link.put("c",3);
        link.put("d",4);
        System.out.println(link);
        link.get("b");
        System.out.println(link);

        System.out.println("=================== 基于LinkedHashMap,实现LRU缓存 =====================");

        LRUCache cache = new LRUCache(2);
        cache.put("1",1); // 缓存节点1
        cache.put("2",2); // 缓存节点2
        System.out.println("不访问节点1时,链表的输出:"+cache);// {1,2}
        cache.get("1");// 访问1,则会将1节点重置(重新安放)到链表的尾部
        System.out.println("访问节点1时,链表的输出:"+cache);// 再打印的时候,此时{2,1}
        // 缓存节点3,因为cache的容量最大就是2,超过这个值后,removeEldestEntry的值为true
        // 就要触发其中afterNodeInsertion方法中的removeNode方法了,该方法将移除头结点
        // 因为,当accessOrder = true时,是按照访问顺序来的,也就是访问节点时,就会触发将节点重置到链表尾部的操作
        // 所以,当cache的容量满时,再put节点时,会触发两个操作,一个就是将当前节点插入到链表尾部,一个就是将头节点删除
        cache.put("3",3);
        System.out.println(cache.get("3") == -1 ? "找不到了":"找到了,value = "+cache.get("3"));
        System.out.println(cache.get("2") == -1 ? "找不到了":"找到了,value = "+cache.get("3"));
        System.out.println(cache.get("1") == -1 ? "找不到了":"找到了,value = "+cache.get("1"));
    }

}

        运行结果

{a=1, b=2, c=3, d=4}
{a=1, c=3, d=4, b=2}
=================== 基于LinkedHashMap,实现LRU缓存 =====================
不访问节点1时,链表的输出:{1=1, 2=2}
访问节点1时,链表的输出:{2=2, 1=1}
找到了,value = 3
找不到了
找到了,value = 1

 

 

4、基于LinkedHashMap衍生出我们自实现的LRUCache

 

        理解了LinkedHashMap是如何实现LRU算法的话,手写一个LRU缓存容器就变得不是那么难了,思路:

 

(1)数据结构使用,HashMap+双链表

     (至于为什么,因为链表的访问速度不快,需要一个个遍历,而HashMap是基于key进行hash值的,定位数据的效率要比链表快,所以借助于HashMap,可以快读命中缓存,双链表用起来很嗨皮,操作简单,只要解决了节点快速定位的问题,剩下的不管是节点新增、删除还是移动,性能是很ok的,手写的代码也不多。总之,使用HashMap+双链表的数据结构,就是一种空间换时间的概念。当然,你可以尝试用其他的数据结构组合。)

(2)get数据的时候,将当前访问的数据节点移动到链表的头部

(3)put数据的时候,如果节点不存在,直接新建一个插入到链表的头部,同时判断下当前缓存中的元素Size是否比当前的容量capacity要大,如果大了,就是缓存满了,这时候就需要进行LRU节点的移除操作了,移除谁呢,当然是尾节点啊

  (这个和LinkedHashMap的LRU实现正好相反,它是将最近常访问的节点放到了链表的尾部,而移除LRU节点时,移除的是头部节点,如果你想反着来,也是可以的,只要实现LRU算法就行,不在乎正反

(4)put数据的时候,如果节点存在,就替换该节点的val,同时将该节点移动到链表的头部 

  (这个和get同理)

 


不废话了,直接上代码

引导版(核心功能,没实现,提供思路,可以自己先不看完整版,来一遍,效果更佳):

import java.util.HashMap;
import java.util.Map;

public class LRUCache<K, V> {

    /**默认容量*/
    private static final int DEFAULT_CAPACITY = 16;
    /**缓存容量,既缓存最大可承受的存储对象的个数*/
    private int capacity;
    /**缓存元素的大小*/
    private int size = 0;
    /**缓存元素被修改的次数*/
    private int modCount = 0;
    /**构建一个Map集合,主要用来基于key快速定位Node(缓存数据),时间查找复杂度O(1)*/
    private Map<K,Node> cache = new HashMap<>(capacity);
    /**定义头、尾节点,此处两个变量只是标识(指向),并不存储数据*/
    private Node head,tail;

    /**双向链表,新增、修改、删除的效率高,配合Map使用,空间换时间*/
    class Node {

        K key;
        V val;
        /**前驱结点*/
        Node prev;
        /**后驱节点*/
        Node next;
        public Node() {}
        public Node(K key, V val) {
            this.key = key;
            this.val = val;
        }

        @Override
        public String toString() {
            return ""
        }
    }

    public LRUCache(){
        this(DEFAULT_CAPACITY);
    }

    public LRUCache(int capacity){
        this.capacity = capacity;
        head = new Node();
        tail = new Node();
        // 构建双向链表,头后驱节点指向尾,尾前驱节点指向头
        head.next = tail;
        tail.prev = head;
    }


    /**
     * 基于key取缓存数据
     * @param key 缓存key
     * @return V 返回缓存数据
     */
    public V get(K key){
        // 1、先从map缓存中查找key对应的node

        // 2、如果存在的话,干一件事情,把当前正在被使用的节点移动到头部
        moveToHead(node);
        return node.val;
    }

    /***
     * 缓存数据
     * @param key 键
     * @param val 值
     */
    public void put(K key,V val){

        // 1.首先要先从cache中取key对应的Node

        // 2.如果等于空的话,走创建,并put进cache
        if(node == null){
            // 2.1 创建新的节点
            node = new Node(key,val);
            // 2.2 放进去
            cache.put(key,node);
            // 2.3 把最近添加的(新增)node,添加到头部
            addToHead(node);
            // 2.4 别忘了size+1
            size++;

            // 2.5 判断当前size是否超过最大capacity
            if(size > capacity){
                // 2.5.1 将尾部的节点移除掉
                Node tail = removeTail();
                // 2.5.2 链表移除后,Map中也移除下
                cache.remove(tail.key);
                // 2.5.3 别忘了,缓存数据的个数减1
                size--;
            }

        }else{
            // 3.如果node存在,替换下当前的值
            node.val = val;
            // 4.将当前node移动到头部
            moveToHead(node);
        }
    }

    public int size(){
        return cache.size();
    }

    /**
     * 从cache里遍历数据(不保证顺序)
     * @return String {a=1,b=2,....}
     */
    @Override
    public String toString() {
        return  "";
    }

    /**
     * 从当前双向链表中遍历节点
     * @return String {a=1,b=2,....}
     */
    public String list() {
        return "";
    }

    /**
     * 在头部添加一个node
     * @param node 缓存节点(数据)
     */
    public void addToHead(Node node){

        // 1、保存下头的后驱节点(即可以称作带有数据的链表中的第一个(first)节点)

        // 2、将头的后驱节点重定向到当前节点
  
        // 3、设置当前节点的前驱节点为头结点
   
        // 4、设置当前节点的后驱节点为之前头节点的后驱节点
 
        // 5、设置first的前驱节点为当前节点(这一点一定不要忽略了!!!,否则会抛NPE)
  
    }

    /**
     * 将node移动到头部
     * @param node 缓存节点(数据)
     */
    public void moveToHead(Node node){
        // 1、先重置当前节点的前后驱节点
        removeNode(node);
        // 2、在将当前节点添加到头部
        addToHead(node);
    }

    /**
     * 对当前node进行移动(移动的话,需要断开前驱和后驱的指向)
     * @param node 缓存节点(数据)
     */
    public void removeNode(Node node){
        // 1、当前节点的前驱节点的后驱节点指向其后驱节点

        // 2、当前节点的后驱节点的前驱节点指向其前驱节点
      
    }

    /**
     * 移动最近最久未使用的节点,即尾节点!!!
     * @return Node 返回(被移除)的尾节点
     */
    public Node removeTail(){
        // 1、直接找到尾节点
        Node last = tail.prev;
        // 2、移动该节点
        removeNode(last);
        System.out.println("** 节点:"+last+"被移除! **");
        return last;
    }
    
}

 


 最终完整版:

package com.appleyk.leetcode.手写LRU缓存;

import java.util.HashMap;
import java.util.Map;

/**
 * <p>实现方式哈希Map+双向链表</p>
 *
 * @author appleyk
 * @version V.0.1.1
 * @blob https://blog.csdn.net/appleyk
 * @date created on  11:05 下午 2020/10/13
 */
public class LRUCache<K, V> {

    /**默认容量*/
    private static final int DEFAULT_CAPACITY = 16;
    /**缓存容量,既缓存最大可承受的存储对象的个数*/
    private int capacity;
    /**缓存元素的大小*/
    private int size = 0;
    /**缓存元素被修改的次数*/
    private int modCount = 0;
    /**构建一个Map集合,主要用来基于key快速定位Node(缓存数据),时间查找复杂度O(1)*/
    private Map<K,Node> cache = new HashMap<>(capacity);
    /**定义头、尾节点,此处两个变量只是标识(指向),并不存储数据*/
    private Node head,tail;

    /**双向链表,新增、修改、删除的效率高,配合Map使用,空间换时间*/
    class Node {
        K key;
        V val;
        /**前驱结点*/
        Node prev;
        /**后驱节点*/
        Node next;
        public Node() {}
        public Node(K key, V val) {
            this.key = key;
            this.val = val;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "key=" + key +
                    ", val=" + val +
                    '}';
        }
    }

    public LRUCache(){
        this(DEFAULT_CAPACITY);
    }

    public LRUCache(int capacity){
        this.capacity = capacity;
        head = new Node();
        tail = new Node();
        // 构建双向链表,头后驱节点指向尾,尾前驱节点指向头
        head.next = tail;
        tail.prev = head;
    }


    /**
     * 基于key取缓存数据
     * @param key 缓存key
     * @return V 返回缓存数据
     */
    public V get(K key){
        // 1、先从map缓存中查找key对应的node
        Node node = cache.get(key);
        if(node == null){
            return null;
        }
        // 2、如果存在的话,干一件事情,把当前正在被使用的节点移动到头部
        moveToHead(node);
        return node.val;
    }

    /***
     * 缓存数据
     * @param key 键
     * @param val 值
     */
    public void put(K key,V val){

        // 1.首先要先从cache中取key对应的Node
        Node node = cache.get(key);
        // 2.如果等于空的话,走创建,并put进cache
        if(node == null){
            // 2.1 创建新的节点
            node = new Node(key,val);
            // 2.2 放进去
            cache.put(key,node);
            // 2.3 把最近添加的(新增)node,添加到头部
            addToHead(node);
            // 2.4 别忘了size+1
            size++;

            // 2.5 判断当前size是否超过最大capacity
            if(size > capacity){
                // 2.5.1 将尾部的节点移除掉
                Node tail = removeTail();
                // 2.5.2 链表移除后,Map中也移除下
                cache.remove(tail.key);
                // 2.5.3 别忘了,缓存数据的个数减1
                size--;
            }

        }else{
            // 3.如果node存在,替换下当前的值
            node.val = val;
            // 4.将当前node移动到头部
            moveToHead(node);
        }
    }

    public int size(){
        return cache.size();
    }

    /**
     * 从cache里遍历数据(不保证顺序)
     * @return String {a=1,b=2,....}
     */
    @Override
    public String toString() {
        // StringBuilder非线程安全,不考虑并发,使用这种方式对string操作效率快
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        for (Map.Entry<K, Node> nodeEntry : cache.entrySet()) {
            Node node = nodeEntry.getValue();
            sb.append(nodeEntry.getKey())
                    .append("=")
                    .append(node.val)
                    .append(",");
        }
        return  sb.toString().substring(0,sb.lastIndexOf(","))+"}";
    }

    /**
     * 从当前双向链表中遍历节点
     * @return String {a=1,b=2,....}
     */
    public String list() {
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        Node n = head.next ;
        // n必须是实际存储数据的节点,因此判断需要排除tail节点
        while(n!=null && n!=tail){
            sb.append(n.key)
                    .append("=")
                    .append(n.val)
                    .append(",");
            n=n.next;
        }
        return  sb.toString().substring(0,sb.lastIndexOf(","))+"}";
    }

    /**
     * 在头部添加一个node
     * @param node 缓存节点(数据)
     */
    public void addToHead(Node node){
        // 1、保存下头的后驱节点(即可以称作带有数据的链表中的第一个(first)节点)
        Node first = head.next;
        // 2、将头的后驱节点重定向到当前节点
        head.next = node;
        // 3、设置当前节点的前驱节点为头结点
        node.prev = head;
        // 4、设置当前节点的后驱节点为之前头节点的后驱节点
        node.next = first;
        // 5、设置first的前驱节点为当前节点(这一点一定不要忽略了!!!,否则会抛NPE)
        first.prev = node;
    }

    /**
     * 将node移动到头部
     * @param node 缓存节点(数据)
     */
    public void moveToHead(Node node){
        // 1、先重置当前节点的前后驱节点
        removeNode(node);
        // 2、在将当前节点添加到头部
        addToHead(node);
    }

    /**
     * 对当前node进行移动(移动的话,需要断开前驱和后驱的指向)
     * @param node 缓存节点(数据)
     */
    public void removeNode(Node node){
        // 1、当前节点的前驱节点的后驱节点指向其后驱节点
        node.prev.next = node.next;
        // 2、当前节点的后驱节点的前驱节点指向其前驱节点
        node.next.prev = node.prev;
    }

    /**
     * 移动最近最久未使用的节点,即尾节点!!!
     * @return Node 返回(被移除)的尾节点
     */
    public Node removeTail(){
        // 1、直接找到尾节点
        Node last = tail.prev;
        // 2、移动该节点
        removeNode(last);
        System.out.println("** 节点:"+last+"被移除! **");
        return last;
    }

    public static void main(String[] args) {
        LRUCache<String,Integer> lruCache = new LRUCache<>(5);
        lruCache.put("a",1);
        lruCache.put("b",2);
        lruCache.put("c",3);
        lruCache.put("d",4);
        lruCache.put("e",5);
        lruCache.get("a");//命中a,给a"续命",将a放到链表的头部
        lruCache.put("f",6);//默认缓存容量是5,再添加一个数据,就需要触发LRU机制移除尾节点(b)了
        System.out.println("遍历Map  :"+lruCache);
        System.out.println("遍历双链表:"+lruCache.list());

    }
}

 


 

最终执行效果:

 

** 节点:Node{key=b, val=2}被移除! **
遍历Map  :{a=1,c=3,d=4,e=5,f=6}
遍历双链表:{f=6,a=1,e=5,d=4,c=3}

 

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值