LinkedHashMap 源码分析

·  阅读 610

LinkedHashMap 数据结构

这篇文章需要先了解 HashMap 的相关知识,详情可以参考这篇文章

由于 LinkedHashMap 是通过集成 HashMap 实现的,所以它本身会用到 HashMap 的数据结构,即:数组+链表(或红黑树, java8),这里主要分析 LinkedHashMap的数据结构,可以查看下图:

其中黑色部分是 HashMap 的数据结构(数组+单向链表),绿色箭头链接起来的是一个双向链表,是 LinkedHashMap 所维护的数据结构。

其实我们可以看到,它主要是在 HashMapNode 的基础上,添加了额外两个变量(指针),beforeafter ,供双向链表使用,根据构造函数的 accessOrder 参数以不同的顺序(访问顺序或者插入顺序)来连接 HashMap 中各个节点

这样通过增加很小的内存空间(beforeafter 两个指针),重复利用节点就可以实现有序性,当我们迭代 LinkedHashMap 中的元素就可以从链表的 HEAD 开始顺序读取元素,达到一种有序的遍历。

源码分析

这里我们主要挑选几个比较重要的方法来讲述 LinkedHashMap 是怎样维护链表的顺序的:get(Object)afterNodeInsertion(Node)afterNodeRemoval(Node)afterNodeAccess(Node)

讲这些方法前,我们需要先了解 LinkedHashMap.Entry 类,这个是存储节点数据的关键:

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);
    }
}
复制代码

也就是上图中的:

从这个类可以看到,LinkedHashMap.Entry<K,V> 是继承自 HashMap.Node<K,V> 的,并且比 HashMap.Node<K,V> 多了两个变量 beforeafter 用来指向节点的上一个节点和下一个节点。

通过覆写 HashMap.newNode() 方法来创建 LinkedHashMap.Entry<K,V> 节点:

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;
}
复制代码
  1. get(Object) 方法和 afterNodeAccess(Node) 方法。
public V get(Object key) {
    Node<K,V> e;
    // 通过 HashMap 的 getNode() 方法获得 key 对应的节点
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果是根据访问顺序来排序,则将节点添加至末尾
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

// afterNodeAccess具体实现
// 主要是维护双向链表的指针,删除对应节点,并将其添加至链表末尾
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;
    }
}
复制代码

只有当构造 LinkedHashMap 的时候 accessOrdertrue 时,才会根据访问顺序将元素放置双向链表最末尾。

  1. afterNodeInsertion(Node) 方法

这个方法是 HashMap.putVal() 中的一个钩子(hook),在添加了新的元素之后判断是否需要删除最早(插入的顺序,或者访问的顺序)的元素。这个方法会在后面的 LRU算法 中讲到。

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    // 判断是否需要删除节点
    // 其中 removeEldestEntry
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}
复制代码
  1. afterNodeRemoval(Node) 方法

这是 HashMap.removeNode() 中的一个钩子(hook)方法。

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;
}
复制代码

这里主要做的是也就是在双向链表中删除指定节点。

Tips: 可能小伙伴会有疑问,当链表转换成树的时候怎么办?即 HashMap 中当链表长度大于等于8时转换成红黑树。

其实我们看 HashMap 中树节点的类 TreeNode<K,V> 就可以知道,其实它是继承 LinkedHashMap.Entry<K,V> 的:

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<K,V> 树节点也当作一个双向链表节点使用。

为什么使用双向链表

因为这样我们可以很方便的删除元素和添加元素,时间复杂度基本是 O(1)。

我们可以利用 HashMap 在 O(1) 的时间找到节点(getNode 方法),然后通过这个节点的前后指针很容易的删除节点,而不需要像单向链表那样通过遍历链表来找这个节点的前一个节点,然后再删除。

LRU算法实现

LRU(Least Recently Used,最近最少使用的)算法主要应用于内存缓存中,在使用的内存达到限制的内存时需要淘汰最近最少使用的元素。这里利用 LinkedHashMap 很方便地实现这个算法。

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {

    private int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    // 当元素容量超过限制容量将返回 true
    // 然后 afterNodeInsertion 方法中会删除双向链表的 head 节点
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }

}
复制代码
分类:
代码人生
标签:
分类:
代码人生
标签: