jdk1.8 LinkedHashMap源码剖析

LinkedHashMap字面上意思是链表HashMap,那么到底增加了什么特性呢?

从一个简单的case剖析下去

Map map = new HashMap<>();
map.put(3, 3);
map.put(5, 5);
map.put(1, 1);
map.forEach((k, v) -> System.out.print(k + " "));

结果是

1 3 5
Map map = new LinkedHashMap<>();
map.put(3, 3);
map.put(5, 5);
map.put(1, 1);
map.forEach((k, v) -> System.out.print(k + " "));

结果是

3 5 1 

LinkedHashMap是不是发现了什么!没错,LinkedHashMap按照put的顺序输出(注意,不是排序)。

第一个问题,LinkedHashMap是如何记录输入的顺序的呢?

我们跟着代码进去一步步的看,首先还是先进入put方法,因为不管怎么样,你肯定是在put方法内部进行的吧?

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else{
        	……
        }    
        ……

是不是看newNode很可疑(强行解释一波),我们点进去看看

/* ------------------------------------------------------------ */
    // LinkedHashMap support


    /*
     * The following package-protected methods are designed to be
     * overridden by LinkedHashMap, but not by any other subclass.
     * Nearly all other internal methods are also package-protected
     * but are declared final, so can be used by LinkedHashMap, view
     * classes, and HashSet.
     */

    // Create a regular (non-tree) node
    Node newNode(int hash, K key, V value, Node next) {
        return new Node<>(hash, key, value, next);
    }

    // For conversion from TreeNodes to plain nodes
    Node replacementNode(Node p, Node next) {
        return new Node<>(p.hash, p.key, p.value, next);
    }

    // Create a tree bin node
    TreeNode newTreeNode(int hash, K key, V value, Node next) {
        return new TreeNode<>(hash, key, value, next);
    }

    // For treeifyBin
    TreeNode replacementTreeNode(Node p, Node next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

    /**
     * Reset to initial default state.  Called by clone and readObject.
     */
    void reinitialize() {
        table = null;
        entrySet = null;
        keySet = null;
        values = null;
        modCount = 0;
        threshold = 0;
        size = 0;
    }

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node p) { }

不点进去不知道,一点进去吓一跳啊,看最上面的注释,这不就是说这些方法都是特地为LinkedHashMap而准备的吗?

老规矩,我们只看newNode,其它的先不管,这时候要去看LinkedHashMap对newNode的重写

Node newNode(int hash, K key, V value, Node e) {
        LinkedHashMap.Entry p =
            new LinkedHashMap.Entry(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

嗯? LinkedHashMap.Entry是个什么东东,进去看看

static class Entry extends HashMap.Node {
        Entry before, after;
        Entry(int hash, K key, V value, Node next) {
            super(hash, key, value, next);
        }
    }

原来是Node的子类,加了两个指针,类似于双链表的next和pre。

OK,接下来看linkNodeLast这个方法是干嘛的,不用看猜也能知道是将节点放到双链表的尾部。

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry p) {
    LinkedHashMap.Entry last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

双链表的基本操作,不解释

最后newNode这个方法就完事了,有一个点要注意,不然到下面讲删除的时候转不过弯来,那就是newNode返回的是LinkedHashMap.Entry这个子类,所以hashmap中的node才会有before和after这两个指针。

接着,你会看到讲hashmap的时候没有讲到的那两个方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        ……
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //!!!!here!!!!!    
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        //!!!!here!!!!!    
        afterNodeInsertion(evict);
        return null;
    }

首先,我们看一下afterNodeAccess是做什么的?
这个方法的作用很简答,注释都告诉你了,就是将该节点移到末尾。

void afterNodeAccess(Node e) { // move node to last
        LinkedHashMap.Entry last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry p =
                (LinkedHashMap.Entry)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;
        }
    }

可以看到,只有当accessOrder为true的时候,这个方法才起作用,那accessOrder这个参数是干嘛用的呢?好的,我们在LinkedHashMap中搜索一下,看看都在哪里出现了。

public LinkedHashMap(int initialCapacity, float loadFactor) {
   super(initialCapacity, loadFactor);
   accessOrder = false;
}
    ……
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}
……
public LinkedHashMap() {
  	super();
    accessOrder = false;
}
……
public LinkedHashMap(Map m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}
……
public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
 	super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

很明显的看到,只有在构造LinkedHashMap的时候,accessOrder才会被赋值,而且只有最后一个构造函数可以将它设置为true。

这个参数字面意思就是访问排序,也就是说按照访问的顺序进行排序。先暂时这么理解。关于这个参数以及使用场景,又可以扯一波,留到末尾讲。

OK,再来看afterNodeInsertion这个方法

void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

和上面不一样的是,此方法一般由removeEldestEntry这个方法控制是否执行,所以我们看看这个方法是干嘛的?

protected boolean removeEldestEntry(Map.Entry eldest) {
        return false;
    }

这个方法其实是给你重写用的,返回true表示删除最不常访问的。
来个LRU缓存的简易版实现瞧瞧

public class LRUCache extends LinkedHashMap{

    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache<>();
        lruCache.put("A", 1);
        lruCache.put("B", 2);
        lruCache.put("C", 3);
        //此时顺序是A->B->C
        lruCache.get("B");
        //此时顺序是A->B->C
        lruCache.put("D", 4);
        //此时的数据为B->C->D
    }
    public LRUCache(){
        super(16, 0.75f, true);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        //如果size大于3就删除最不常访问的key
        return this.size() > 3;
    }
}

什么是LRU缓存?
最近最少使用,也就是不常访问的删掉,一般常访问的移到末尾,那么当空间不足时,删除头部的数据即可。

OK,put方法相当于分析完了,那么接下来分析比较重要的remove方法

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

这个都是常规操作,接着往下看removeNode方法

final Node removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        
            ……
                ++modCount;
                --size;
                //!!!!here!!!!
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

前面的和hashmap的一毛一样,不再分析,主要看afterNodeRemoval这个方法做了啥?
首先我们的疑问是,LinkedHashMap是如何做到添加删除都做到O(1)复杂度的?

void afterNodeRemoval(Node e) { // unlink
        //这里分别拿到了e的前后节点,那么删除e还不是很简单的事情?
        LinkedHashMap.Entry p =
            (LinkedHashMap.Entry)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;
    }

前面说过,LinkedHashMap的节点node其实就是LinkedHashMap.Entry,所以这里做一个向下强转,可以拿到before和after的信息,一个双链表想要删除中间某一个节点,那么必须要拿到它的前后节点。这里因为直接传来要删除的节点,而它又包含前后节点的信息,所以O(1)的实际复杂度就能够完成了。

添加删除分析完了,最后就是解释开头那个case的结果了,为什么顺序输出?

public void forEach(BiConsumer action) {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        for (LinkedHashMap.Entry e = head; e != null; e = e.after)
            action.accept(e.key, e.value);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }

这个就比较简单了,因为LinkedHashMap是直接遍历的双向链表,而不是hashmap。

总结一下:

  • LinkedHashMap继承HashMap,基本所有操作都直接用的HashMap,然后用HashMap提供的定制化方法,重写它,就成为了LinkedHashMap的特性了。
  • LinkedHashMap在每个节点都加了两个指针,组成一个双向链表,记录put的顺序。所以这是用空间换的特性。
  • LinkedHashMap可以利用它的某些方法的特性,直接用来定制化实现LRU缓存。

你可能感兴趣的:(java)