LinkedHashMap分析

199 阅读3分钟

1 简述

LinkedHashMap继承自HashMap,它的很多操作都基于HashMap实现,存储结构同样是数组+链表+红黑树(JDK1.8)。
同时,它里面维护着一个Entry的双向链表,保证了插入数据的顺序。

2 源码分析

这里主要分析核心的部分。
包括双向链表的节点结构Entry,维护双向链表的两个方法 afterNodeRemovalafterNodeAccess,以及重写的get方法。

Entry

/**
 * HashMap.Node subclass for normal LinkedHashMap entries.
 */
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);
    }
}

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

继承了HashMap的Node,并增加before、after前后指针。
创建头结点head,尾结点tail,记录信息。

afterNodeRemoval

LinkedHashMap的remove方法是继承自HashMap,同时自身实现了双向链表的维护。

// 在节点删除后,维护链表,传入删除的节点
void afterNodeRemoval(Node<K,V> e) {
    // p指向待删除元素,b执行前驱,a执行后驱
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // 这里执行双向链表删除p节点操作
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

afterNodeAccess

在某个节点被访问后,根据accessOrder的值,看看是否需要调整数据到双向链表尾部。
accessOrder在后面会提及。

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

get

LinkedHashMap的get方法是重写HashMap的。

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

这里的逻辑跟HashMap一样,通过key的hash值快速找到value。
如果accessOrder为true,就把节点挪到双向链表尾部。

小结

针对我们最常用的三个方法,getputremove,LinkedHashMap都是使用的HashMap那套逻辑。同时,为了维护自身的双向链表,LinkedHashMap实现了HashMap中的三个方法,

void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

这三个方法是HashMap专门留下用于扩展的,在数据被put、remove时都会经历其中的方法。
LinkedHashMap也是抓住这一点,实现了数据在被操作后,继续维护每一个节点的前后指针,保证双向链表的正确。

3 accessOrder

在看LinkedHashMap源码时,我们会看到这个布尔变量,默认为false。
这个值就相当于一个开关,
为false时,双向链表就按照正常的插入顺序进行排列。
当为true时,LinkedHashMap就会在节点被访问后(get、remove、put),将节点放在双向链表尾部,保证最近访问过的节点永远在节点尾部。
这个结构就可以用来实现LRU缓存。
如果数据量满了,就把头节点删除,头节点永远是最近的,最少被访问的节点。
下面是个demo,这个也是LeetCode上的146题!

class LRUCache extends LinkedHashMap {

    private int capacity;

    public LRUCache(int capacity) {
        //accessOrder为true
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return (int)super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > capacity; // 缓存数据满了,就删除最近的头节点
    }
}