剖析Java 集合框架(五)-LinkedHashMap

这周感觉有点堕落,晚上都没怎么好好利用,看B站,突然还玩起了游戏,哎,视频真好看,游戏真好玩。
每天最少一个小时的学习时间,好不容易养成的习惯,可不能贪玩丢弃了。自勉,今天看了几期《棋魂》,下围棋动辄好几年都在练习,职业选手更是天天就是对着棋盘,这份毅力,热爱真是令人惊叹。

概述

LinkedHashMap 继承了 HashMap,具备 HashMap 的所有功能,不同点是,LinkedHashMap 还维护了一个双向链表,这个链表决定了迭代顺序。可以是插入的顺序和访问顺序。
可以查看前篇剖析Java 集合框架(二)-HashMap

本篇源码基于JDK 1.8

数据结构

LinkedHashMap

类的申明和成员属性

public class LinkedHashMap<K,V> extends HashMap<K,V>    implements Map<K,V> {

   // 双向链表的头结点
    transient LinkedHashMap.Entry<K,V> head;

    // 双向链表的尾节点
    transient LinkedHashMap.Entry<K,V> tail;

   // 双向链表顺序 是否为访问顺序 默认false
    final boolean accessOrder;

还是HashMap那套数据结构,只是每个 entry 节点还是有 beforeafter 节点。

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

默认情况下,beforeafter 维护的就是插入顺序。
LinkedHashMap 重写创建节点的方法。

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

// 把新创建的节点 放到链表的最后
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

理解LinkedHashMap 源码基本思路就是--多态,重写。LinkedHashMap重写了大量HaspMap的方法。

初始化

列出两个代表,默认构造方法 和 最多参数构造方法,可以发现双向链表的顺序默认为插入顺序,而且自身没做什么,全都都是调用的 HashMap 的方法

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

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

添加元素

在IDEA里Ctrl+F3或者Ctrl+O 可以调出该类的所有方法,可以发现LinkedHashMap没有put方法,所以肯定调用的 HashMapput 方法。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
      // LinkedHashMap  重写了
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
         // LinkedHashMap  重写了
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
     // LinkedHashMap  重写了
        afterNodeInsertion(evict);
        return null;
    }

着重注意 newNode,afterNodeAccess,afterNodeInsertion这几个方法, LinkedHashMap 都重写了这些方法,所以实际调用的是LinkedHashMap的方法。
newNode方法在前面已经分析过了,现在先分析 afterNodeAccess:

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
    // 如果是访问顺序 并且双向链表的尾节点不是传入的节点 
    // 这将e节点移动到双向链表的最后
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

这个方法通过名称也能看出一二,访问节点之后。如果是双向链表的顺序为访问顺序,所以每一层访问节点,都需要把节点移动到链表最后。
再来分析 afterNodeInsertion,插入之后的逻辑

void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

removeEldestEntry默认返回false,这是实现LUR算法的关键,我们自己重写这个方法,逻辑为:map里的元素个数大于某个值时返回true。则会执行移除双向链表的操作。
removeNodeHaspMap里面的方法,删除元素.需要注意的是,里面调用了 afterNodeRemoval方法,而该方法LinkedHashMap也重写了:

   // 将e从双向链表中移除
    void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

一个put方法几乎把所有操作都覆盖了,过程大体如下:

  1. 通过key的hash定位到桶的位置。
  2. 如果key已存在,直接替换value。并访问 LinkedHashMapafterNodeAccess方法,如果双向链表的顺序为访问顺序,则讲元素移动到链表尾部。
  3. 如果key不存在:
    1. 使用 LinkedHashMapnewNode方法创建节点,并把改节点添加到双向链表的尾部。
    2. LinkedHashMap.afterNodeInsertion方法 和 removeEldestEntry方法 判断是否移除掉双向链表的头结点。

其他方法

其他方法基本都是调用 HashMap 方法和上面分析的方法。比如 get,先获取到元素,然后判断是否执行afterNodeAccess,删除方法类似。

LUR 算法

我们用 LinkedHashMap实现一个LUR淘汰策略算法实例。

class LRUCache<K, V> extends LinkedHashMap<K, V> {

    private static final int MAX_SIZE = 10;

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_SIZE;
    }

    LRUCache() {
        super(MAX_SIZE, 0.75f, true);
    }
}

可以看到代码就几行,就实现最近最久未使用淘汰策略算法。

  1. 继承了 LinkedHashMap,构造方法将 accessOrder 设置为 true,开启访问顺序。
  2. 最大容量 MAX_SIZE 限制为10个。
  3. 重写了removeEldestEntry方法,当前map里的容量大于 MAX_SIZE时触发移除头结点方法。
    当然也可以不用集成 LinkedHashMap实现,就是一个HashMap+linkedList

总结

LinkedHashMap保证了元素有序(插入顺序/访问顺序),有序是通过双向链表实现的,链表的元素即为HashMap的节点,只是多了两个前后驱节点属性。维护数据时之后使用HashMap 的方法,维护顺序时,通过 HashMap 的方法定位到元素之后再通过前后驱属性操作链表。实现了插入,删除,访问时维护顺序也能控制在O(1)时间复杂度。

量变引发质变,经常进步一点点,期待蜕变的自己。

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

推荐阅读更多精彩内容