Java集合之LinkedHashMap

LinkedHashMap是HashMap的子类,通过维护一个双向链表,实现Map有序遍历元素的特性。

因此,对于LinkedHashMap来说,其基本特性如下:

基本特性 结论
元素是否允许为null key和value可以为null
元素是否允许重复 key重复会覆盖,value可以重复
是否有序
是否线程安全

源码分析

本文使用的是JDK 1.8.0_201的源码。

成员变量

LinkedHashMap是继承HashMap的成员变量,实现有序的特性,还维护了额外几个成员变量:

成员变量 作用
transient LinkedHashMap.Entry<K,V> head; 链表的头部
transient LinkedHashMap.Entry<K,V> tail; 链表的尾部
final boolean accessOrder; 遍历的模式,默认是false

put操作

LinkedHashMap没有实现自己的put操作,继承自HashMap。问题在于,LinkedHashMap是怎么维护元素的插入顺序的呢?换句话说,LinkedHashMap的双向链表是在哪里维护的?答案是在HashMap put方法中调用的模板方法newNode()。HashMap中存在许多的模板方法,以方便LinkedHashMap去实现自己的特性。

对于LinkedHashMap来说,newNode()方法实现如下:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    // 创建一个Entry
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 维护双向链表
    linkNodeLast(p);
    return p;
}

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

get操作

LinkedHashMap自己实现了get()方法,代码如下:

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 与HashMap的区别在这里
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

上面的代码与HashMap的区别在于accessOrder条件的判断,如果accessOrder为true,程序就会去执行afterNodeAccess(e)方法。

LRU缓存实现

accessOrder变量,是LinkedHashMap中一个有趣的属性。我们先看源码文档注释:

A structural modification is any operation that adds or deletes one or more mappings or, in the case of access-ordered linked hash maps, affects iteration order. In insertion-ordered linked hash maps, merely changing the value associated with a key that is already contained in the map is not a structural modification. In access-ordered linked hash maps, merely querying the map with get is a structural modification.

对于insertion-ordered模式来说,即accessOrder为false的情况,只有添加和删除操作,才会改变元素的顺序。而对于access-ordered模式来说,即accessOrder为true的情况,即使是get()操作,都会改变元素的顺序。

access-ordered模式,是实现LRU缓存(最近最少使用)的关键。所谓最近最少使用,就是说当缓存满了,优先淘汰那些最近最少被访问的数据。

LinkedHashMap的具体实现就是,每次访问时,都把访问的数据移到双向队列的尾部,那么队列头部的元素就是最少被访问的数据,每次要淘汰数据时,就删除队列头部的数据。

回过头再看前面get操作中的afterNodeAccess(e)方法,就会发现这个方法正是每次访问时,把访问数据移到队尾的关键。

总结

1. LinkedHashMap是有序的吗?它是如何保证有序的?

LinkedHashMap通过维护一个双向链表,从而保证元素的顺序。需要注意的是,LinkedHashMap保证的有序是指元素的插入顺序或者访问顺序,而不是指元素字典顺序。至于LinkedHashMap具体保证的是插入顺序还是访问顺序,通过初始化时accessOrder字段的值来确定。如果accessOrder=true,表示访问顺序,false表示插入顺序。

2. 如何使用LinkedHashMap实现一个LRU缓存?

LinkedHashMap有两种维护顺序的模式,insetion-ordered模式跟access-ordered模式,当成员变量accessOrder为true,即access-ordered模式时,每次访问元素时,就会将元素移到队列的尾部,这样头部的元素就成了最少被访问的元素。当需要淘汰数据时,头部的元素先被淘汰。这个特性非常适合用来实现LRU缓存。

至于何时淘汰数据,LinkedHashMap提供了一个模板方法removeEldestEntry()。具体做法如下:

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int size;

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

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 当缓存的元素个数大于设置的大小时,淘汰最老的那个元素
        return this.size() > this.size;
    }
}
3. 为什么LinkedHashMap的遍历性能会比HashMap好?

因为LinkedHashMap的遍历是通过双向链表实现的,与size的大小有关,即与map的实际元素个数有关。与此同时,HashMap的遍历,是需要遍历底层数组的,举个例子,一个只存放了一个元素的HashMap,其底层数组的大小至少是16,那么这个遍历有15次是多余的。由于负载因子的存在(不考虑负载因子大于0的情况),HashMap的遍历总是会存在多余的次数。

原文地址:https://www.cnblogs.com/bluemilk/p/10733215.html