大话系列 - LinkedHashMap源码分析

  再来看一下LinkedHashMap的源码分析,这个数据结构可以方便我们构建LFU时使用,因为其实内部的数据存储是利用了HashMap的功能,然后额外通过一个链表来记录元素之间的顺序关系。

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

  首先看类的继承关系,可以看到是直接继承了HashMap,而且本身也没有实现增删改查的相关操作,实际的数据操作都是直接使用父类的

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

  这里定义了一个Entry的子类,用于在LinkedHashMap中记录元素相关顺序,可以看到这个新的数据结构除了Entry基本的属性信息外,只提供了一个前置和后置指针。

    /**
     * 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;

    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;

  然后看三个成员属性,首先是accessOrder,通过注释我们可以知道这个属性是用来确定元素的迭代排序的方式 ,true表示按照访问顺序,false表示代表元素的插入顺序。那对应的head和tail就是这个列表的头尾指针。

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> 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;
    }

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> 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 <tt>LinkedHashMap</tt> instance
     * with the default initial capacity (16) and load factor (0.75).
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    /**
     * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
     * the same mappings as the specified map.  The <tt>LinkedHashMap</tt>
     * instance is created with a default load factor (0.75) and an initial
     * capacity sufficient to hold the mappings in the specified map.
     *
     * @param  m the map whose mappings are to be placed in this map
     * @throws NullPointerException if the specified map is null
     */
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

    /**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

  构造方法一共提供了5个,但是具体的构造都是以来父类。还有就是如果在构造方法中如果没有明确指定accessOrder的话,这个值是false,那也就是说默认情况下遍历迭代的顺序是按照元素的插入顺序的。

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

  从这里开始咱们看几个子类重写的方法,首先是 afterNodeAccess ,这个方法的调用入口是在HashMap的put方法中,如果能够在原有结构中找到待添加的元素时,会调用这个方法,被访问的Entry节点作为方法入参,但是这个方法在HashMap中是没有实现的。

  首先说一下这链表是一个双向链表,如果是按照插入顺序,那么最早添加的元素是在队首,如果是按照访问顺序,那最近最少被访问的元素也是在队首。所以这里会先判断当前需要被处理的元素,如果后指针为null ,那就证明他已经是在队尾了,不需要再调整了。

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

  接下来就是 afterNodeInsertion 方法,这个也是在put方法中调用的,只是这里是在元素第一次被添加的时候调用的,这里存在一个入参,就是evict属性,是否会驱逐当前Map中的元素,那就必须要看一下 removeEldestEntry 方法了。

    /**
     * Returns <tt>true</tt> if this map should remove its eldest entry.
     * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
     * inserting a new entry into the map.  It provides the implementor
     * with the opportunity to remove the eldest entry each time a new one
     * is added.  This is useful if the map represents a cache: it allows
     * the map to reduce memory consumption by deleting stale entries.
     *
     * <p>Sample use: this override will allow the map to grow up to 100
     * entries and then delete the eldest entry each time a new entry is
     * added, maintaining a steady state of 100 entries.
     * <pre>
     *     private static final int MAX_ENTRIES = 100;
     *
     *     protected boolean removeEldestEntry(Map.Entry eldest) {
     *        return size() &gt; MAX_ENTRIES;
     *     }
     * </pre>
     *
     * <p>This method typically does not modify the map in any way,
     * instead allowing the map to modify itself as directed by its
     * return value.  It <i>is</i> permitted for this method to modify
     * the map directly, but if it does so, it <i>must</i> return
     * <tt>false</tt> (indicating that the map should not attempt any
     * further modification).  The effects of returning <tt>true</tt>
     * after modifying the map from within this method are unspecified.
     *
     * <p>This implementation merely returns <tt>false</tt> (so that this
     * map acts like a normal map - the eldest element is never removed).
     *
     * @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  This is the entry that will be removed it this
     *           method returns <tt>true</tt>.  If the map was empty prior
     *           to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
     *           in this invocation, this will be the entry that was just
     *           inserted; in other words, if the map contains a single
     *           entry, the eldest entry is also the newest.
     * @return   <tt>true</tt> if the eldest entry should be removed
     *           from the map; <tt>false</tt> if it should be retained.
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

  这里的注释比较长,挑干的说几点:

  1. 首先这是一个protected的方法,可以被子类覆盖,默认这里是不会删除Map对象中的元素的
  2. 我们知道HashMap即使我们指定了容量,但是如果添加元素超过了初始化容量,是会出发扩容操作的。但是这方法是提供了一个条件入口,用于自定义删除元素的条件

  所以如果我们通过这个对象作为构建缓存的基础数据结构,几个一定要重写这个方法。

  再结合上面的双向链表存储结构,那我们可以知道这个Eldest Element如果是按照插入顺序,那就是队尾的那一个元素,如果是按照访问顺序,那就是队首的那一个元素。

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

  这个方法就是删除元素的核心方法了,就是如果满足被驱逐条件的情况下,需要通过这个方法来驱逐相应的元素。上面已经分析过了,如果是按照访问顺序来处理,此时head对象就是需要被驱逐的对象,细心的可以发现其实这个方法就是HashMap中的删除方法。

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

  最后就是这个删除时回调的方法了,其实这里并没有做过多额外的处理,只是上面的删除方法是在table中删除对应对应,但是在双向链表中的对象还没有被删除,那就是在这里进行链表遍历并删除元素。

  说到这里不知道大家有没有发现一个问题,就是我们说了,数组的操作是依赖于HashMap提供的能力,然后元素关系是通过链表来维护,而且在插入和删除的关键性方法中都提供了回调方法,用于维护双向链表的结构。那么双向链表是在哪里添加的呢?

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

  这里我们找到了一个核心方法,在创建单链表或者红黑树的时候添加新节点中,都会间接的调用到这个方法,在这里会把新添加的元素添加到链表的尾部,这里不区分是按照什么顺序访问的,新添加的元素也可以理解为最近被访问的元素嘛,与accessOrder也不冲突。

posted @ 2021-08-13 16:03  SyrupzZ  阅读(39)  评论(0)    收藏  举报