乐趣区

JDK18源码九javautilLinkedHashMap-类

前面我们介绍了 Map 集合的一种典型实现 HashMap,关于 HashMap 的特性,我们再来复习一遍:

①、基于 JDK1.8 的 HashMap 是由数组 + 链表 + 红黑树组成,相对于早期版本的 JDK HashMap 实现,新增了红黑树作为底层数据结构,在数据量较大且哈希碰撞较多时,能够极大的增加检索的效率。

②、允许 key 和 value 都为 null。key 重复会被覆盖,value 允许重复。

③、非线程安全

④、无序(遍历 HashMap 得到元素的顺序不是按照插入的顺序)

HashMap 集合可以说是最重要的集合之一,上篇博客介绍的 HashSet 集合就是继承 HashMap 来实现的。而本篇博客我们介绍 Map 集合的另一种实现——LinkedHashMap,其实也是继承 HashMap 集合来实现的,而且我们在介绍 HashMap 集合的 put 方法时,也指出了 put 方法中调用的部分方法在 HashMap 都是空实现,而在 LinkedHashMap 中进行了重写。所以想要彻底了解 LinkedHashMap 的实现原理,HashMap 的实现原理一定不能不懂。

1、LinkedHashMap 定义

LinkedHashMap 是基于 HashMap 实现的一种集合,具有 HashMap 集合上面所说的所有特点,除了 HashMap 无序的特点,LinkedHashMap 是有序的,因为 LinkedHashMap 在 HashMap 的基础上单独维护了一个具有所有数据的双向链表,该链表保证了元素迭代的顺序。

所以我们可以直接这样说:LinkedHashMap = HashMap + LinkedList。LinkedHashMap 就是在 HashMap 的基础上多维护了一个双向链表,用来保证元素迭代顺序。

更形象化的图形展示可以直接移到文章末尾。

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

2、字段属性

①、Entry<K,V>

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,我们看到对于 Entry 继承自 HashMap 的 Node 结构,相对于 Node 结构,LinkedHashMap 多了 before 和 after 结构。

下面是 Map 类集合基本元素的实现演变。


LinkedHashMap 中 Entry 相对于 HashMap 多出的 before 和 after 便是用来维护 LinkedHashMap 插入 Entry 的先后顺序的。

②、其它属性

// 用来指向双向链表的头节点
transient LinkedHashMap.Entry<K,V> head;
// 用来指向双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
// 用来指定 LinkedHashMap 的迭代顺序
//true 表示按照访问顺序,会把访问过的元素放在链表后面,放置顺序是访问的顺序
//false 表示按照插入顺序遍历
final boolean accessOrder;

注意 :这里有五个属性别搞混淆的,对于 Node next 属性,是用来维护整个集合中 Entry 的顺序。对于 Entry before,Entry after,以及 Entry head,Entry tail,这四个属性都是用来维护保证集合顺序的链表,其中前两个 before 和 after 表示某个节点的上一个节点和下一个节点,这是一个双向链表。后两个属性 head 和 tail 分别表示这个链表的头节点和尾节点。

3、构造函数

①、无参构造

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

调用无参的 HashMap 构造函数,具有默认初始容量(16)和加载因子(0.75)。并且设定了 accessOrder = false,表示默认按照插入顺序进行遍历。

②、指定初始容量

     public LinkedHashMap(int initialCapacity) {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;
     }

⑤、构造包含指定集合中的元素

     public LinkedHashMap(Map<? extends K, ? extends V> m) {super();
         accessOrder = false;
         putMapEntries(m, false);
     }

上面所有的构造函数默认 accessOrder = false,除了第四个构造函数能够指定 accessOrder 的值。

4、添加元素

LinkedHashMap 中是没有 put 方法的,直接调用父类 HashMap 的 put 方法。关于 HashMap 的 put 方法,可以参看我对于 HashMap 的介绍。

我将方法介绍复制到下面:

//hash(key) 就是上面讲的 hash 方法,对其进行了第一步和第二步处理
    public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
    }
    /**
     * 
     * @param hash 索引的位置
     * @param key  键
     * @param value  值
     * @param onlyIfAbsent true 表示不要更改现有值
     * @param evict false 表示 table 处于创建模式
     * @return
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
            boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;
         // 如果 table 为 null 或者长度为 0,则进行初始化
         //resize() 方法本来是用于扩容,由于初始化没有实际分配空间,这里用该方法进行空间分配,后面会详细讲解该方法
         if ((tab = table) == null || (n = tab.length) == 0)
             n = (tab = resize()).length;
         // 注意:这里用到了前面讲解获得 key 的 hash 码的第三步,取模运算,下面的 if-else 分别是 tab[i] 为 null 和不为 null
         if ((p = tab[i = (n - 1) & hash]) == null)
             tab[i] = newNode(hash, key, value, null);//tab[i] 为 null,直接将新的 key-value 插入到计算的索引 i 位置
         else {//tab[i] 不为 null,表示该位置已经有值了
             Node<K,V> e; K k;
             if (p.hash == hash &&
                 ((k = p.key) == key || (key != null && key.equals(k))))
                 e = p;// 节点 key 已经有值了,直接用新值覆盖
             // 该链是红黑树
             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);
                         // 链表长度大于 8,转换成红黑树
                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                             treeifyBin(tab, hash);
                         break;
                     }
                     //key 已经存在直接覆盖 value
                     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;
                 afterNodeAccess(e);
                 return oldValue;
             }
         }
         ++modCount;// 用作修改和新增快速失败
         if (++size > threshold)// 超过最大容量,进行扩容
             resize();
         afterNodeInsertion(evict);
         return null;
    }

这里主要介绍上面方法中,为了保证 LinkedHashMap 的迭代顺序,在添加元素时重写了的 4 个方法,分别是第 23 行、31 行以及 53、60 行代码:

 newNode(hash, key, value, null);
 putTreeVal(this, tab, hash, key, value)//newTreeNode(h, k, v, xpn)
 afterNodeAccess(e);
 afterNodeInsertion(evict);

①、对于 newNode(hash,key,value,null) 方法

HashMap.Node<K,V> newNode(int hash, K key, V value, HashMap.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) {
        // 用临时变量 last 记录尾节点 tail
        LinkedHashMap.Entry<K,V> last = tail;
        // 将尾节点设为当前插入的节点 p
        tail = p;
        // 如果原先尾节点为 null,表示当前链表为空
        if (last == null)
            // 头结点也为当前插入节点
            head = p;
        else {
            // 原始链表不为空,那么将当前节点的上节点指向原始尾节点
            p.before = last;
            // 原始尾节点的下一个节点指向当前插入节点
            last.after = p;
        }
    }

也就是说将当前添加的元素设为原始链表的尾节点。

②、对于 putTreeVal 方法

是在添加红黑树节点时的操作,LinkedHashMap 也重写了该方法的 newTreeNode 方法:

     TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
         linkNodeLast(p);
         return p;
     }

也就是说上面两个方法都是在将新添加的元素放置到链表的尾端,并维护链表节点之间的关系。

③、对于 afterNodeAccess(e) 方法,在 putVal 方法中,是当添加数据键值对的 key 存在时,会对 value 进行替换。然后调用 afterNodeAccess(e) 方法:

// 把当前节点放到双向链表的尾部
    void afterNodeAccess(HashMap.Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 当 accessOrder = true 并且当前节点不等于尾节点 tail。这里将 last 节点赋值为 tail 节点
        if (accessOrder && (last = tail) != e) {
            // 记录当前节点的上一个节点 b 和下一个节点 a
            LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            // 释放当前节点和后一个节点的关系
            p.after = null;
            // 如果当前节点的前一个节点为 null
            if (b == null)
                // 头节点 = 当前节点的下一个节点
                head = a;
            else
                // 否则 b 的后节点指向 a
                b.after = a;
            // 如果 a != null
            if (a != null)
                // a 的前一个节点指向 b
                a.before = b;
            else
                // b 设为尾节点
                last = b;
            // 如果尾节点为 null
            if (last == null)
                // 头节点设为 p
                head = p;
            else {
                // 否则将 p 放到双向链表的最后
                p.before = last;
                last.after = p;
            }
            // 将尾节点设为 p
            tail = p;
            //LinkedHashMap 对象操作次数 +1,用于快速失败校验
            ++modCount;
        }
    }

该方法是在 accessOrder = true 并且 插入的当前节点不等于尾节点时,该方法才会生效。并且该方法的作用是将插入的节点变为尾节点,后面在 get 方法中也会调用。代码实现可能有点绕,可以借助下图来理解:


④、在看 afterNodeInsertion(evict) 方法

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

该方法用来移除最老的首节点,首先方法要能执行到 if 语句里面,必须 evict = true,并且 头节点不为 null,并且 removeEldestEntry(first) 返回 true,这三个条件必须同时满足,前面两个好理解,我们看最后这个方法条件:

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

这就奇怪了,该方法直接返回的是 false,也就是说怎么都不会进入到 if 方法体内了,那这是这么回事呢?

这其实是用来实现 LRU(Least Recently Used,最近最少使用)Cache 时,重写的一个方法。比如在 mybatis-connector 包中,有这样一个类:

package com.mysql.jdbc.util;

import java.util.LinkedHashMap;
import java.util.Map.Entry;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final long serialVersionUID = 1L;
    protected int maxElements;

    public LRUCache(int maxSize) {super(maxSize, 0.75F, true);
        this.maxElements = maxSize;
    }

    protected boolean removeEldestEntry(Entry<K, V> eldest) {return this.size() > this.maxElements;
    }
}

可以看到,它重写了 removeEldestEntry(Entry<K,V> eldest) 方法,当元素的个数大于设定的最大个数,便移除首元素。

5、删除元素

同理也是调用 HashMap 的 remove 方法,这里我不作过多的讲解,着重看 LinkedHashMap 重写的第 46 行方法。

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
    final Node<K,V> removeNode(int hash, Object key, Object value,
            boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;
        //(n - 1) & hash 找到桶的位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 如果键的值与链表第一个节点相等,则将 node 指向该节点
        if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        node = p;
        // 如果桶节点存在下一个节点
        else if ((e = p.next) != null) {
            // 节点为红黑树
        if (p instanceof TreeNode)
         node = ((TreeNode<K,V>)p).getTreeNode(hash, key);// 找到需要删除的红黑树节点
        else {
         do {// 遍历链表,找到待删除的节点
             if (e.hash == hash &&
                 ((k = e.key) == key ||
                  (key != null && key.equals(k)))) {
                 node = e;
                 break;
             }
             p = e;
         } while ((e = e.next) != null);
        }
        }
        // 删除节点,并进行调节红黑树平衡
        if (node != null && (!matchValue || (v = node.value) == value ||
                      (value != null && value.equals(v)))) {if (node instanceof TreeNode)
         ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
        else if (node == p)
         tab[index] = node.next;
        else
         p.next = node.next;
        ++modCount;
        --size;
        afterNodeRemoval(node);
        return node;
        }
        }
        return null;
    }

我们看第 46 行代码实现:

void afterNodeRemoval(HashMap.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;
    }

该方法其实很好理解,就是当我们删除某个节点时,为了保证链表还是有序的,那么必须维护其前后节点。而该方法的作用就是维护删除节点的前后节点关系。

6、查找元素

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

相比于 HashMap 的 get 方法,这里多出了第 5,6 行代码,当 accessOrder = true 时,即表示按照最近访问的迭代顺序,会将访问过的元素放在链表后面。

对于 afterNodeAccess(e) 方法,在前面第 4 小节 添加元素已经介绍过了,这就不在介绍。

7、遍历元素

在介绍 HashMap 时,我们介绍了 4 中遍历方式,同理,对于 LinkedHashMap 也有 4 种,这里我们介绍效率较高的两种遍历方式:

①、得到 Entry 集合,然后遍历 Entry

LinkedHashMap<String,String> map = new LinkedHashMap<>();
        map.put("A","1");
        map.put("B","2");
        map.put("C","3");
        map.get("B");
        Set<Map.Entry<String,String>> entrySet = map.entrySet();
        for(Map.Entry<String,String> entry : entrySet){System.out.println(entry.getKey()+"---"+entry.getValue());
        }

②、迭代

         Iterator<Map.Entry<String,String>> iterator = map.entrySet().iterator();
         while(iterator.hasNext()){Map.Entry<String,String> entry = iterator.next();
             System.out.println(entry.getKey()+"----"+entry.getValue());
         }

这两种效率都还不错,通过迭代的方式可以对一边遍历一边删除元素,而第一种删除元素会报错。

打印结果:

8、迭代器

我们把上面遍历的 LinkedHashMap 构造函数改成下面的:

LinkedHashMap<String,String> map = new LinkedHashMap<>(16,0.75F,true);

也就是说将 accessOrder = true,表示按照访问顺序来遍历,注意看上面的 第 5 行代码:map.get(“B)。也就是说设置 accessOrder = true 之后,那么 B—2 应该是最后输出,我们看一下打印结果:


结果跟预期一致。那么在遍历的过程中,LinkedHashMap 是如何进行的呢?

我们追溯源码:首先进入到 map.entrySet() 方法里面:


发现 entrySet = new LinkedEntrySet(),接下来我们查看 LinkedEntrySet 类。


这是一个内部类,我们查看其 iterator() 方法,发现又 new 了一个新对象 LinkedEntryIterator,接着看这个类:


这个类继承 LinkedHashIterator。

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

        public final void remove() {
            HashMap.Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

9、总结

通过上面的介绍,关于 LinkedHashMap,我想直接用下面一幅图来解释:


去掉红色和蓝色的虚线指针,其实就是一个 HashMap。

本系列教程持续更新,可以微信搜索「IT 可乐」第一时间阅读。回复《电子书》有我为大家特别筛选的书籍资料

退出移动版