我的jdk源码(十六):LinkedHashMap类

一、概述

    LinkedHashMap类是继承自HashMap类,但是在HashMap的数据结构基础上,使得每个桶的元素又通过新Entry特殊的结构,组成一条双向链表。有了双向链表的结构,就能保证LinkedHashMap的实例在默认情况下能够保持元素的插入顺序。

二、源码剖析

    (1) 类的声明

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

    LinkedHashMap的继承实现结构比较简单,就是继承了HashMap类,然后实现了Map类,让LinkedHashMap拥有Map的特性。

    (2) 元素结构

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

    LinkedHashMap类的结点Entry类实际是继承了HashMap类的结点Node类,并且在此基础上添加了before和after两个引用,用来记录双向链表的前后结点。

    (3) 成员变量

    //序列化标识ID
    private static final long serialVersionUID = 3801124242820219131L;
    //记录头结点
    transient LinkedHashMap.Entry<K,V> head;
    //记录尾结点
    transient LinkedHashMap.Entry<K,V> tail;
    //当accessOrder设置为false时,会按照插入顺序进行排序(在创建新节点的时候,把该节点放到了尾部),当accessOrder为true时,会按照访问顺序(也就是插入和访问都会将当前节点放置到尾部,尾部代表的是最近访问的数据)
    final boolean accessOrder;

    LinkedHashMap类中除了记录了头尾结点外,最重要的设置属性accessOrder来维持LinkedHashMap的实例中元素的顺序,有两种情况:当accessOrder设置为false时,会按照插入顺序进行排序(在创建新节点的时候,把该节点放到了尾部);当accessOrder为true时,会按照访问顺序(也就是插入和访问都会将当前节点放置到尾部,尾部代表的是最近访问的数据)。

    对于成员变量accessOrder来说,使用final关键字修饰,表示不能改变:

    a、和局部变量的不同点在于,成员变量有默认值,因此必须手动赋值

    b、final的成员变量可以定义的时候直接赋值,或者使用构造方法在构造方法体里面赋值,但是只能二者选其一

    c、如果没有直接赋值,那就必须保证所有重载的构造方法最终都会对final的成员变量进行了赋值

    (4) 构造方法

    //无参构造函数
    public LinkedHashMap() {
        //调用父类HashMap的构造函数(下同)
        super();
        //设置默认的排序规则为插入顺序(下同)
        accessOrder = false;
    }

    //带初始容量的构造函数,initialCapacity也只是建议容量,并非最终容量
    public LinkedHashMap(int initialCapacity) {
        //调用父类HashMap的构造函数
        super(initialCapacity);
        //设置默认的排序规则为插入顺序
        accessOrder = false;
    }

    //带初始容量和负载因子的构造函数
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    //带初始容量、负载因子、排序规则的构造函数
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

    //参数为Map的构造函数
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        //将原Map中的元素插入新LinkedHashMap中
        putMapEntries(m, false);
    }

    可以看到LinkedHashMap类几乎所有的构造函数都是调用的父类HashMap的构造函数,只是多了一步设置成员变量accessOrder为false的操作。当LinkedHashMap()是带Map的构造函数的时候,就需要调用HashMap的putMapEntries()方法,使得原Map的元素变得有序了,并且顺序就为元素插入顺序。

    (5) putMapEntries()方法

    //将一个Map中的所有元素添加到LinkedHashMap中,排序规则为evict
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        //获取m的元素个数,必须大于0才执行
        int s = m.size();
        if (s > 0) {
            如果LinkedHashMap中的容器table为null 
            if (table == null) { // pre-size
                //获取根据m的元素个数s获取table的新容量,此时是float类型ft 
                float ft = ((float)s / loadFactor) + 1.0F;
                //判断新容量是否小于2的30次方MAXIMUM_CAPACITY,小于则取ft的整数部分为t,大于则取MAXIMUM_CAPACITY
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                //如果新容量t是否大于了阈值threshold,如果大于了,那么设置阈值threshold为大于等于形容量t的最小2次幂
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            //如果容器table不为null,那么先判断m中的元素个数s是否超过了阈值,超过则调用HashMap的resize()方法进行扩容
            else if (s > threshold)
                resize();
            //容器准备就绪,就开始往LinkedHashMap循环放入元素
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                //调用HashMap的putVal()方法添加元素
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

    resize()方法涉及HashMap的动态扩容,是HashMap的核心方法之一,就不再多说,详细请看《我的jdk源码(十三):HashMap  一磕到底,追根溯源!》。putVal()方法虽然是写在HashMap类里,但是里面调用的afterNodeAccess()方法,HashMap中并没具体实现,而是在LinkedHashMap中重写了该方法,并且LinkedHashMap还重写了newNode()方法,那么我们结合putVal()方法和afterNodeAccess()方法一起看一下。

    (6) putVal()方法和LinkedHashMap.afterNodeAccess()等方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的key、key 的 hashcode与写入的 key 是否相等,相等就赋值给e,在第下面会统一进行赋值及返回。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 该链为链表
        else {
            //如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
            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;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 调用afterNodeAccess()方法进行链表排序
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 最后判断是否需要进行扩容。超过最大容量就扩容,实际大小大于阈值则扩容。
    if (++size > threshold)
        resize();
    // 调用afterNodeInsertion()方法进行链表排序
    afterNodeInsertion(evict);
    return null;
}

    //这是LinkedHashMap重写后的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;
    }

    //把元素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重写后的afterNodeAccess()方法,当accessOrder为true并且传入的节点不是最后一个时,然后将该节点放到尾部
void afterNodeAccess(Node<K,V> e) {
    //在执行方法前的上一次的尾结点
    LinkedHashMap.Entry<K,V> last;
    //当accessOrder为true并且传入的节点并不是上一次的尾结点时,执行下面的方法
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //p:当前节点
        //b:当前节点的前一个节点
        //a:当前节点的后一个节点;
        
        //将p.after设置为null,断开了与后一个节点的关系,但还未确定其位置
        p.after = null;
        /**
         * 因为将当前节点p拿掉了,那么节点b和节点a之间断开了,我们先站在节点b的角度建立与节点a
         * 的关联,如果节点b为null,表示当前节点p是头结点,节点p拿掉后,p的下一个节点a就是头节点了;
         * 否则将节点b的后一个节点设置为节点a
         */
        if (b == null)
            head = a;
        else
            b.after = a;
        /**
         * 因为将当前节点p拿掉了,那么节点a和节点b之间断开了,我们站在节点a的角度建立与节点b
         * 的关联,如果节点a为null,表示当前节点p为尾结点,节点p拿掉后,p的前一个节点b为尾结点,
         * 但是此时我们并没有直接将节点p赋值给tail,而是给了一个局部变量last(即当前的最后一个节点),因为
         * 直接赋值给tail与该方法最终的目标并不一致;如果节点a不为null将节点a的前一个节点设置为节点b
         *
         * (因为前面已经判断了(last = tail) != e,说明传入的节点并不是尾结点,既然不是尾结点,那么
         * e.after必然不为null,那为什么这里又判断了a == null的情况?
         * 以我的理解,java可通过反射机制破坏封装,因此如果都是反射创建出的Entry实体,可能不会满足前面
         * 的判断条件)
         */
        if (a != null)
            a.before = b;
        else
            last = b;
        /**
         * 正常情况下last应该也不为空,为什么要判断,原因和前面一样
         * 前面设置了p.after为null,此处再将其before值设置为上一次的尾结点last,同时将上一次的尾结点
         * last设置为本次p
         */
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        //最后节点p设置为尾结点,完事
        tail = p;
        ++modCount;
    }
}


    //这是LinkedHashMap重写后的afterNodeInsertion()方法,目的是移除链表中最老的节点对象,也就是当前在头部的节点对象,但实际上在JDK8中不会执行,因为removeEldestEntry方法始终返回false。
    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);
        }
    }

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

    由源码可以得知,当accessOrder设置为false时,会按照插入顺序进行排序,当accessOrder为true时,会按照访问顺序进行排序。具体的操作就是,把元素放到双向链表的末尾。

    (7) get()方法

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

三、总结

    LinkedHashMap类其实也比较好理解,动态扩容就是HashMap的resize()方法,LinkedHashMap类只是会根据属性accessOrder值来进行排序,当accessOrder为默认值false的时候,每次插入元素的时候,就将插入的元素放到双向链表的末尾;当accessOrder为true时,每次调用put方法和get方法的时候,都会将元素放到链表的末尾。敬请期待《 我的jdk源码(十七):Objects 》。

    更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!

猜你喜欢

转载自blog.csdn.net/qq_34942272/article/details/106434832