Java集合-LinkedHashMap工作原理

概述

在了解了Java集合-HashMap源码实现深入解析后,再来看LinkedHashMap就简单的多了。
先来个示例程序:

LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
map.put("语文", "1");
map.put("数学", "2");
map.put("英语", "3");
map.put("历史", "4");
map.put("政治", "5");
map.put("地理", "6");
map.put("生物", "7");
map.put("化学", "8");
map.put("物理", "9");
for(Entry<String, String> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

想想输出的结果是什么?结果顺序怎样?

语文: 1
数学: 2
英语: 3
历史: 4
政治: 5
地理: 6
生物: 7
化学: 8
物理: 9

有没有发现,输出结果的顺序和元素插入的顺序一致?而我们再HashMap中插入的顺序和输出的顺序却不一致。Why?要解释这个问题,必须从其数据存储结构说起。

数据结构

代码调试观察的变量结果:

图:LinkedHashMap调试时数据存储的结构图
根据上述调试结果,画出了如下的结构图:图:数据结构示意图,蓝色箭头为Entry节点的链接next,绿色和棕色箭头为链表节点的after和before

再看看源码中的变量声明:

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
    /**
     * The head of the doubly linked list.
     */
    private transient Entry<K,V> header;

    private final boolean accessOrder;
    // 其他省略
}

源码中只声明了一个header的Entry节点和accessOrder属性:
(1)accessOrder:迭代顺序标识,默认为false,按插入顺序迭代,true则按访问顺序迭代。
(2)header节点:实现链表的关键。是一个Entry节点,该Entry继承自HashMap,采用内部类实现,源码如下:

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;
        // 其他省略
}

因此LinkedHashMap内部的Entry节点除了继承HashMap.Entry的key、value、hash、next引用属性外,还添加了before和after引用节点以支持双向链表。这里只见到了header,没有tail,因此header其实也充当了tail的角色,正如所图中所示,所有元素和header一起形成互连的双向链表结构。到此,就拥有了链表的结构。

而LinkedHashMap还拥有HashMap一样的结构,原因在于其继承自HashMap,因此HashMap中用于存储元素的table也被继承过来,于是乎元素的存储也具备和HashMap一样的规则,所有元素存储在table中,table下的每个索引在原HashMap的结构下也是一个单向链表,同时每个元素中额外增加的before和after引用将table中的元素互连起来。正如调试代码中看到的那样。

到此我们就不难理解,为什么默认情况下,我们迭代时的顺序和插入的顺序一致了。因为迭代时,LinkedHashMap总是先从head出发,根据before引用遍历到所有元素,而在链表结构上插入元素总是插入到末尾。

put方法原理

LinkedHashMap没有直接重写HashMap的put方法,而是重写了HashMap中put方法用到的addEntry和createEntry方法,方法源码如下:只是多了一个after和before引用链的操作。

    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 先调用HashMap的addEntry方法存到table中
        super.addEntry(hash, key, value, bucketIndex);

        // 设置before,after引用,总是放在header的after引用上
        // 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);// 从这行代码可以看出和header建立了关系
        size++;
    }

其中Entry的addBefore方法实现如下,就是把header的after指向新元素,原来的尾部元素也指向新元素:

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

既然LinkedHashMap继承自HashMap,那么扩容规则也和HashMap一样。

get方法原理

LinkedHashMap重新了get方法:

    public V get(Object key) {
        Entry<K,V> e = (Entry<K,V>)getEntry(key);// getEntry用的是父类的方法
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }

实现基本和HashMap一致,e.recordAccess(this), 这个调用是设置accessOrder为true时有效的。即按访问顺序读取,这个方法会将最近访问的元素移动到链表末尾,方便下次访问,因此如果设置了这个参数,不管插入还是读取,都会改变链表的结构,在使用时需要谨慎评估。

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

总结


1、LinkedHashMap继承于HashMap。
2、数据存储结构结合了Hash表和链表结构,元素存储用Hash表,元素链接通过继承父类Entry节点并增加before和after引用实现。
3、LinkedHashMap的迭代顺序收到accessOrder属性的影响,默认为false,按插入顺序访问,true按上次读取顺序访问:即每次访问元素时会把元素移动到链表末尾方便下次访问,结构会时刻变化。


白天在加班加点的干活,还要抽出时间成为技术知识的分享者,分享不易,转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值