02_LinkedHashMap

Hash table and linked list implementation of the Map interface, with predictable iteration order.
This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries.
This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map ( insertion-order ).

LinkedHashMap的特征

  • HashMap的子类
  • 有序迭代
    LinkedHashMap实现与HashMap的不同之处在于,LinkedHashMap的链表是双向链表,维护着元素的加入顺序(默认)。此双向链表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。

构造器

看下它的无参构造器,了解下对象属性有什么。

/**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the default initial capacity (16) and load factor (0.75).
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    // 
    /**
    * accessOrder的作用,设置迭代顺序,默认为false
    */
        /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;

由构造器可以看出,LinkedHashMap也是链表数组(和HashMap一样)。那它在什么地方把数组元素的HashMap的单向链表替换成双向链表的实现呢。
在put方法里(LinkedHashMap没有重写此方法)初始化table(第一次resize())时,发现调用了LinkedHashMap重写的newNode()方法,使得其数组元素为Node<K,V>的子类
LinkedHashMap.Entry<K,V>(static class Entry<K,V> extends HashMap.Node<K,V>)。

// 这里的参数e是链表的next对象
 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;
    }

LinkedHashMap.Entry对象维护着父类的next属性,这是解决hash碰撞时的单向链表,另外LinkedHashMap.Entry的head和tail属性维护着node的顺序。所以它的结构类似以下:


linkedhashmap.PNG

迭代

默认的迭代顺序是Insert顺序:accessOrder=false,可想而知,顺序是基于这个双向链表的,那么迭代顺序的设置,实际上就是此双向链表的维护。
按照insert的顺序迭代很好理解,先加入的在前面,迭代(正序)时先输出;那么按照access顺序呢?
看一下HashMap的putVal方法里面的两个语句:

            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 看这里
                afterNodeAccess(e);
                return oldValue;
            }
         // ...
         // 以及在putVal方法最后的
          afterNodeInsertion(evict);

看一下LinkedHashMap中对这两个方法的重写:

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

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            // b指向e的前驱,a指向e的后继,如果前驱和后继都不为null的话,直接把前驱的后继指向后继,即把e在链中的关系注销了。
            if (b == null)
                head = a;
            else
                b.after = a;
            //  此处的a 应该不会是null的 是null的话就证明e是末尾元素了
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            // 更新双向链表的tail
            tail = p;
            ++modCount;
        }
    }

关于AfterNodeInsertion方法的evict参数,在put方法中传入的是true:

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

还有就是afterNodeInsertion内部调用的removeEldestEntry:

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

它是固定返回false的,也就是说在当前的LinkedHashMap的实现中,没有开启remove eldest的功能,所以这个afterNodeInsertion方法在目前来看其实是空的。

再看afterNodeAccess,它的业务逻辑很清晰,是把Node e移到链表的最后,这个e就是put进去的Node或者是get的Node。在这种情况下,每次put或get元素,都会引起底层双向链表的变化。
示例:

    @Test
    public void testLinkedHashMap() {
        LinkedHashMap<Integer, String> lhm = new LinkedHashMap<>(16, 0.75f, true);
        lhm.put(1, "one");
        lhm.put(3, "three");
        lhm.put(2, "two");

        // for (Integer key : lhm.keySet()) {
        // System.out.println(lhm.get(key));
        // }
        System.out.println(lhm);
        lhm.get(1);
        System.out.println(lhm);

    }

上面代码的输出是:

{1=one, 3=three, 2=two}
{3=three, 2=two, 1=one}

如果在迭代时,访问了元素,像上面注释掉的代码,会抛ConcurrentModificationException。

if (modCount != expectedModCount)
    throw new ConcurrentModificationException();

为什么modCount != expectedModCount呢,在开始迭代时,创建迭代对象把modCount值赋给了expectedModCount,但是后面访问元素触发了afterNodeAccess方法,导致modCount变化使得二者不一致。

//迭代开始时创建迭代对象
        LinkedHashIterator() {
            next = head;
            expectedModCount = modCount;
            current = null;
        }

那么想要去迭代怎么办呢?
至少有以下两个方法:

// 方法一
        Set<Entry<Integer, String>> entrySet = lhm.entrySet();
        Iterator<Entry<Integer, String>> iterator = entrySet.iterator();
        while (iterator.hasNext()) {
            Entry<Integer, String> entry = iterator.next();
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
// 方法二
        entrySet.forEach(new Consumer<Entry<Integer, String>>() {

            @Override
            public void accept(Entry<Integer, String> entry) {
                System.out.println(entry.getKey() + ":" + entry.getValue());

            }
        });

这两个方式的共同点都是直接拿到这个双向链表,取到Entry对象,直接操作Entry对象。看下迭代器iterator的代码就很清晰了:

// entrySet = new LinkedEntrySet()-> LinkedEntryIterator -> LinkedEntryIterator extends LinkedHashIterator

 final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
         
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
        // ...
        // 省略
    }

// 核心在这里,构成单向链表
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;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }
         // ...
        // 省略 
    }

实际上抛开accessOrder不看,这两个方式比for循环里面get(key)的效率要高,至少不用循环对keyhash寻址去找链表元素,然后再迭代链表元素。

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

推荐阅读更多精彩内容

  • 春深天益热,草木已见深。 曲槐不耐闲,花白滴素嫩。 清香醒春困,不知醉路人。 尤忆小儿时,采此作餐饪。 ...
    森垚阅读 310评论 0 0
  • 今天分享的主题是正念饮食,你吃进的是什么,你的生命就是什么样! 一天三餐的饮食,是每个人每天都要进行的,可你知道你...
    谷应阅读 1,336评论 0 1
  • 你知道自考生吗?相信你对自考生非常不理解,这一族群你知道多少?有人对我说过考试超级简单,考试内容不看书都会过,60...
    小民刷剧阅读 238评论 0 0
  • 锤党性 结友谊 ——2016年省直机关工委党校培训学习 明天,就要毕业了,一个字——酸;两个字——不舍;三个字——...
    品书彧彩阅读 921评论 0 1