09-LinkedHashMap 核心源码分析(集合)

注:源码系列文章主要是对某付费专栏的总结记录。如有侵权,请联系删除。

1 LinkedHashMap 整体架构

HashMap 是无序的,TreeMap 可以按照 key 进行排序,那有木有 Map 是可以维护插入的顺序的呢?接下来我们看看 LinkedHashMap。

LinkedHashMap 本身是继承 HashMap 的,所以它拥有 HashMap 的所有特性,在此基础上,还提供了两大特性:

  • 按照插入顺序进行访问;
  • 实现了访问最小最先删除功能,其目的是把很久都没有访问的 key 自动删除。

1.1 按照插入顺序访问

1.1.1 LinkedHashMap 链表结构

// 链表头
transient LinkedHashMap.Entry<K,V> head;

// 链表尾
transient LinkedHashMap.Entry<K,V> tail;

// 继承 Node,为数组的每个元素增加了 before 和 after 属性
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

// 控制两种访问模式的字段,默认 false
// true: 按照访问顺序,会把经常访问的 key 放到队尾
// false: 按照插入顺序提供访问
final boolean accessOrder;

从新增的属性可以看到,LinkedHashMap 的数据结构很像是把 LinkedList 每个元素换成了 HashMap 的 Node,像是两者的结合体,也正是因为增加了这些结构,从而能把 Map 的元素都串联起来,形成一个链表,而链表就可以保证顺序了,就可以维护元素插入进来的顺序。

1.1.2 按照顺序新增

LinkedHashMap 初始化时,默认的 accessOrder 是 false,就是会按照插入顺序提供访问,插入方法使用的是父类 HashMap 的 put 方法,不过覆写了 put 方法执行中调用的 newNode/newTreeNode 和 afterNodeAccess 方法。

newNode/newTreeNode 方法,控制新增节点追加到链表的尾部,这样每次新节点都追加到尾部,即可保证插入顺序了,我们以 newNode 源码为例:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    // 初始化一个新增的节点
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 追加到链表的尾部
    linkNodeLast(p);
    return p;
}

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    // 缓存旧的尾节点
    LinkedHashMap.Entry<K,V> last = tail;
    // 赋值尾节点为新增节点
    tail = p;
    // 如果旧的尾节点为null则表示当前链表为空,直接将新增节点赋值于头节点
    if (last == null)
        head = p;
    // 链表有数据,将旧的尾节点和新的尾节点互联
    else {
        p.before = last;
        last.after = p;
    }
}

LinkedHashMap 通过新增头节点、尾节点,给每个节点增加 before、after 属性,每次新增时,都把节点追加到尾节点等手段,在新增的时候,就已经维护了按照插入顺序的链表结构了。

1.1.3 按照顺序访问

LinkedHashMap 只提供了单向访问,即按照插入的顺序从头到尾进行访问,不能像 LinkedList 那样可以双向访问。

我们主要通过迭代器进行访问,迭代器初始化的时候,默认从头节点开始访问,在迭代的过程中,不断访问当前节点的 after 节点即可。

Map 对 key、value 和 entity(节点)都提供了迭代的方法,假设women携带 entity,就可以使用 LinkedHashMap.entrySet().iterator() 这种写法直接返回 LinkedHashIterator,LinkedHashIterator 是迭代器,我们通过调用迭代器的 nextNode() 方法就可以得到下一个节点,迭代器的源码如下:

abstract class LinkedHashIterator {
    LinkedHashMap.Entry<K,V> next;
    LinkedHashMap.Entry<K,V> current;
    int expectedModCount;

    // 初始化时,默认从头节点开始访问
    LinkedHashIterator() {
        next = head;
        expectedModCount = modCount;
        current = null;
    }

    public final boolean hasNext() {
        return next != null;
    }

    final LinkedHashMap.Entry<K,V> nextNode() {
        // 当前遍历的节点
        LinkedHashMap.Entry<K,V> e = next;
        // 判断 modCount
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        current = e;
        // 通过链表的 after 结构,找到下一个迭代的节点
        next = e.after;
        return e;
    }
}

在新增节点时,我们就已经维护了元素之间的插入顺序了,所以迭代访问是非常简答的,只需要不断的访问当前节点的下一个节点即可。

1.2 访问最少删除策略

1.2.1 演示

这种策略也叫做 <a href="https://baike.baidu.com/item/LRU/1269842?fr=aladdin">LRU</a>(least recently used,最近最少使用),大概的意思就是经常访问的元素会被追加到队尾,这样不经常访问的数据自然就靠近队头,然后我们可以设置删除策略,比如当 Map 元素个数大于多少时,把头节点删除,如下:

@Test
public void accessOrderTest() {
    // 使用带参构造函数,分别指定初始化大小,加载因子,访问模式(accessOrder)
    LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(5, 0.75f, true) {
        {
            put(1, 1);
            put(2, 2);
            put(3, 3);
            put(4, 4);
            put(5, 5);
        }

        // 覆写了删除策略方法,我们设定当节点个数大于 4 时,就开始删除头节点
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
            return size() > 4;
        }
    };

    System.out.println("初始化: " + JSON.toJSONString(map));
    map.get(3);
    System.out.println("map.get(3): " + JSON.toJSONString(map));
    map.get(2);
    System.out.println("map.get(2): " + JSON.toJSONString(map));
}

执行结果:

初始化: {2:2,3:3,4:4,5:5}
map.get(3): {2:2,4:4,5:5,3:3}
map.get(2): {4:4,5:5,3:3,2:2}

可以看到,map 初始化的时候,我们放进去五个元素,但结果只有四个元素, 1 不见了,这个主要是因为我们覆写了 removeEldestEntry 方法,我们实现了如果 map 中元素个数大于 4 时,则就把队头的元素删除,当 put(5,5) 执行的时候,正好把队头的 1 删除,这个体现了当达到我们设定的删除策略时,会自动删除头节点。

当我们调用 map.get(3) 方法时,元素 3 移动到了队尾,调用 map.get(2) 方法时,元素 2 被移动到了队尾,这个体现了经常被访问的节点会被移动到队尾。

这个例子就很好的说明了访问最少删除策略,接下来看看原理。

1.2.2 元素被转移到队尾

为什么 get 时,元素会被移动到队尾:

public V get(Object key) {
    Node<K,V> e;
    // 调用 HashMap get 方法
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果设置了 LRU 策略
    if (accessOrder)
        // 这个方法把当前 key 移动到队尾
        afterNodeAccess(e);
    return e.value;
}

从上述源码中,可以看到,通过 afterNodeAccess 方法把当前访问节点移动到了队尾,其实不仅仅是 get 方法,执行 getOrDefaultcomputecomputeIfAbsentcomputeIfPresentmerge 方法时,也会这么做,通过不断的把经常访问的节点移动到队尾,那么靠近队头的节点,自然就是很少被访问的元素了。

1.2.3 删除策略

上述例子中我们在执行 put 方法时,发现队头元素被删除了,LinkedHashMap 本身是没有 put 方法实现的,调用的是 HashMap 的 put 方法,但 LinkedHashMap 实现了 put 方法中的调用 afterNodeInsertion 方法,这个方法实现了删除,源码如下:

// 删除很少被访问的元素,被 HashMap 的 put 方法所调用
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    // 得到链表的头节点
    LinkedHashMap.Entry<K,V> first;
    // 如果队列不为空,并且删除策略允许的情况下. removeEldestEntry 为我们覆写的删除策略
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        // 得到头节点的 key
        K key = first.key;
        // 删除头节点
        removeNode(hash(key), key, null, false, true);
    }
}

1.3 小结

LinkedHashMap 提供了两个很有意思的功能:按照插入顺序访问和删除最少访问元素策略,简单的通过链表的结构就实现了,设计的非常巧妙。

LinkedHashMap 在 HashMap 的基础上简单的增加了链表结构,就形成了节点的顺序,非常巧妙。

------------------------------------- END -------------------------------------

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

推荐阅读更多精彩内容