LinkHashMap 源码分析

LinkHashMap 简介

LinkHashMap 继承自 HashMap 同时也实现了 Map 接口。所以 LinkHashMap 的方法与 HashMap 中的方法类似,我们知道 HashMap 的存储的数据是无序的,而 LinkHashMap 则在 HashMap 的基础上实现了有序的数据存储,本文就主要查看源码看看 LinkHashMap 是如何实现有序的存储方式的;

LinkHashMap继承关系.png

如果对于 HashMap 的源码不了解的,建议先看 HashMap 的源码分析
)

LinkHashMap 的数据存储结构

我们知道 HashMap 在 JDK1.7 上的数据是基于数组和链表结构进行存储的,而 LinkHashMap 在此基础上增加了链表的双向存储结构,先看下大致的示意图

LinkHashMap 双向链表结构.png

LinkHashMap 维护的 Entry 数据包含 beforehashkeyvaluenextafter 值,相对与 HashMap.Entry 多了 beforeafter 的指向,从而构成双向链表;

LinkHashMap 分析

LinkHashMap 在使用方法上和 HashMap 基本一致:

    LinkedHashMap<String, Integer> hss = new LinkedHashMap<>();
    hss.put("aaa", 1);
    hss.put("bbb", 2);
    hss.put("aaa", 3);
    
    hss.get("aaa");
    hss.remove("aaa");

    for(Map.Entry<String, Integer> entry : hss.entrySet()){
        System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue());
    }
    

    Iterator<Map.Entry<String, Integer>> entries = hss.entrySet().iterator();
    while (entries.hasNext()) {
        Map.Entry<String, Integer> entry = entries.next();
        System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
    }

接下来继续查看 LinkHashMap 对数据的相关操作,是如何能够做到有序地存取数据?

LinkHashMap 的构造方法:

public LinkedHashMap() {
    super();
    accessOrder = false;
}

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

LinkHashMap 在调用了 HashMap 的构造方法后,设置了一个 accessOrder 为 false,根据意思猜想这个和访问顺序有关,此处先放着不管,后面会进行详细介绍。同时 LinkHashMap 重写了 init 方法,创建了一个 header 的 节点 ,这里的 Entry 是 LinkHashMap 的内部类,和 HashMap 中的 Entry 有所区别;

LinkHashMap 的 put 方法:

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

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

LinkHashMap 的 put 方法其实就是调用了 HashMap 的 put 方法,前面逻辑和 HashMap 一致,直到调用到 addEntry 方法,LinkHashMap 进行了重写;

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

    // Remove eldest entry if instructed
    Entry<K,V> eldest = header.after;
    if (removeEldestEntry(eldest)) {
        removeEntryForKey(eldest.key);
    }
}


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

addEntry 方法依然会调用到 HashMap 的 addEntry 方法,这里也可以再回顾下 HashMap 的 addEntry 方法;

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);// 创建数据节点
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

HashMap 的 addEntry 最终会调用 createEntry 方法,而 LinkHashMap 同时也重写了 createEntry 方法,所以这里重点分析就在 LinkHashMap 中的 addEntry 和 createEntry 方法了;

首先,和 HashMap 一样对数据扩容判断,之后就直接调用了 createEntry 方法;

createEntry 方法中 LinkHashMap 比 HashMap 多一步,e.addBefore(header)

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

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

   
    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }
    }

    void recordRemoval(HashMap<K,V> m) {
        remove();
    }
}

LinkHashMap 的 Entry 为自身的内部类,前面在初始化的方法中提到会创建一个 header Entry;

LinkHashMap 的 addBefore 方法其过程就是双向链表指向的不断交换过程,下图演示 LinkHashMap 在依次添加 Num1、 Num2、 Num3 的过程中链表指向不断改变的过程;

未进行添加元素前:

LinkHashMap_001.png

看到 Header 元素的 before 和 after 都指向自己;

接下来添加 Num1 :

LinkHashMap_002.png

Header 和 Num1 数据的 before 和 after 相互进行链接指向;

继续添加 Num2:

LinkHashMap_003.png

此时,header 的 before 指向 Num2,同时 Num2 的 before 指向 Num1, Num1 的 before 指向 Header,这里的 before 就像是一个倒序的循环,Num2 -> Num1;而 after 指向的情况正好相反,Num1 -> Num2, 这正好符合我们加入的顺序;

最后加入 Num3 ,假设 Num3 的数组索引位置和 Num1 相同,同样遵循 HashMap 的插入规则:

LinkHashMap_004.png

这里 header 和 after 的指向和上述情况一致,只不过 Num3 有个 next 的指向为 Num1;

看完 LinkHashMap 的插入操作,可以发现通过双向的链表 LinkHashMap 其实已经完成了插入顺序的记录过程;仔细看从 header 的 after 开始访问数据 ,依次是 Num1、Num2、Num3 和插入顺序一致;当再次访问到 header 节点的时候即数据访问完成;

需要注意的是,上述过程在完成添加操作之后,还会做一个 removeEldestEntry 操作, 删除第一个元素;默认返回 false 不执行判断体,可供继承者使用;

LinkHashMap 的 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;
}

调用 HashMap 的 getEntry 方法,和 HashMap 处理基本一致;

LinkHashMap 的 remove 方法:

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}

同样调用 HashMap 的 removeEntryForKey 方法移除相应的数据即可;

LinkHashMap 的遍历

通过 HashMap 的介绍我们可以知道,HashMap 是通过 HashIterator 实现 Iterator 来完成的,前面 LinkHashMap 通过双向链表已经记录了数据的插入顺序,接下来看看 LinkHashMap 如何去按顺序取出数据?

private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() { return nextEntry(); }
}

private abstract class LinkedHashIterator<T> implements Iterator<T> {
    Entry<K,V> nextEntry    = header.after; // 访问开始点
    Entry<K,V> lastReturned = null;

    int expectedModCount = modCount;

    public boolean hasNext() {
        return nextEntry != header;// 再次指向 header
    }

    public void remove() {
        if (lastReturned == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();

        LinkedHashMap.this.remove(lastReturned.key);
        lastReturned = null;
        expectedModCount = modCount;
    }

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

LinkHashMap 内部维护了自身的 Entry 类,Entry 类中维护的 EntryIterator 类集成自 LinkedHashIterator;

LinkedHashIterator 从 header.after 开始访问,以 after 进行访问点移动,直到节点指向 header 结束;以上述 Num1、Num2、Num3的案例来看,依次取出数据是 Num1、Num2、Num3 ,和存放的数据一致,从而实现了有序的排序;

在这里 modCount 和 expectedModCount 的判断过程以及内部操作无同步机制,表明 LinkHashMap 是非线程安全的集合类(同 HashMap)。

LinkHashMap 的 accessOrder

最后,来看下 accessOrder 这里变量的意义。accessOrder 在 LinkHashMap 的初始化中默认为 false,代表按照插入顺序进行排序。加入设置true 则代表访问顺序进行排序,具体可以搜索到 LinkHashMap 中的相关代码:

    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }
    }

LinkHashMap 中 recordAccess 方法会用到 accessOrder 这个变量,可以看到这个变量将当前数据进行删除,addBefore 方法会将其插入到双向链表最后一位。

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

而 recordAccess 方法则会在 LinkHashMap get 数据的时候进行调用。此时将访问的到的数据放到最末位。

   public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {// 带 accessOrder 赋值的构造方法
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
    }



    LinkedHashMap<String, Integer> hss = new LinkedHashMap<>(16,0.75f,true);
    
    hss.put("aaa", 1);
    hss.put("bbb", 2);
    hss.put("ccc", 3);
    
    hss.get("aaa");

    for(Map.Entry<String, Integer> entry : hss.entrySet()){
        System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue());
    }

上述案例将 accessOrder 设置为 true ,最终遍历到的顺序为 2、 3、 1;

LinkHashMap 总结

最后,通过本篇文章对 LinkHashMap 做一个简单总结:

1、LinkHashMap 是一个有序的集合类,可存储 null key;
2、LinkHashMap 是非线程安全的集合类;
3、LinkHashMap 采用了双向链表保证数据的存储顺序;
4、LinkHashMap accessOrder 可以用来把最近访问的数据放在最后一位,这一点在实际相关 LRU 数据缓存操作经常用使用到;

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

推荐阅读更多精彩内容