LinkedHashMap与LRU缓存算法

LinkedHashMap 继承HashMap 所以拥有它的全部功能,但在HashMap基础上新增了双向链表的功能,可以说LinkedHashMap=HashMap+双向链表,所以保证了其元素的有序性。默认是元素插入时顺序,支持按元素访问排序,所以可以用作LRU缓存算法。

1.构造方法
 public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
 } 
public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

accessOrder表示是否根据访问次数排序,默认是false,即根据插入顺序排序

调用了父类的构造函数,并初始化了header 节点

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
     	//LinkedHashMap对此方法进行了重写
        init();
    }
    //初始化了header 节点
    void init() {
        header = new Entry<>(-1, null, null, null);
        header.before = header.after = header;
    }

img

//对entry进行了重写
private static class Entry<K,V> extends HashMap.Entry<K,V> {
       
        Entry<K,V> before, after;

        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }
        //省略部分代码
 }

从构造方法我们可以看出,LinkedHashMapHashMap的基础上,重写了Entry()新增了两个属性beforeafter用来支撑双向链表数据结构。注意这两个属性和原属性next没有任何关系 before指向上一个插入的元素,after指向后一个插入的元素。

2.put操作
//此方法是父类HashMap中的方法
public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
    	//定位到数组的下标
        int i = indexFor(hash, table.length);
        //数组中每个元素都是链表,JDK1.8后变成红黑数
        //遍历链表,如果比对KEY的hash值,hash码和equals同时相同才表示该元素存在
        //如果该元素已经存在集合中,则覆盖老值
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                //这个是一个在HashMap中是一个空方法,在LinkedHashMap中对
                //其进行了重写,后面再详细分析
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

put()是直接继承的HashMap的,在linkedHashMap中并没有定义此方法,但是重写了addEntry(hash, key, value, i);recordAccess()。我们一个个来看

 void addEntry(int hash, K key, V value, int bucketIndex) {
        super.addEntry(hash, key, value, bucketIndex);
        //如果要删除双向链表中最后一个元素,将执
       //行下面代码,这段代码后面再分析,实现LRU算法的关键所在
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
    }
 

super.addEntry()调用的是HashMap中的方法

 void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

但是linkedHashMapcreateEntry()方法进行重写

    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
        //在这里将要添加的元素放到数组中,记住,新增的元素是位于数组下标中链表的头部
        Entry<K,V> e = new Entry<>(hash, key, value, old);
        table[bucketIndex] = e;
        //这里是重点,用作维护双向双向链表的结构
        e.addBefore(header);
        size++;
    }
 private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
   }

我们这里重点分析addBefore(),此方法是用来维护双向链表结构的

新增一个元素

新增两个元素

我们会发现header.after永远指向第一个元素(最先插入的元素) ,header.before永远指向最后一个元素,而且最后一个元素的after永远指向头结点,这就是双向链表

我们再来分析addEntry(hash, key, value, i);

void addEntry(int hash, K key, V value, int bucketIndex) {
        super.addEntry(hash, key, value, bucketIndex);
        //如果要删除双向链表中最后一个元素,将执
       //行下面代码,这段代码后面再分析,实现LRU算法的关键所在
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
    }
 

取出header.after 即最早插入的元素,如果removeEldestEntry(eldest)返回true则删除这个元素,所以我们可以重写这个方法,根据元素个数是否超过预期返回true或者false来实现LRU算法

前面说到如果一个已经存在的元素再次保存到容器中,如果是按访问次数排序,那么这个元素会被删除,并重新添加到链表的末尾,相当于刷新了这个元素的访问次数,主要执行Entry#recordAccess()

void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                //这个删除元素
                remove();
                //重新添加该元素
                addBefore(lm.header);
            }
        }

3.get操作
    public V get(Object key) {
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }

可以发现执行get()操作后,如果是按访问顺序排序,那么将该元素删除后,重新添加该元素,即刷新钙元素的访问次数,前面已经分析过了

4.总结

所谓的LRU算法就是(Least recently used,最近最少使用)。使用一个双向链表,记录插入元素的顺序,默认肯定是先插入的元素最先被淘汰,因为最老。如果被命中,即get()操作后,删除该元素,重新再插入该元素,相当于更新了使用频率一样。依次类推。

5.附件,自定义LRU缓存
public class MyLRU<K,V> extends LinkedHashMap<K,V>{
    private int limit;
    public MyLRU(int limit){
        super(limit,0.75f,true);
        this.limit = limit;
    }

    /***
     * 重写此方法即可,当元素个数达到上限,返回true,就会删除一个最老元素
     * @param eldest
     * @return
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return super.size()>limit;
    }
}
public class Main {
    public static void main(String[] args) {
        MyLRU<Integer,String> lru = new MyLRU<>(3);
        lru.put(1,"1");
        lru.put(2,"2");
        lru.put(3,"3");
        lru.get(1);
        lru.put(4,"4");

        for (Map.Entry entry :lru.entrySet()){
            System.out.println(entry.getKey());
        }
    }
}
输出
3
1
4
因为执行了一次get(1)操作,那么元素1,就会被排到最后,此时容器中元素顺序为2,3,1,再新增元素4的时候,元素2就被被删掉,最终结果就是3,1,4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值