LinkedHashMap原理分析

本文主要内容

  • LinkedHashMap使用
  • LinkedHashMap源码解析
  • Lru算法

今天打算看看android的图片缓存相关知识点,没想到引申到了LinkedHashMap容器了。

LinkedHashMap继承HashMap,相比于HashMap的无序,它是有序的,甚至它还能根据访问顺序输出内部存储元素,正是因为此特性,Lru(近期最少使用)算法一般由LinkedHashMap实现,尤其适合缓存算法。

LinkedHashMap使用

先来看看LinkedHashMap是如何使用的,以及它的神奇效果。

public class Lru<K, V> extends LinkedHashMap<K, V> {

  public Lru(int initialCapacity,
        float loadFactor,
        boolean accessOrder){
    super(initialCapacity, loadFactor, accessOrder);
  }

  @Override
  protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
    if(size() > 6){
        return true;
    }
    return false;
  }

  public static void main(String[] args) {
    Lru<Character, Integer> lru = new Lru<Character, Integer>(16, 0.75f, true);
    String s = "abcdefghijkl";
    for (int i = 0; i < s.length(); i++) {
        lru.put(s.charAt(i), i);
    }
    System.out.println("LRU中key为h的Entry的值为: " + lru.get('h'));
    System.out.println("LRU的大小 :" + lru.size());
    System.out.println("LRU :" + lru);
  }
}

它的输出结果是:

LRU中key为h的Entry的值为: 7
LRU的大小 :6
LRU :{g=6, i=8, j=9, k=10, l=11, h=7}

代码中的构造方法,前两个在HashMap中见过,可参照HashMap源码解析阅读。最后一个参数,accessOrder,非常神奇。

accessOrder,表示LinkedHashMap不按照插入顺序排序,而是按照访问顺序排序。

联系上述代码的输出结果,h落在了最后,这就是accessOrder元素的作用。

值得一提的是,HashMap就无序的,因为HashMap内的元素在数组中的位置是由元素的hash值决定的,并不是先插入就在位置的前面。但LinkedHashMap是有序的,默认按插入顺序排序。

那么,为什么LinkedHashMap这么神奇呢?

LinkedHashMap源码解析

LinkedHashMap中申明了两个成员变量:

private transient Entry<K,V> header;
private final boolean accessOrder;

而且LinkedHashMap对Entry也重写了,Entry类中添加了两个成员变量 before, after。

private static class Entry<K,V> extends HashMap.Entry<K,V> {
  Entry<K,V> before, after;
}

从上边来看,LinkedHashMap不但继承了父类使用数组存储元素,hash定位,自己内部还保存着一个双向链表,正是因为此双向链表的存在,LinkedHashMap才能有序。

我们来看它的put方法,不过LinkedHashMap并没有重写put方法,所以它的插入方法仍然同HashMap一致:

//HashMap的put方法
public V put(K key, V value) {
    //省略中间过程
    addEntry(hash, key, value, i);
}

但LinkedHashMap重写了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);
    }
}

查看createEntry方法:

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

乍一看和HashMap很类似,但它额外调用了addBefore方法:

  private void addBefore(Entry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }

LinkedHashMap在初始化的时候,初始化header节点,header节点的after和before都是它自己。

void init() {
    header = new Entry<>(-1, null, null, null);
    header.before = header.after = header;
}

如果先后添加元素key0和key1,那么根据addBefore的逻辑,最终的连接情况如下:

如果 accessOrder 值为true时,当用户调用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;
}
    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }
    }

如果accessOrder为true,那么会将此节点原来的after和before节点重置,最后调用addBefore方法,节点位于双向链表的最后一位,类似于上图中的 key1。

回顾第1节关于LinkedHashMap的使用,最后打印LinkedHashMap,其实就是调用LinkedHashMap的toString方法,元素的排序非常有意思。

LinkedHashMap的toString方法是定义在AbstractMap类中:

//AbstractMap的toString方法
public String toString() {
    Iterator<Entry<K,V>> i = entrySet().iterator();
    if (! i.hasNext())
        return "{}";

    StringBuilder sb = new StringBuilder();
    sb.append('{');
    for (;;) {
        Entry<K,V> e = i.next();
        K key = e.getKey();
        V value = e.getValue();
        sb.append(key   == this ? "(this Map)" : key);
        sb.append('=');
        sb.append(value == this ? "(this Map)" : value);
        if (! i.hasNext())
            return sb.append('}').toString();
        sb.append(',').append(' ');
    }
}
//HashMap类
//HashMap类中的newEntryIterator方法,返回一个Iterator对象
public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator();
    }
//LinkedHashMap类
//LinkedHashMap重写newEntryIterator方法,返回自己的Iterator对象,以便于符合自己风格的遍历
Iterator<Map.Entry<K,V>> newEntryIterator() { return new EntryIterator(); }
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
    //LinkedHashMap返回的Iterator,最后调用nextEntry方法,实现Iterator接口,实现遍历
    public Map.Entry<K,V> next() { return nextEntry(); }
}
//以下代码是LinkedHashIterator类中的,初始化的nextEntry指向header的after
Entry<K,V> nextEntry    = header.after;
//nextEntry方法,就是在返回nextEntry对象的after节点
Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (nextEntry == header)
            throw new NoSuchElementException();

        Entry<K,V> e = lastReturned = nextEntry;
        nextEntry = e.after;
        return e;
    }

查看完以上代码,发现LinkedHashMap最终遍历时,其实是以header为第1个节点,不停地向后遍历after节点,所以第1章中产生的结果就会是那样,先插入的先显示出来,后插入的后显示,如果accessOrder为true,那么调用get方法得到的元素也会被插入到链表尾部,它也会被最后遍历。

Lru算法

Lru算法,即是 Least recently used,最近最少使用。使用LinkedHashMap可以轻易实现此算法。

当LinkedHashMap添加元素时:

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;
    //如果removeEldestEntry方法返回为true,那么则删除1个节点
    //默认就是删除离header节点最近的节点,即header的after,即链表的头部
    if (removeEldestEntry(eldest)) {
        removeEntryForKey(eldest.key);
    } else {
        if (size >= threshold)
            resize(2 * table.length);
    }
}

默认情况下removeEldestEntry方法返回为false,所以,如果要实现Lru算法,那么需要重写此方法,返回为true才行

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

当removeEldestEntry方法返回为true时,则每次都是删除离header最近的节点,回想起第1节,我们的测试代码结果也符合这一描述。

ps:没想到LinkedHashMap是这么有趣的一个容器,使用双向链表即可实现Lru算法,惊叹于Java的完美封装,LinkedHashMap能完美的继承于HashMap,同时还能有自己的特色,真是完美的代码,多向源码学习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容