全网最易懂的Map之三---LinkedHashMap 源码解析

前言:

这篇文章的内容包括 LinkedHashMap 的常用api 的源码解析以及利用 LinkedHashMap来实现LRUCache算法。看此文章前最好看看我之前写的2片HashMap源码分析的文章;
HashMap源码分析一
HashMap源码分析二

说明:

  1. LinkedHashMap 是一个双向的链表,包含了指向链表头、尾的指针:headtail,正是由于这个特性,所以LinkedHashMap 是有序的,而HashMap是无序的
  2. LinkedHashMap 存储的Entey对象和HashMap的有不一样,因为是双向链表,所以肯定存在指向上个Node的指针 before以及指向下一个Node的指针 after
  3. LinkedHashMap 还有一个特有的变量 accessOrder,他是布尔型变量,true 表示链表按访问顺序,false 表示按插入顺序,

这里解释下 什么是什么是插入顺序和访问顺序?

比方说: 代码如下:

 LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
 linkedHashMap.put("1", "a");
 linkedHashMap.put("2", "b");
 linkedHashMap.put("3", "c");

我们传入的第3个参数就表示 accessOrder的值为true,我们现在上么也不做直接输出链表的值看看:
结果如下:
在这里插入图片描述
现在我们调用 linkedHashMap.get(“1”),结果是什么呢?
在这里插入图片描述
可以看出结果不一样了,key = “1”的结点放到了链表的最后,这个就表示按访问顺序了,当 accessOrder的值为true,那么get后会把这个Node放在链表的末尾,这就是利用LinkedHashMap实现LRUCache的核心思想了。而 accessOrder的值为false时并不会将该结点放到链表的末尾,后续我会讲到为什么是会这样的。

一、put操作:

LinkedHashMap 继承了HashMap,LinkedHashMap 并没有重写父类 HashMap 的 put 方法,只是重写了put操作中的部分方法,看代码:

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 注释1
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;// 最开始时table为空,初始会 table
        if ((p = tab[i = (n - 1) & hash]) == null) // 注释2
            tab[i] = newNode(hash, key, value, null);// 注释3 LinkedHashMap 中重写此方法
        else {
            Node<K,V> e; K k;
            // 注释4 如果是相同的key则替换value的值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode) // 如果是红黑树 则执行红黑树的插入操作
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                // 注释5 表示hash冲突了,找到冲突结点所在链表的最后一个结点,把当前结点插入到最后一个结点的后面
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 注释6
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;// 注释7
                }
            }
            // 注释8 表示key相同 则更新并返回旧的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); // 注释9 LinkedHashMap 中重写此方法,作用就是将这个结点放到链表的尾部
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold) // 注释10
            resize();
        afterNodeInsertion(evict); // 注释11 LinkedHashMap 中重写此方法, LRU用来删除头结点的方法,
        return null;
    }

按照之前讲HashMap的惯例,我们举例来分析:
下面我们开始以实例来分析了,为了方便说明,我这里的数组长度为 4,假设我们要put 3个元素,3个元素的key分别为 m,n和t;假设m的计算出来的hash值为5, n的计算出来的hash值为6,t的计算出来的hash值为9,

第一步:插入key 为 m的数据(m 的 key = 5):

走到注释1:由于是第一次插入数据所以 注释1的条件成立,先要执行resize方法进行table的初始化,这个很简单 可以看 HashMap源码分析一, 这里不做过多赘述。

走到注释2: p = tab[i = (n - 1) & hash],我们之前假设了链表的初始长度等于4,所以 (n - 1) & hash = 3&5 = 1,即将key为 m 的数据插入到数组 table index = 1的位置,

走到注释3:tab[i] = newNode(hash, key, value, null), LinkedHashMap 重写了 newNode 方法,如下:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMapEntry<K,V> p =
            new LinkedHashMapEntry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

LinkedHashMapEntry 是 LinkedHashMap 中插入的 Entry对象,包含了 before, after指针。

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

newNode 中的 linkNodeLast 方法如下:

 private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
        LinkedHashMapEntry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

就是将待插入结点p插入到链表的末尾。具体分析我通过画图来解释:

由于是第一次插入,所以 tail = null,即 last = null,然后让tail 指向待插入的结点 p,
由于 last = null,所以 if 条件语句成立,即 head 也指向待插入的 结点 p, 如下图:
在这里插入图片描述
然后 将 这个结点放到 数组 tab index = 1 的位置,如下图:
在这里插入图片描述
然后直接走到 注释 10
注释 10 : 判断 size 是否大于 阈值,这里的阈值 threshold = 4*0.75 = 3,这个公式应该都知道就不说了,然后 走到注释11,注释11 先不说,下面再说。

第二步:插入key 为 m的数据(n 的 key = 6):

走到注释2 : p = tab[i = (n - 1) & hash] , (n - 1) & hash = 3&6 = 2,即插入结点的位置位于数组tab的索引等于2。由于 数组索引等于2的位置没有元素,所以执行注释3

注释3tab[i] = newNode(hash, key, value, null) 即new一个结点放入 索引等于2的位置。 下面通过画图来解释 newNode 方法:

首先: LinkedHashMapEntry<K,V> last = tail; 即让局部变量 last指向链表的尾部,如下:
在这里插入图片描述

然后: tail = p; 即让 tail指向当前待插入的结点,如下:
在这里插入图片描述
然后: if (last == null) 由于last指向了上一个结点 m,所以 if 条件不成立,走到下面的else 语句:p.before = last; last.after = p; 如下图:
在这里插入图片描述

可以看出 执行完上面的2步操作后,tail 指向链表的尾部,且形成了双向链表。

第三步:插入key 为 t的数据(t 的 key = 9):

走到注释2: p = tab[i = (n - 1) & hash] (n - 1) & hash = 3&9 = 1,tab[1]之前插入了key 为 m 的结点,所以走到注释4:

注释4if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 判断当前的key和之前插入的key 是否相等, 之前 的key m 和 现在的 key t 不相等,所以走到注释5。

注释5if ((e = p.next) == null) 由注释2 可知 p 执行 key 为 m的结点,p.next = null,所以 条件成立,走到下面的 p.next = newNode(hash, key, value, null);

结果就是将 p 的 next 指针指向 当前插入的结点。接着分析 newNode:

首先: LinkedHashMapEntry<K,V> last = tail 即让 last 执行尾结点 tail,这一步就不画图了,
然后:尾指针 tail 指向待插入的结点p,如下图
在这里插入图片描述
然后:if (last == null) 由于last指向上一个结点 即 key为 n 的结点,所以 if 条件不成立,
走到 else 中 p.before = last; last.after = p;
在这里插入图片描述
然后 p.next = newNode(hash, key, value, null); ,p 执向的是key为 m的结点。最终图如下:

在这里插入图片描述
插入3个数据后的示意图如上,为了研究扩容,我这里再插入一个数据 r , 假设 r 的 key = 7;

插入key 为 r 的数据(r 的 key = 7)

根据之前的 公式算出 key 为 r 的结点在 数组 table 中的 index, (n - 1) & hash = 3 & 7 = 3,所以将 该结点插入到 table数组的最后。具体的分析就不讲了,最终的图如下:
在这里插入图片描述
插入第4个数据后,tail 指针指向最后插入的key 为 r 的结点。这时候走到注释10
if (++size > threshold) 其中 size++后 等于4 大于 threshold (threshold= 3),所以要进行扩容。即 走到 resize 方法。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
      。。。。。。
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) { // 注释1
                    oldTab[j] = null;
                    if (e.next == null)  // 注释2
                        newTab[e.hash & (newCap - 1)] = e;  // 注释3
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { //  // 注释4 preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;// 注释5
                            if ((e.hash & oldCap) == 0) {  // 注释6
                                if (loTail == null) // 注释7
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;  // 注释8
                            }
                            else { // 注释9
                                if (hiTail == null)  // 注释10
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;  // 注释11
                            }
                        } while ((e = next) != null);  // 注释12
                        if (loTail != null) { // 注释13
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) { // 注释14
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

省略号的部分不重要 就是计算 新数组的容量 和阈值,新数组的容量 和 阈值等于老数组的2倍,即 新的 table 容量等于8,新的阈值 threshold = 6;下面就是将 老数组中的数据 移到 新的数组中。具体操作就是遍历老数组中的数据,然后移到新数组中相应的位置。

走到注释1if ((e = oldTab[j]) != null) 由于老数组中 index 为0 的位置未插入数据,所以条件不成立,++j 后继续循环,依然走到注释1 ,由于老数组中index=1中插入的结点 key = m,即条件成立,走到 注释2。

注释2if (e.next == null) 由之前的结构图可知,e.next 指向的是 key=t的结点,即if 条件不成立,走到注释4

注释4: 这里面 就是定义了一些变量 : loHead, loTail, hiHead, hiTail 以及 next,接着就是 do…while循环了,走到注释5

注释5next = e.next; 即 变量next 指向 key为t的结点,如下图
在这里插入图片描述
注释6if ((e.hash & oldCap) == 0),oldCap 就是老数组的容量 即等于 4,oldCap应该是 oldCapacity 的省略写法。e 指向的结点 m 的hash值等于 5, 5&4 = 4 所以if条件不成立,走到注释:10:

注释10if (hiTail == null) hiHead 是之前定义的变量 赋值为 null,所以 条件成立,
执行 hiHead = e; 接着执行 注释11 hiTail = e;如下图:
在这里插入图片描述

接着执行注释12while ((e = next) != null) e 赋值为 key 为 t 的结点,所以不等于 null。如下图:
在这里插入图片描述
即while 条件满足,继续执行do while ,走到注释5 next = e.next; 由于 e 此时没有了后续结点了,所以 e.next = null,即 next = null;接着到 注释6。

注释6if ((e.hash & oldCap) == 0) e 指向结点的 key = t,hash值为 9 ,9&4 = 0,条件成立,走到注释7

注释7 if (loTail == null) loTail 是之前定义的局部变量且未被赋值,所以 条件成立,执行loHead = e; 和 注释8 的 loTail = e,如下图:

在这里插入图片描述

接着执行注释12while ((e = next) != null) next 等于null 所以 e = null,while 条件不成立。执行到注释13

注释13if (loTail != null) 条件成立:执行 loTail.next = null; newTab[j] = loHead; 执行的结果就是将 key 为 t 的node放到 新数组 index = 1的位置。接着执行注释14:

注释14 if (hiTail != null) hiTail 指向的结点的 key 为 m,所以,条件成立;执行 hiTail.next = null; newTab[j + oldCap] = hiHead; 执行的结果就是 将 key 为 m 的结点 放到新数组 所以等于 j + oldCap = 1+4 = 5 的位置。

接着 j++ j =2,老数组 索引等于 2 的位置存放的是 key = n 的结点,所以 注释1 if ((e = oldTab[j]) != null) 条件成立, e 指向key=n的结点,此结点没有后继结点(结点的next=null),所以注释2 if (e.next == null)成立, 执行注释3

注释3newTab[e.hash & (newCap - 1)] = e; n 的hash值为6 ,newCap 新数组的容量为8,所以 e.hash&newCap= 6&7 = 6,即 将 key = n 的结点放到新数组索引值为6 的位置。

同理 将 key = r ,hash = 7 的结点 插入的位置为 7&7 = 7;

最终扩容后的结果如下图:
在这里插入图片描述

现在put 和 resize 扩容操作基本讲完了。接下来讲 get方法,我们这里的 的 accessOrder = true LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>(4, 0.75f, true); 就是前面讲到的按访问顺序来的,即 通过 get 或者 put Map中已有的 key 的操作,会将相应的结点移动到链表尾部。来看看源码是怎么实现的。

四: get 操作

我们还是用插入了4条数据的Map来分析,但是没扩容,为的是好理解,即插入 key 分别为 m (hash = 5)n(hash = 6)、t (hash = 9)、 r(hash = 7) 如下图。

在这里插入图片描述

我们直接 get 一个最复杂的 即 get(t) 。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;
    }

getNode 代码实在 父类HashMap中实现的:

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) { // 注1
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k)))) // 注2
                return first;
            if ((e = first.next) != null) { // 注3
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) // 注4
                        return e;
                } while ((e = e.next) != null); // 注5
            }
        }
        return null;
    }

走到注1if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) first = tab[(n - 1) & hash],t 的 hash 值为9 , n 为数组的长度 等于 4,3&9 = 1,first 等于数组中索引等于1的结点,即 key = m 的结点;
走到注2if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) 由于我们get key = t的数据,所以注2不成立,走到 注3

注3if ((e = first.next) != null) first.next 指向 key 为m 结点的后继结点即 key = t 的结点。走到注释4

注4if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 条件成立 返回找到的结点 e, return e;

接着执行 LinkedHashMap 中get 方法的剩下代码:if (accessOrder) afterNodeAccess(e); 由于 accessOrder 等于 true ,所以执行 afterNodeAccess

方法。如下:

// 将结点e移到链表的尾部
 void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        // 判断 accessOrder 和 当前结点 e 是否是链表的尾结点
        if (accessOrder && (last = tail) != e) { // 注1
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after; // 注2
            p.after = null;
            if (b == null) // 注3
                head = a;
            else
                b.after = a;
            if (a != null) // 注4
                a.before = b;
            else
                last = b;
            if (last == null)// 注5
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p; // 注6
            ++modCount;
        }
    }

执行完 注1和注2后的图如下:
在这里插入图片描述
接着执行 p.after = null; 如下图:
在这里插入图片描述
红色的X号表示断开连接。

走到注释3if (b == null) b 指向 key = n的结点,所以条件不成立,执行 b.after = a; 如下图:在这里插入图片描述
我把断开连接的那条线去掉了,这样更清晰。

接着 执行 注释4if (a != null), a 执行的key= r的结点,所以条件成立,执行 a.before = b; 如下图:
在这里插入图片描述
接着 执行 注释5if (last == null) last 指向 尾结点 即 key = r的结点,所以条件不成立,执行 p.before = last; last.after = p; 执行后 如下图:
在这里插入图片描述

最后 执行 : tail = p; 即 将该结点设为尾结点,如下图:
在这里插入图片描述
这样的话, key = t 的结点就移到链表的末尾啦。
刚才讲了 put 的key 在map中已存在的话也会导致该结点会移到链表的尾部,在代码的哪里体现的呢?其实是在 putVal 方法中体现的,如下:

...
// 注释8 表示key相同 则更新并返回旧的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);// 注释9 LinkedHashMap 中重写此方法,作用就是将这个结点放到链表的尾部
                return oldValue;
            }
            ...

最后分析下 是怎么实现LRUCache 的,LRU 表示的是最近最少使用,我们通过给LinkedHashMap 是在 accessOrder = true后,就保证了 最近使用的会放到链表的尾部,那么我们可以猜想到 达到删除条件的时候肯定删除的是链表头部的结点,是不是这样的呢? 还有 是不是 设置了 accessOrder = true 就可以实现LRUCache 了呢?

我们通过源码来给出强有力的答案吧。

什么时候会触发删除结点的条件呢?我们想一想,猜测肯定是 put 数据的时候,对不对,看源码。

我们可以看到 put 方法的最后 会调用 afterNodeInsertion(evict); 这个方法在 LinkedHashMap 中实现了,如下:

 void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMapEntry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key; // first 指向的是头结点,即删除链表头的结点
            removeNode(hash(key), key, null, false, true);
        }
    }

其中 evict 默认是true , 而 removeEldestEntry 默认返回的是false,所以要想实现LRUCache 不光要设置 accessOrder = true,还要重写此方法。重写如下:

 private MyLRUCache() {
        map = new LinkedHashMap<String, String>(6, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> entry) {
                //当map的size 大于3的时候就删除表头结点
                return map.size() > 3;
            }
        };
    }

然后就是看真正的删除结点的算法啦,removeNode 是在 HashMap 中实现的,我这里只摘出 部分代码

// node 表示的就是待删除的头结点 index 表示头结点在table数组中的索引值
tab[index] = node.next; // 注1 
 afterNodeRemoval(node); // 注2

执行完 注1 后的图如下:
在这里插入图片描述
接着执行 afterNodeRemoval 如下:

 LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after; // 注1
        p.before = p.after = null; // 注2
        if (b == null)// 注3
            head = a;
        else
            b.after = a;
        if (a == null)// 注4
            tail = b;
        else
            a.before = b;

执行完 注1 后,如下图:
在这里插入图片描述
由于 删除的是头结点,所以 b = p.before = null, a 执行 key = n 的结点,

注2: p.before = p.after = null; 执行完如下图,即断开p.after的那条链接
在这里插入图片描述
注3if (b == null) 条件成立,执行 head = a; head 指向结点a,如下图:
在这里插入图片描述
注4if (a == null) 条件不成立,执行 a.before = b; 断开结点a执行结点p的连接,如下:

在这里插入图片描述
这样整个删除头结点的操作就执行完了。这也是实现LRUCache的实现原理了。

总结:

  1. LinkedHashMap 是有序的,是双向链表
  2. LinkedHashMap 如果 accessOrder 设置为true,那么它是按访问顺序的。
  3. 利用 LinkedHashMap 实现LRUCache 必须同时满足 accessOrder = true 且 重写 removeEldestEntry 方法

最后附上自定义的LRUCache 代码:

public class MyLRUCache {
    private static volatile MyLRUCache mInstance;
    private LinkedHashMap<String, String> map;

    private MyLRUCache() {
        map = new LinkedHashMap<String, String>(6, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> entry) {
                //当map的size 大于3的时候就删除表头结点
                return map.size() > 3;
            }
        };
    }

    public static MyLRUCache getInstance() {
        if (mInstance == null) {
            synchronized (MyLRUCache.class) {
                if (mInstance == null) {
                    mInstance = new MyLRUCache();
                }
            }
        }
        return mInstance;
    }

    public void put(String key, String value) {
        map.put(key, value);
    }

    public String get(String key) {
        return map.get(key);
    }

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

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值