LinkedHashMap源码解析——基于JDK1.8

前言

LinkedHashMap继承自HashMap。和HashMap不同的是,它维护一条双向链表,解决了遍历顺序和插入顺序一致的问题。并且还对访问顺序提供了相应的支持。因为LinkedHashMap的很多实现是基于HashMap实现的,所以如果要读懂LinkedHashMap还是需要先了解HashMap。可以参考我的这篇文章

    /**
     * 指向双向链表的头结点(最老的结点)
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * 指向双向链表的尾结点(最年轻的结点)
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;

    /**
     * LinkedHashMap的迭代排序方法,如果是true,是进入顺序,如果为false,为插入顺序
     * The iteration ordering method for this linked hash map: true
     * for access-order, false for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;

1 常量介绍

    /**
     * Constructs an empty insertion-ordered LinkedHashMap instance
     * with the default initial capacity (16) and load factor (0.75).
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    /**
     * Constructs an empty insertion-ordered LinkedHashMap instance
     * with the specified initial capacity and a default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity
     * @throws IllegalArgumentException if the initial capacity is negative
     */
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    /**
     * Constructs an empty insertion-ordered LinkedHashMap instance
     * with the specified initial capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

2 链表结点的插入过程(LinkedHashMap保持顺序的原理)

首先是LinkedHashMap的插入过程。使用put方法,LinkedHashMap本身并没有重写自己继承的put方法,它用的还是HashMap中的put方法,那究竟是怎么做到不重写put方法就保证了插入的顺序了呢?

LinkedHashMap.Entry

    /**
     * HashMap.Node的子类
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    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);
        }
    }

原本HashMap的Node结点肯定不会记录插入顺序,但是上面的Entry结点就不一样了,它可以记录自己的上一个结点和下一个结点,记录插入的顺序。那这个结点是怎么添加进去的呢?那就不得不说另一个重要的方法了。LinkedHashMap通过重写HashMap的newNode方法,在HashMap添加结点时调用了子类LinkedHashMap的newNode方法,实现Entry结点的插入。这样既添加了双向链表的需求又没有破坏原本HashMap原本的方法。

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

LinkedHashMap#linkNodeLast

    // link at the end of list
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        //将尾结点复制给last
        LinkedHashMap.Entry<K,V> last = tail;
        //新添加的结点为尾结点
        tail = p;
        //如果是第一次添加,尾结点也是头结点
        if (last == null)
            head = p;
        //否则
        else {
            //将上一个尾结点赋值给新添加结点的上一个结点,插入结点的前驱
            p.before = last;
            //上一个尾结点的后继是当前尾结点
            last.after = p;
        }
    }

在HashMap中结点的类型有两种,一种是Node普通结点,一种是TreeNode树节点,Entry继承了HashMap.Node,可以无缝的加入,那么TreeNode呢?我们来看看HashMap中TreeNode结点的定义。

HashMap.TreeNode

    /**
     * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
     * extends Node) so can be used as extension of either regular or
     * linked node.
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        ....
    }

我们可以看到,HashMap中的TreeNode结点居然继承了LinkedHashMap.Entry结点,因为这个缘故,当然可以直接在LinkedHashMap中使用TreeNode了。

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

补充

当使用HashMap的putVal方法时还会调用一个在LinkedHashMap中重要的操作afterNodeInsertion

HashMap#putVal

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        ....
        ++modCount;
        if (++size > threshold)
            resize();
        //注意这个操作
        afterNodeInsertion(evict);
        return null;
    }

LinkedHashMap#afterNodeInsertion

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

LinkedHashMap#removeEldestEntry

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

当我们基于 LinkedHashMap 实现缓存时,通过覆写removeEldestEntry方法可以实现自定义策略的 LRU 缓存。

3 链表结点的删除过程

LinkedHashMap结点的删除操作同样的是使用父类HashMap内的相关方法(remove,removeNode),而没有重写相关方法。那该方法又怎么保持删除结点后链表的维护呢?让我们来看相关代码。

HashMap#remove

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

HashMap#removeNode

   final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        //p表示头结点或者将要被删除的结点的上一个结点
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //如果table不为空,长度>0,table中hash对应下标的第一个结点不为null,有值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //node指将要被删除的结点
            Node<K,V> node = null, e; K k; V v;
            //与上面相同
            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
                            node = e;
                            break;
                        }
                        //赋值给p,方便后面操作
                        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;
    }

删除结点后关键的操作是afterNodeRemoval方法,对于HashMap来说,该方法未做任何操作。LinkedHashMap重写了该方法,实现了删除结点后链表的维护操作。

LinkedHashMap#afterNodeRemoval

    void afterNodeRemoval(Node<K,V> e) { // unlink
        //复制删除结点e,并记录删除结点的上一个结点和下一个结点
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //将删除结点指向前结点和后结点的引用清空
        p.before = p.after = null;
        //如果删除结点的前一个结点为空,表明是第一个结点,改变head指向删除结点的下一个结点
        if (b == null)
            head = a;
        //否则前面存在结点,(删除节点的上一个结点的after)指向(删除结点的下一个结点)
        else
            b.after = a;
        //如果删除结点为尾结点,改变tail指向删除结点的上一个结点
        if (a == null)
            tail = b;
        //否则改变(删除结点的下一个结点的before)指向(删除结点的上一个结点)
        else
            a.before = b;
    }

4 链表结点的获得过程

LinkedHashMap#get

    /**
     * 返回指定键的值
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     * 如果map中保存着相应的键,返回相应的值,否则返回null
     * 

More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * //返回null不一定代表不存在键 *

A return value of {@code null} does not necessarily * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. */ public V get(Object key) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null) return null; //如果为true,将结点移动到链表末尾 if (accessOrder) afterNodeAccess(e); return e.value; }

LinkedHashMap#afterNodeAccess

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        //accessOrder=true并且访问的结点不是尾结点
        if (accessOrder && (last = tail) != e) {
            //复制访问结点e,并记录删除结点的上一个结点和下一个结点
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //访问节点的after置空(变成尾结点)
            p.after = null;
            //如果当前结点e为头结点,更换头结点为下一个结点
            if (b == null)
                head = a;
            //否则,当前结点的上一个结点的after指向当前结点的下一个结点
            else
                b.after = a;
            //如果当前结点不是尾结点,当前结点的下一个结点的before指向当前节点的上一个结点
            if (a != null)
                a.before = b;
            //否则,last指向当前结点的上一个结点(这里是为了适配非尾结点的状况,p.before = last)
            else
                last = b;
            //如果既是头结点又是尾结点,头结点指向当前结点(上面头结点指向空)
            if (last == null)
                head = p;
            //否则
            else {
                //当前结点的上一个结点是尾结点
                p.before = last;
                //尾结点的下一个结点是当前结点(至此将访问的当前结点移动到链表尾部)
                last.after = p;
            }
            //尾结点指向当前结点
            tail = p;
            ++modCount;
        }
    }

测试

Map<String, String> linkedHashMap = new LinkedHashMap<>(4, 0.75f, true);
        linkedHashMap.put("test1", "fjx1");
        linkedHashMap.put("test2", "fjx2");
        linkedHashMap.put("test3", "fjx3");
        System.out.println("开始时顺序:");
        Set<Map.Entry<String, String>> set = linkedHashMap.entrySet();
        set.forEach((entry)-> System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue()));



 System.out.println("通过get方法,导致key为test1对应的Entry到表尾");
        linkedHashMap.get("test1");
        Set<Map.Entry<String, String>> set2 = linkedHashMap.entrySet();
        set2.forEach((entry)-> System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue()));    

LinkedHashMap源码解析——基于JDK1.8_第1张图片

你可能感兴趣的:(Java)