java基础解析系列(四)---LinkedHashMap的原理及LRU算法的实现

java基础解析系列(四)---LinkedHashMap的原理及LRU算法的实现

实验

遍历HashMap

public static void main(String[] args)
    {
        Map<String, String> map=new HashMap<String,String>();
        map.put("white", "小白");
        map.put("black", "小黑");
        map.put("red", "小红");
        map.put("yellow", "小黄");
        map.get("yellow");
        map.get("red");
        map.get("black");
        map.get("white");
        
        Iterator iter = map.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            System.out.println(entry.getKey() + "=" + entry.getValue());
        }
}
  • 结果发现,hashmap遍历出来的是无序的

换成LinkedHashMap

    Map<String, String> map=new LinkedHashMap<String,String>();
  • 结果发现,遍历出来的顺序是按找插入的顺序

改变LinkedHashmap的参数

Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,true);
  • 结果发现,遍历出来的顺序是按照访问的顺序的来的

分析

与HashMap的关系

  • LinkedHashMap可以看做是LinkedList+hashmap的组合
  • LinkedHashMap是hashmap的子类
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
  • LinkedHashMap中的Entry有before和after两个域,这是用来实现双向链表的
 private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;

        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }

构造方法

LinkedHashMap
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder)构造一个带指定初始容量、加载因子和排序模式的空 LinkedHashMap 实例。 

参数:
initialCapacity - 初始容量
loadFactor - 加载因子
accessOrder - 排序模式 - 对于访问顺序,为 true;对于插入顺序,则为 false 
抛出: 
IllegalArgumentException - 如果初始容量为负或者加载因子为非正
  • 从api可以看出,有一个参数accessOrder,默认情况下该参数为false,为false的时候,顺序为插入顺序,如果为true的话,顺序为访问顺序
public LinkedHashMap() {
      super();
      accessOrder = false;
  }
  void init() {
      header = new Entry<K,V>(-1, null, null, null);
      header.before = header.after = header;
  }
  • 从这里看出,还未put的时候就创建了一个header

put方法

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    //如果table数组该下标的内容不为空,遍历链表
    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;
            e.recordAccess(this);
            return oldValue;
        }
    }
//如果没有该key
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
  • put 方法继承自hashmap的,不同的是addEntry被重写了,如果put的键值对中是新的,那么执行addEntry方法

addEntry方法

void addEntry(int hash, K key, V value, int bucketIndex) {
    createEntry(hash, key, value, bucketIndex);

    // Remove eldest entry if instructed, else grow capacity if appropriate
    Entry<K,V> eldest = header.after;
    if (removeEldestEntry(eldest)) {
        removeEntryForKey(eldest.key);
    } else {
        if (size >= threshold)
            resize(2 * table.length);
    }
}
  • addEntry方法先调用createEntry方法

createEntry方法


void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMap.Entry<K,V> old = table[bucketIndex];
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
    table[bucketIndex] = e;
    e.addBefore(header);
    size++;
}
  • 这里将Entry放到table之后,还执行了一个addBefore方法

addBefore方法

private void addBefore(Entry<K,V> existingEntry) {
    after  = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
}
  • 这个方法用于维护双向链表,可以自己画一个图看一下

回到addEntry方法,执行完createEntry方法后,下面的部分代码

 // Remove eldest entry if instructed, else grow capacity if appropriate
    Entry<K,V> eldest = header.after;
    if (removeEldestEntry(eldest)) {
        removeEntryForKey(eldest.key);
    } else {
        if (size >= threshold)
            resize(2 * table.length);
    }
  • 第一句注释的意思是如果指示那么删除最年长的,否则扩容
  • 如果removeEldestEntry返回true,那么删除老的键值对,否则的话如果超过阈值,进行扩容

removeEldestEnrey方法

protected boolean More removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
  • removeEldestEntry方法,也就是是否删除最年长的Entry。这里始终返回的是false,我们可以对他进行重写

recordAccess方法

  • 当put的是新的键值对,键是之前没有的话,那么会创建一个新的Entry,而如果是之前有的话,那么会覆盖value并执行revordAccess,中文意思为记录访问
void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    if (lm.accessOrder) {
        lm.modCount++;
        remove();
        addBefore(lm.header);
    }
}
private void remove() {
    before.after = after;
    after.before = before;
}
private void addBefore(Entry<K,V> existingEntry) {
    after  = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
}
  • 这个方法会调用remove方法,而这个方法实际上是改变链表的指针,执行后,相当于之前没有这个这个Entry(可以自己动手画一下指针指向),然后再将这个Entry重新加入,这个时候以前添加的Entry,由于最近被使用,就被移到到链表的后面了
  • 同样使用get的时候也会调用此方法,也就是会由于被访问被移动到前面

总结

  • 初始化一个LinkedHash,会创建一个header
  • put的时候,将Entry放入数组中后,会和header连在一起
  • 再put的一个的时候,同样加入数组后,会继续和前面一个相连,此时header,小黑,小红三个连在一起,
  • 此时get小黑,小黑会从链表中移除,此时相当于只添加了小红,然后再将小黑添加进去,这样的话就保证了,最新访问的在后面
  • 如果put的时候该键是之前已有的,那么会覆盖value,然后重新调整该Entry在链表中的位置

LRU实现

  • 所谓LRU也就是Least Recently Used,最近最少使用,当缓存满了,删除最少使用的
  • 那么LinkedHashMap适合于解决这个问题,因为他使用了双向链表,将最新访问的移到了后面,那么这样的话,前面的就是最少使用,这时候就可以将最少使用的删除
  • removeEldestEntry,前面说过这方法,这个方法就是删除最少使用的,默认下不会删除,因为该方法返回false,所以当空间满了的话会扩容,我们可以重写这个方法,那么这样就不需要扩容,满了的时候删除最少使用的就可以
  • 重写removeEldestEntry,当大小大于容量的时候,会删除最年长的键值对
public class LRUCache {
    private int capacity;
    private Map<Integer, Integer> cache;
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new LinkedHashMap<Integer, Integer> (capacity, 0.75f, true) {
            // 定义put后的移除规则,大于容量就删除eldest
            protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
                return size() > capacity;
            }
        };
    }
    public int get(int key) {
        if (cache.containsKey(key)) {
            return cache.get(key);
        } else
            return -1;
    }
    public void set(int key, int value) {
        cache.put(key, value);
    }
    public static void main(String[] args) {
        LRUCache cache=new LRUCache(2);
        cache.set(1,1);
        cache.set(2, 2);
        //此时超出容量,put3的时候删除cache
        cache.set(3, 3);
        Set<Map.Entry<Integer,Integer>> set = cache.cache.entrySet();
        Iterator<Map.Entry<Integer,Integer>> iterator = set.iterator();
        
        while (iterator.hasNext())
        {
            System.out.print(iterator.next() + "\t");
        }
        System.out.println();
        
        //输出2=2 3=3
        
    }
}

我觉得分享是一种精神,分享是我的乐趣所在,不是说我觉得我讲得一定是对的,我讲得可能很多是不对的,但是我希望我讲的东西是我人生的体验和思考,是给很多人反思,也许给你一秒钟、半秒钟,哪怕说一句话有点道理,引发自己内心的感触,这就是我最大的价值。(这是我喜欢的一句话,也是我写博客的初衷)

作者:jiajun 出处: http://www.cnblogs.com/-new/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧。

posted @ 2017-09-06 14:59 jiajun_geek 阅读(...) 评论(...) 编辑 收藏