LinkedHashMap实现LRU缓存

本文介绍了如何利用Java中的LinkedHashMap实现LRU(最近最少使用)缓存策略。LinkedHashMap在HashMap的基础上增加了链表结构,通过重写afterNodeAccess、afterNodeInsertion和afterNodeRemoval方法,实现节点的访问、插入和删除操作。当缓存满时,会删除最近最少使用的节点。此外,文章还探讨了get、put方法的实现,以及containsValue方法的改进,提供了两种不同的LRU实现方式。
摘要由CSDN通过智能技术生成

LinkedHashMap实现LRU缓存

  最近在面试的过程中,遇到了需要设计能实现LRU缓存的数据结构。不考虑时间复杂度,可以使用LinkedList实现,但是缓存需要考虑时间复杂度,所以,需要再加上HashMap的结构,这样就能实现增、查的时间复杂度都是O(1)。
  JDK的集合框架中正好提供了一个这样的数据结构:LinkedHashMap,它继承了HashMap。在HashMap中有三个方法是没有实现的:

  1. afterNodeAccess:访问节点之后调用的方法
  2. afterNodeInsertion:插入节点之后调用的方法
  3. afterNodeRemoval:删除节点之后调用的方法
// Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }  //访问节点之后调用的方法
    void afterNodeInsertion(boolean evict) { } //插入节点之后调用的方法
    void afterNodeRemoval(Node<K,V> p) { }  //删除节点之后调用的方法

  这三个方法都是在put、get、remove方法中回调的方法。在学习HashMap的时候,我并没有注意到这三个方法,现在才知道他们的重要性,目前这三个方法只在LinkedHashMap中实现了。
  LinkedHashMap默认是按照节点的插入顺序,即先进先出,但是通过实例化时设置参数(AccessOrder = true),可以修改为按访问顺序进出。

  LRU:最近最少访问,实现原理是:

  • 当访问元素时,先在HashMap中找到该节点,再将该节点移动到链表的末尾;
  • 当添加元素时,先在HashMap中定位,将节点插入到HashMap中,同时将节点插入到链表的末尾;
  • 因为缓存是有大小的,如果插入的节点数目超过了缓存大小,就需要删除最近最少使用的节点,即在HashMap中删除节点,同时删除链表的表头节点;

1,afterNodeAccess(Node<K,V> p) { } //访问节点之后调用的方法

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

  看过HashMap源码的同学应该知道,这个方法在put方法中被调用,具体是在当put()是更新操作时,即put的key已经存在。这个方法的具体作用就是:在对节点进行访问之后,会更新链表,将节点移动到链表的尾部,表示最近被访问过。细心的同学会问,那调用get方法进行访问的时候,该怎么办呢?是的,因为HashMap的get方法并没有回调这个方法,所以LInkedHashMap自己实现了get方法(下面介绍)。

2,afterNodeInsertion(boolean evict) { } //插入节点之后调用的方法

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

  这个方法是在HashMap中的put方法中被调用,具体是当put方法是添加操作时,即put的key不存在。这个方法的具体作用是:在插入新节点后,因为缓存不够,需要删除最近最少使用的节点。
  这里的删除操作还是HashMap中实现的removeNode方法,只是在removeNode方法中调用了 afterNodeRemoval方法(下面介绍)。这里需要用户实现removeEldestEntry(first)方法,即如果需要进行删除的话,将这个方法重写。

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

  这个方法默认是返回false,即不进行删除操作。用户如果需要的话,就可以重写该方法,根据自己的条件返回true即可,比如return size() > cacheSize。

3,afterNodeRemoval(Node<K,V> p) { } //删除节点之后调用的方法

void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

  因为在HashMap的removeNode方法中,只是删除了HashMap中的节点,并没有在链表中删除。所以在removeNode中,回调了这个方法,将该节点从链表中删除(这里是删除的头结点,因为头结点是最早进入或者最近最久未使用的)。

get和put方法

LinkedHashMap自己并没有重写put方法,上面已经介绍过。但是为了访问,所以LinkedHashMap自己实现了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中的方法,这里只是多了一个判断:if(accessOrder),即如果需要按照访问顺序进行迭代,就调用afterNodeAccess方法,将节点移动到链表的末尾。

containsValue方法有改进

在LinkedHashMap中,containsValue时间复杂度更低,因为使用了链表,所以时间复杂度只有O(n),而HashMap中使用了双重循环。

/**
*LinkedHashMap中
*/
public boolean containsValue(Object value) {
    for (LinkedHashMap.Entry<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中
*/
public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
        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;
}

在这里插入图片描述

代码实现LRU1:类似内部类

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

/**
 * Created by on 19-4-13
 */
public class LRUCache2 {
    public static void main(String[] args) {
        final int cacheSize = 3;
        Map<String, Integer> map = new LinkedHashMap<String, Integer>((int) Math.ceil(cacheSize / 0.75f) + 1,
                0.75f, true){
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
                return size() > cacheSize;
            }

            @Override
            public String toString() {
                for (Map.Entry<String, Integer> map : entrySet()){
                    System.out.print(String.format("%s:%s ", map.getKey(), map.getValue()));
                }
                System.out.println();
                return null;
            }
        };
        map.put("政治", 5);
        map.put("语文", 1);
        map.put("英文", 3);
        System.out.println(map.toString());
        map.get("政治");
        map.put("地理", 6);
        System.out.println(map.toString());
    }
}

运行结果:
在这里插入图片描述

LRU实现2:标准实现

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

/**
 * Created by on 19-4-12
 */
public class LRUCache<K, V> extends LinkedHashMap<K, V>{
    private final int MAX_CACHE_SIZE;

    public LRUCache(int cacheSize){
        super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
        MAX_CACHE_SIZE = cacheSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > MAX_CACHE_SIZE;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for(Map.Entry<K, V> entry : entrySet()){
            sb.append(String.format("%s:%s ", entry.getKey(), entry.getValue()));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        LRUCache<String, Integer> lruCache = new LRUCache<>(3);
        lruCache.put("政治", 5);
        lruCache.put("语文", 1);
        lruCache.put("英文", 3);
        System.out.println(lruCache.toString());
        lruCache.get("政治");
        lruCache.put("地理", 6);
        System.out.println(lruCache.toString());
    }
}

运行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值