JDK1.8集合之LinkedHashMap

175 阅读3分钟

1.介绍

  • LinkedHashMap是继承于HashMap,是基于HashMap双向链表来实现的
  • HashMap无序,LinkedHashMap的元素是有序的,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)
  • LinkedHashMap同样也是非线程安全的
  • LinkedHashMap可以用来实现LRU算法

2.LinkedHashMap的属性

// 静态内部类Entry继承自HashMap的Node,在其基础上加上了before和after两个指针实现双向链表特性
// 这两个属性保存前后节点的引用,将哈希表中所有的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);
    }
}
// 链表头结点
transient LinkedHashMap.Entry<K,V> head;
// 链表尾结点
transient LinkedHashMap.Entry<K,V> tail;
// 这里是指是否基于访问排序,默认为false
// 为true时,被访问的元素会被放到链表尾结点
// 为false时,按插入的顺序排序
final boolean accessOrder;

3.重要函数

afterNodeAccess、afterNodeInsertion、afterNodeRemoval这三个钩子函数,在父类HashMap中只是空实现,LinkedHashMap进行了重写,实现双向链表的维护

3.1 afterNodeAccess函数

把当前节点放到双向链表尾部,此函数在很多函数(如get,put)中都可能会被回调,LinkedHashMap重写了HashMap中的此函数。若访问顺序为true,且访问的对象不是尾结点

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    /* accessOrder就是我们前面说的LRU控制,当它为true,同时e对象不是尾节点
    (如果访问尾节点就不需要设置,该方法就是把节点放置到尾节点)
    */
    if (accessOrder && (last = tail) != e) {
        // 用a和b分别记录该节点前面和后面的节点
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // 释放当前节点与后节点的关系 
        p.after = null;
        // 如果当前节点的前节点是空
        if (b == null)
            // 那么头节点就设置为a
            head = a;
        else
            // 如果b不为null,那么b的后节点指向a
            b.after = a;
        // 如果a节点不为空
        if (a != null)
            // a的后节点指向b
            a.before = b;
        else
            // 如果a为空,那么b就是尾节点
            last = b;
        // 如果尾节点为空
        if (last == null)
            // 那么p为头节点
            head = p;
        else {
            // 否则就把p放到双向链表最尾处
            p.before = last;
            last.after = p;
        }
        // 设置尾节点为P  
        tail = p;
        // LinkedHashMap对象操作次数+1
        ++modCount;
    }
}


从上面的效果图中可以看到,结点3链接到了尾结点后面

3.2 afterNodeInsertion函数

在哈希表中插入了一个新节点时会调用afterNodeInsertion函数,而afterNodeInsertion函数调用了removeEldestEntry,如果返回true则删除链表的头节点。

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

removeEldestEntry也只是简单的返回false,换言之LinkedHashMap新增元素后默认是不会删除表头元素的。
可以通过重写removeEldestEntry方法,实现一个最简单的LRU缓存,代码如下:

 Map<String, String> map = new LinkedHashMap<String, String>(7, 0.75F, true){
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        //当LinkHashMap的容量大于等于5的时候,再插入就移除旧的元素
        return this.size() >= 5;
    }
};
for (int i = 0; i < 10; i++) {
    map.put(i, i);
}
System.out.println("map size is:"+map.size());
map.entrySet().iterator().forEachRemaining(System.out::println);

输出结果

map size is:5
5=5
6=6
7=7
8=8
9=9

3.2 afterNodeRemoval函数

当HashMap删除结点时调用的,afterNodeRemoval会把这个节点一并从链表中删除,保证了哈希表和链表的一致性

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