LinkedHashMap详解

前言

HashMap是我们经常使用的存储对象,而LinkedHashMap却不常使用,以至于我们对这个类往往仅仅停留在知道:一个有序排列的键值对存储数据结构。本文就深入了解一下这个类。

用法

demo

    public static void main(String[] args) {
        System.out.println("-------HashMap--------");
        m1(new HashMap<>());
        System.out.println("-------LinkedHashMap--------");
        m1(new LinkedHashMap<>());
    }

    public static void m1(Map<String, String> map) {
        map.put("aaa", "1");
        map.put("bbb", "2");
        map.put("ccc", "3");

        map.forEach((k, v)  -> {
            System.out.println(k + " - " + v);
        });
    }

输出

-------HashMap--------
aaa - 1
ccc - 3
bbb - 2
-------LinkedHashMap--------
aaa - 1
bbb - 2
ccc - 3

几种构造方法

//默认构造方法:无参
Map<String, String> map = new LinkedHashMap<>();
//传入初始容量
Map<String, String> map2 = new LinkedHashMap<>(100);
//传入初始容量,扩容因子,是否以访问顺序排序
Map<String, String> map3 = new LinkedHashMap<>(100, 0.75f, true);

解释
LinkedHashMap作为Map的一个实现,可以像HashMap一样方便的使用多态机制。
后面会详细解释构造方法中的几个参数含义。

代码详解

继承关系

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

解释
LinkedHashMap是HashMap的子类,实现了Map接口。
所以LinkedHashMap有着HashMap的key-value存储数据的特性。

关键成员变量

    transient LinkedHashMap.Entry<K,V> head;//链表头节点引用(指针)

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;//链表尾节点引用(指针)

    /**
     * The iteration ordering method for this linked hash map: {@code true}
     * for access-order, {@code false} for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;//排序方式

解释
相对于HashMap,这是三个成员变量是LinkedHashMap所特有的,他们是实现子类特殊特性的关键成员变量。

  • head,tail 代表头和尾,类型是双向链表的节点(除了有next,还有before和after)(为什么有next了,还需要after?因为next是为了实现HashMap基础功能的,是为了实现HashMap中的链表或红黑树用的。二before和after是LinkedHashMap专门为了实现双向链表的。两者功能不同,无法共用)。表示存入map的数据,除了存在HashMap里,还在一个双向链表里存了一份。从而实现了顺序存取的效果。
  • accessOrder:控制存储顺序。这个参数可能不太好理解。我们一般认为,既然是顺序存储,当然是像List那样按插入的顺序存储呀。需要规定什么顺序吗? 是的,这里规定了第二种存储顺序:按访问时间排序,哪个刚访问(包括新增),哪个就排在前面。那些很久之前插入,并且一直没被访问过的数据就排在后面。也就意味着,当使用这种排序模式时,链表会不断进行重新排序。

顺序遍历

前面的demo示例中,展示了LinkedHashMap的顺序遍历特性,并且我们知道了是链表实现的顺序存取,下面就是forEach的源码:

    public void forEach(BiConsumer<? super K, ? super V> action) {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e.key, e.value);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }

解释
顺着链表从head一直往后迭代:e = e.after,实现顺序遍历map的特性。
类似的,LinkedHashMap中实现的迭代器也是依赖链表。

afterNodeAccess

节点被访问之后:把被访问的节点移动到队列尾部(move node to last)
在这里插入图片描述
p在队列里的三种情况示意图:
在这里插入图片描述

解释

  • 1、创建p前后两个指针 b,a,为后面操作做准备
  • 2、分三种情况(p在头,尾,中间),把p移出队列
  • 3、分两种情况(经过前面处理后:队列为空,不为空),把p加入队列尾部

LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
等效于:
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e;
LinkedHashMap.Entry<K,V> b = p.before;
LinkedHashMap.Entry<K,V> a = p.after;

afterNodeInsertion

插入节点之后:可能删除最老的节点

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

解释
入参evict控制了是否执行这样的逻辑
if条件中的removeEldestEntry(first)是一个钩子方法,同样控制删除与否,用于给子类实现,默认是返回false

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

如果重写成

private static final int MAX_ENTRIES = 100;
 
protected boolean removeEldestEntry(Map.Entry eldest) {
   return size() > MAX_ENTRIES;
}

就表示当数量超过100时删除,删除最老的元素。

为什么第一个元素(head)就是最老的元素?
因为根据前面afterNodeAccess可知道:每次访问元素,都会把该元素挪到队尾。那么,相对应的队头就是最长时间没被访问过的元素。
这就是所谓的LRU(Last Recently Used 最近最少使用)缓存淘汰算法。
其原理是 :如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很低,当数据所占据的空间达到一定阈值时,这个最少被访问的数据将被淘汰掉

afterNodeRemoval

删除节点之后
前面在执行removeNode(hash(key), key, null, false, true);之后,就会在该方法里执行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;
    }

解释
和afterNodeAccess类似,同样是创建当前节点前后两个指针b,a。
把b指向a,就把p节点排除掉了(同时考虑p点所在的三种可能位置:头,中间,尾)

小结

通过前面几个方法,我们对LinkedHashMap就有了一个基本的了解:

  • LinkedHashMap是在HashMap基础上增加了一个链表
  • 实现机制上并没有增加一个所谓”链表“的数据结构,而是
    继承并扩充了HashMap的Node节点,把HashMap的普通链表节点(HashMap为什么需要用链表节点来存储数据?),变成了双向链表节点(为什么要双向链表?),然后又增加了head,tail成员变量,一个”看不见的双向链表“就形成了。
  • 为什么HashMap需要用链表节点来存储数据:因为HashMap在遇到Hash冲突时,需要用链表或红黑树来把冲突的数据”串起来“。
  • 为什么LinkedHashMap需要双向链表:因为LinkedHashMap在实现顺序存储时,增加了”按最近访问时间“的机制来排序(accessOrder=true)。要实现这种特性,就需要在增加,访问节点时对节点进行重新排序(涉及到了删除节点),一旦涉及删除节点,就需要双向链表来快速(如果不是为了快速,单向链表也可以从头遍历找到节点)删除操作。
  • HashMap实现了很多”钩子函数“,比如afterNodeAccess(访问节点之后的操作),afterNodeInsertion(插入新节点之后的操作)…。LinkedHashMap作为HashMap的子类,很好的利用了这些钩子函数。通过重写,改写成符合自己特性的方法。从而实现”扩充HashMap“的效果(HashMap中的方法都还正常使用,只不过效果略有不同,这也就是所谓的Java多态

实现LRU缓存

通过前面的源码解析,我们知道LinkedHashMap有意让用户把LinkedHashMap实现为LRU缓存。只需要继承LinkedHashMap,重写唯一的一个protected方法即可。

demo

/**
 * 实现一个LRU算法淘汰策略的缓存
 */
public class MyLruCache extends LinkedHashMap {
    private int capacity;//缓存容量

    public MyLruCache(int capacity) {
        //缓存淘汰采用LRU算法策略
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

	//重写protected方法,控制删除元素的条件
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > capacity;
    }

    public static void main(String[] args) {
        MyLruCache mc = new MyLruCache(100);
        mc.put("aa", "aa");
    }
}

其中调用父类构造方法 super(capacity, 0.75f, true);
第一个参数是缓存容量
第二个参数,0.75f是HashMap的扩容因子默认值。具体原理请见【另一篇博客】
第三个参数accessOrder=true代表采用“访问顺序”排序,也就是LRU淘汰策略(见前面afterNodeAccess中的用法)

优化

既然作为缓存,一般情况下都是作为公共缓存,供多线程访问,保证并发安全是基本要求。

Map m = Collections.synchronizedMap(new MyLruCache(100));

通过Collections.synchronizedMap将Map转成线程安全的。

Collections.synchronizedMap其实就是通过装饰模式,做了一层套壳,给HashMap方法增加了线程同步标识(synchronized):
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}

总结

  • LinkedHashMap通过拓展HashMap的节点,构造出双向链表,从而实现出遍历性能还不错的链表结构。
  • LinkedHashMap是建立在HashMap的良好设计模式基础上,通过重写一些钩子函数,方便的拓展了HashMap的功能
  • LinkedHashMap可以方便的实现一个LRU(最近最少访问淘汰策略)的缓存。同时注意要把缓存转成线程安全的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值