09-LinkedHashMap 核心源码分析(集合)

2020-03-03  本文已影响0人  xinxisimple

注:源码系列文章主要是对某付费专栏的总结记录。如有侵权,请联系删除。

1 LinkedHashMap 整体架构

HashMap 是无序的,TreeMap 可以按照 key 进行排序,那有木有 Map 是可以维护插入的顺序的呢?接下来我们看看 LinkedHashMap。

LinkedHashMap 本身是继承 HashMap 的,所以它拥有 HashMap 的所有特性,在此基础上,还提供了两大特性:

1.1 按照插入顺序访问

1.1.1 LinkedHashMap 链表结构

// 链表头
transient LinkedHashMap.Entry<K,V> head;

// 链表尾
transient LinkedHashMap.Entry<K,V> tail;

// 继承 Node,为数组的每个元素增加了 before 和 after 属性
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);
    }
}

// 控制两种访问模式的字段,默认 false
// true: 按照访问顺序,会把经常访问的 key 放到队尾
// false: 按照插入顺序提供访问
final boolean accessOrder;

从新增的属性可以看到,LinkedHashMap 的数据结构很像是把 LinkedList 每个元素换成了 HashMap 的 Node,像是两者的结合体,也正是因为增加了这些结构,从而能把 Map 的元素都串联起来,形成一个链表,而链表就可以保证顺序了,就可以维护元素插入进来的顺序。

1.1.2 按照顺序新增

LinkedHashMap 初始化时,默认的 accessOrder 是 false,就是会按照插入顺序提供访问,插入方法使用的是父类 HashMap 的 put 方法,不过覆写了 put 方法执行中调用的 newNode/newTreeNode 和 afterNodeAccess 方法。

newNode/newTreeNode 方法,控制新增节点追加到链表的尾部,这样每次新节点都追加到尾部,即可保证插入顺序了,我们以 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;
}

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    // 缓存旧的尾节点
    LinkedHashMap.Entry<K,V> last = tail;
    // 赋值尾节点为新增节点
    tail = p;
    // 如果旧的尾节点为null则表示当前链表为空,直接将新增节点赋值于头节点
    if (last == null)
        head = p;
    // 链表有数据,将旧的尾节点和新的尾节点互联
    else {
        p.before = last;
        last.after = p;
    }
}

LinkedHashMap 通过新增头节点、尾节点,给每个节点增加 before、after 属性,每次新增时,都把节点追加到尾节点等手段,在新增的时候,就已经维护了按照插入顺序的链表结构了。

1.1.3 按照顺序访问

LinkedHashMap 只提供了单向访问,即按照插入的顺序从头到尾进行访问,不能像 LinkedList 那样可以双向访问。

我们主要通过迭代器进行访问,迭代器初始化的时候,默认从头节点开始访问,在迭代的过程中,不断访问当前节点的 after 节点即可。

Map 对 key、value 和 entity(节点)都提供了迭代的方法,假设women携带 entity,就可以使用 LinkedHashMap.entrySet().iterator() 这种写法直接返回 LinkedHashIterator,LinkedHashIterator 是迭代器,我们通过调用迭代器的 nextNode() 方法就可以得到下一个节点,迭代器的源码如下:

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;
        // 判断 modCount
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        current = e;
        // 通过链表的 after 结构,找到下一个迭代的节点
        next = e.after;
        return e;
    }
}

在新增节点时,我们就已经维护了元素之间的插入顺序了,所以迭代访问是非常简答的,只需要不断的访问当前节点的下一个节点即可。

1.2 访问最少删除策略

1.2.1 演示

这种策略也叫做 <a href="https://baike.baidu.com/item/LRU/1269842?fr=aladdin">LRU</a>(least recently used,最近最少使用),大概的意思就是经常访问的元素会被追加到队尾,这样不经常访问的数据自然就靠近队头,然后我们可以设置删除策略,比如当 Map 元素个数大于多少时,把头节点删除,如下:

@Test
public void accessOrderTest() {
    // 使用带参构造函数,分别指定初始化大小,加载因子,访问模式(accessOrder)
    LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(5, 0.75f, true) {
        {
            put(1, 1);
            put(2, 2);
            put(3, 3);
            put(4, 4);
            put(5, 5);
        }

        // 覆写了删除策略方法,我们设定当节点个数大于 4 时,就开始删除头节点
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
            return size() > 4;
        }
    };

    System.out.println("初始化: " + JSON.toJSONString(map));
    map.get(3);
    System.out.println("map.get(3): " + JSON.toJSONString(map));
    map.get(2);
    System.out.println("map.get(2): " + JSON.toJSONString(map));
}

执行结果:

初始化: {2:2,3:3,4:4,5:5}
map.get(3): {2:2,4:4,5:5,3:3}
map.get(2): {4:4,5:5,3:3,2:2}

可以看到,map 初始化的时候,我们放进去五个元素,但结果只有四个元素, 1 不见了,这个主要是因为我们覆写了 removeEldestEntry 方法,我们实现了如果 map 中元素个数大于 4 时,则就把队头的元素删除,当 put(5,5) 执行的时候,正好把队头的 1 删除,这个体现了当达到我们设定的删除策略时,会自动删除头节点。

当我们调用 map.get(3) 方法时,元素 3 移动到了队尾,调用 map.get(2) 方法时,元素 2 被移动到了队尾,这个体现了经常被访问的节点会被移动到队尾。

这个例子就很好的说明了访问最少删除策略,接下来看看原理。

1.2.2 元素被转移到队尾

为什么 get 时,元素会被移动到队尾:

public V get(Object key) {
    Node<K,V> e;
    // 调用 HashMap get 方法
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果设置了 LRU 策略
    if (accessOrder)
        // 这个方法把当前 key 移动到队尾
        afterNodeAccess(e);
    return e.value;
}

从上述源码中,可以看到,通过 afterNodeAccess 方法把当前访问节点移动到了队尾,其实不仅仅是 get 方法,执行 getOrDefaultcomputecomputeIfAbsentcomputeIfPresentmerge 方法时,也会这么做,通过不断的把经常访问的节点移动到队尾,那么靠近队头的节点,自然就是很少被访问的元素了。

1.2.3 删除策略

上述例子中我们在执行 put 方法时,发现队头元素被删除了,LinkedHashMap 本身是没有 put 方法实现的,调用的是 HashMap 的 put 方法,但 LinkedHashMap 实现了 put 方法中的调用 afterNodeInsertion 方法,这个方法实现了删除,源码如下:

// 删除很少被访问的元素,被 HashMap 的 put 方法所调用
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    // 得到链表的头节点
    LinkedHashMap.Entry<K,V> first;
    // 如果队列不为空,并且删除策略允许的情况下. removeEldestEntry 为我们覆写的删除策略
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        // 得到头节点的 key
        K key = first.key;
        // 删除头节点
        removeNode(hash(key), key, null, false, true);
    }
}

1.3 小结

LinkedHashMap 提供了两个很有意思的功能:按照插入顺序访问和删除最少访问元素策略,简单的通过链表的结构就实现了,设计的非常巧妙。

LinkedHashMap 在 HashMap 的基础上简单的增加了链表结构,就形成了节点的顺序,非常巧妙。

------------------------------------- END -------------------------------------

上一篇下一篇

猜你喜欢

热点阅读