Java集合-LinkedHashMap工作原理

来源:互联网 发布:c语言统计单词个数 编辑:程序博客网 时间:2024/06/06 14:12

概述

在了解了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