深入浅出LinkedHashMap

通过前面HashMap的介绍我们知道HashMap可以满足绝大多数使用Map的场景,但是就有几种情况是HashMap处理不了的。一种是想要按照自然顺序或者自定义顺序遍历键的情况;一种是想要输出的顺序和输入的顺序一致或者按照顺序来排序的情况。

针对这两种情况,分别可以采用TreeMap来进行处理和LinkedHashMap。今天这篇文章我们就来了解一下有关LinkedHashMap的奇怪知识。

介绍

可以看做是HashMap+LinkedList,它即使用HashMap操作数据结构,又使用LinkedList维护插入元素的先后顺序,内部采用双向的形式将所有元素连接起来。

除了可以保证遍历顺序,还可以在遍历的时候不用像HashMap一样遍历整个table而是只需要遍历header指向的双链表即可。(迭代的时间只跟entry的个数有关和table大小无关)

  • 继承自HashMap,大多数方法没有重写比如remove、put方法
  • 一个key被重新插入,顺序不受影响
  • 非同步,可以调用Collections来实现类同步
  • assess-ordered结构性的修改会响应遍历的顺序的
  • 迭代有序
  • 比HashMap多了一个双向链表的维护,大多数方法都由HashMap实现了。
  • 底层是散列表加双向链表
  • 插入的顺序是有序的(底层链表导致有序)
  • 可以用来实现最近LRU算法,是页面置换算法常用的一种。
  • 与HashMap一样,初始容量和装载因子对LinkedHashMap影响很大,但它遍历时初始容量是不受有序性
  • 其可以设置两种遍历顺序:
  • 访问顺序
  • 插入顺序(默认情况)
结构图

首先先看一下LinkedHashMap的类继承图:

LinkedMultiValueMap hashmap 转换 linkedhashmap转换为list_数据结构

分析

参数
  • 继承HashMap的Node节点,它是双向链表,包含前置指针和后置指针
  • 在构件新节点时,构件的是LinkedHashMap.Entry不再是Node。
方法
构造方法
  • 调用的是HashMap的构造方法,并且默认使用的是插入顺序。
put方法

LinkedHashMap并没有重写HashMap的put()方法,而是重写了put()方法需要调用的内部方法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;
    }
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;
        }
    }

从代码中我们可以得知,LinkedHashMap在添加第一个元素的时候,会把head赋值为第一个元素,等到第二个元素添加进来的时候,会把第二个元素的befer赋值为订一个元素,第一个元素的afterii第二个元素

get方法
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定义的方法获取对应的节点
  • 如果是访问顺序的时候,把该节点放到链表的最后面
remove方法

LinkedHashMap并没有重写remove方法,只是重写了afterNodeRemoval方法。

遍历方法

从内部维护的双链表的表头开始循环输出,因为遍历的是双向链表,而不是散列表,所以初始容量对遍历没有影响

访问顺序

LinkedHashMap不仅能维护插入的顺序,还能维护访问的顺序。其默认维护的是插入的顺序,但当初始化的时候,指定第三个参数为true时,此时会维护访问顺序,会将最近访问过的元素放到末尾。

如何维持访问顺序

LinkedHashMap是通过三个方法来维持访问顺序的分别是:

  • afterNodeAccess()会在调用get方法的时候被调用
  • afterNodeInsertion()会在调用put方法的时候被调用
  • afterNodeRemoval()会在调用remove方法的时候被调用

下面以afterNodeAccess()为例来说明一下

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 判断accessOrder的属性,确定是否要维护访问顺序
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            // 如果我们get的是表头数据,那么表头就需要更新为表头的后置
            if (b == null)
                head = a;
            else
                // 否则的话,把get的数据的前置节点和get的数据的后置节点连接
                b.after = a;
            // p不是队尾的情况
            if (a != null)
                // 把操作数的后置节点连接上操作数的前置节点
                a.before = b;
             // a等于空说明p的后置节点是空,即p是队尾
            else
                last = b;
            if (last == null)
                head = p;
            else {
                // 把操作数据的前置节点设置为队尾
                p.before = last;
                // 把刚才队尾的后置节点设置为操作数
                last.after = p;
            }
            // 执行队尾赋值
            tail = p;
            ++modCount;
        }
    }

整个方法看下来并不复杂,核心思想就是进行判空判断各种特殊情况,然后针对特殊情况和常规情况进行处理。

LinkedHashMap经典用法

既然LinkedHashMap可以维护访问的顺序,那么我们就可以用它来轻松实现一个采用FIFO替换策略的缓存,具体来说其有一个子类方法protected boolean removeEldestEntry**(Map.Entry<K,V>** eldest**) 。**

该方法的作用是告诉Map是否要删除“最老”的Entry,如果该方法返回true,最老的那个元素就会被删除。

在每次插入新元素以后LinkedHashMap会自动询问removeEldestEntry()是否要删除最老的元素。这样只需要在子类中重载该方法,当元素个数超过一定数量的时候,让其返回true,就可以实现一个固定大小的FIFO策略的缓存

/** 一个固定大小的FIFO替换策略的缓存 */
class FIFOCache<K, V> extends LinkedHashMap<K, V>{
    private final int cacheSize;
    public FIFOCache(int cacheSize){
        this.cacheSize = cacheSize;
    }

    // 当Entry个数超过cacheSize时,删除最老的Entry
    @Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
       return size() > cacheSize;
    }
}