集合框架(六) -- 基于HashMap+LinkedList的有序Map:LinkedHashMap详解

1. 前言:

LinkedHashMap是使用HashMap+LinkedList来实现Map接口。和HashMap的区别在于它维持一个双端链表保存所插入访问的节点。链表定义了迭代顺序,默认情况下顺序就是键值插入到Map的顺序(插入顺序)。
LinkedHashMap主要是基于HashMap中的方法来实现对数据结构的操作的,不同的是,LinkedHashMap维护了一个双向链表来保持顺序,而LinkedHashMap主要是定义了一些方法来维护双向链表。LinkedHashMap是基于HashMap的!!!
在这里插入图片描述
构造方法为LinkedHashMap(int,float,boolean)的可用来创建一个按照访问顺序迭代的LinkedHashMap,按照最少访问到最多访问的顺序链接结点,这种LinkedHashMap可用来实现LRU缓存。
另外由于jdk8的HashMap的源码发生了变化,这里我就拿jdk8以后的版本–jdk13的源码来进行学习了,jdk13相当于jdk8并没有做出较大的改变。
此外:想看HashMap源码解析的可以点击:
jdk8HashMap源码解析
jdk1.7HashMap源码解析

2. 定义

2.1 继承和实现

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

可以看出,LinkedHashMap是继承自HashMap的,LinkedHashMap的数据结构基本上和HashMap是相似的都是Node为基础,数组+链表/红黑树的形式来存储,只不过LinkedHashMap的Node多了类似于LinkedList的前标和后标,接下来我们看具体的结构。

2.2 重要字段

	 /**
     * 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; // 链表尾节点
    
	/**
     * The iteration ordering method for this linked hash map: {@code true}
     * for access-order, {@code false} for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder; // 迭代顺序 true表示访问顺序,false表示插入顺序

2.3 初始化

可以看出LinkedHashMap的构造方法基本上是调用HashMap的构造方法,同时对accessOrder进行赋值。
由于accessOrder是final修饰的,所以只能在初始化的时候进行赋值

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

public LinkedHashMap() {
    super();
    accessOrder = false;
}

public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

2.4 LinkedHashMap中的节点

在HashMap中的节点是Node,包括hash,后标,key和值。LinkedHashMap中的节点用的是Entry,继承了Node:(PS:HashMap中的TreeNode继承自该节点)

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

可以看出多了before和after2个节点,这个作用主要是用来存放排序链表中的前标和后标,注意是排序链表中的,不是hash桶节点中的链表。
用图表示就是:
在这里插入图片描述
在整个系统中就是:
在这里插入图片描述

2.5 整个HashMap的图就像如下的结构:

在这里插入图片描述

3. 插入

3.1 put(K key, V value):

假如我执行了如下方法:

LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("111", "111");

具体做了什么呢?

3.1.1 首先第一行,调用无参构造。

查看IDEA的debug模式:可以看到没有传入排序方式,所以默认按插入顺序,accessOrder为false
在这里插入图片描述
假如传入参数初始化:

 LinkedHashMap<String, String> map = new LinkedHashMap<String, String>(13, 0.75f, true);

在这里插入图片描述

3.1.2 然后第二行put方法做了什么:

这里的put调用的是HashMap的put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null); // LinkedHashMap重写了该方法
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // LinkedHashMap中重写
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); // LinkedHashMap中重写
    return null;
}

3.2 newNode方法:

首先我们来看newNode方法,LinkedHashMap重写了该方法:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
   LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail; // 此时tail为null
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

从代码中我们可以看到,首先创建了我们需要进行插入的节点p,然后如果tail尾节点为null,则我们新插入的节点是尾节点,同时也是头节点;否则p的前顺序节点是原last节点,原last节点的后顺序节点是p节点。

3.3 afterNodeInsertion(boolean evict)实现

afterNodeInsertion(boolean evict)方法,是基于size发生变化的情况。
另外,afterNodeInsertion方法的evict参数如果为false,表示哈希表处于创建模式。只有在使用Map集合作为构造器创建LinkedHashMap或HashMap时才会为false,使用其他构造器创建的LinkedHashMap,之后再调用put方法,该参数均为true。LinkedHashMap的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);
    }
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

可以看出,要想进入if语句,需要满足三个情况:

  1. evict为true。只要不是在构造方法中插入Map,evict就是true;
  2. head不为null。表明表不为空,可是之前说过这个方法只有在size发生变化,也就是变长时调用,也就是此时head一定不为空
  3. removeEldestEntry方法返回true,该方法从名字可以看出来是删除最老的节点。可是我们看到这个方法直接返回了false,什么情况,这个方法会返回true呢?我们需要重写这个方法。
    首先,为什么我们要删除最老的节点?比方说,假如我们的链表最多只能维持100个元素,当插入第101个时,我们就需要把最老的那个元素删除,这也就是LRU算法的理念,所以在这里我们可以通过LinkedHashMap实现LRU缓存。
public boolean removeEldestEntry(Map.Entry<K,V> eldest){
       return size()>100;
}

3.4 afterNodeAccess(Node e)实现

该方法目的是将当前节点放到顺序链表的最后,同时对顺序链表进行重排序。
在key重复时,会调用此方法,其中e表示插入的节点。该方法在accessOrder为true时才会生效,也就是在按读取顺序排序时才会生效。具体实现:

void afterNodeAccess(Node<K,V> e) { // move node to last
   LinkedHashMap.Entry<K,V> last;
   	// 满足按读取顺序排序,并且当前节点不是顺序链表的尾节点
    if (accessOrder && (last = tail) != e) {
    	// 这里定义p为当前节点,b为当前节点的前顺序节点,a为当前节点的后顺序节点
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null; // 将当前节点的后顺序节点设置为null
        if (b == null)
            head = a; // 如果当前节点的前节点为null,说明当前节点为顺序链表的头,那么就将当前节点的后节点设置为头
        else
            b.after = a; // 否则将当前节点的后节点设置为前节点的后节点(断开当前节点的前后联系)
        if (a != null)
            a.before = b; // 如果当前节点的后节点不为null,将当前节点的前节点设置为后节点的前节点
        else
            last = b; // 否则说明当前节点是尾,那么将前节点赋值给last,但是最上面if判断时,如果当前节点是尾节点,那么不会进入,这里我认为是容错
        if (last == null)
            head = p; // 如果尾是空,那么说明b为空,如果b为空,说明当前节点为链表原头节点,说明原链表中只有一个元素
        else {
            p.before = last; // 将当前节点的前节点设置为原尾节点
            last.after = p; // 将尾节点的后节点设置为当前节点
        }
        tail = p; // 将当前节点设为尾节点
        ++modCount;
    }
}

从中我们可以看出这个方法的目的主要是讲put的元素放到链表的尾端,同时修改链表的关联结构

4. 删除

LinkedHashMap的remove()方法根据键删除节点,如果哈希表中不存在键值,那么返回null。LinkedHashMap的remove()方法继承自HashMap的rmeove()方法,在将节点从链表或红黑树中移除后,调用afterNodeRemoval(Node e)方法,LinkedHashMap实现了该方法,其实现如下:

// e表示待删除的节点
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;
}

从上面可以看到,afterNodeRemoval()方法主要就是将节点从双端链表中移除,具体的步骤和afterNodeAccess较为类似,这里就不做重复讲解了。

5. 获取

LinkedHashMap的get方法用于根据键得到值,如果哈希表中不包含该键,那么返回null,注意如果accessOrder为true,说明根据读取顺序排序,要将当前的key放置到链表尾部其实现如下:

public V get(Object key) {
    Node<K,V> e;
    //如果哈希表中不存在该键,返回null
    if ((e = getNode(hash(key), key)) == null)
        return null;
    //如果accessOrder为true,即使用访问顺序
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

从上面代码可以看到,get()方法分为2步:

  1. 调用getNode()方法得到键对应的节点。如果节点为null,表明哈希表中不存在该键,那么返回null
  2. 如果哈希表中存在该键并且accessOrder为true,那么调用afterNodeAccess(e)将节点移到双端链表的尾部

6. 利用LinkedHashMap实现LRU算法缓存

6.1 首先我们先了解一下什么是LRU:

LRU即Least Recently Used,最近最少使用,也就是说,当缓存满了,会优先淘汰那些最近最不常访问的数据,最近访问的数据会被排到最后。比如:我们的链表最多只能维持100个元素,当插入第101个时,我们就需要把最老的那个元素删除。

6.2 基于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);
    }
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

上面说到,要想这个方法生效,需要重写removeEldestEntry方法。同时又因为LinkedHashMap初始化时可以传入一个参数accessOrder,如果传入的为true,LinkedHashMap链表就按照访问顺序倒序
那么很好,我们要想实现一个LRU缓存,只需要继承LinkedHashMap,并重写removeEldestEntry方法:

public class LRUCacheByLinkedHashMap extends LinkedHashMap {
    private int count;

    public LRUCacheByLinkedHashMap(int count) {
        super(count, 0.75f, true);
        this.count = count;
    }

    // 判断长度是否超过限制,超过返回true,否则返回false
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > count;
    }

    public static void main(String args[]) {
        LRUCacheByLinkedHashMap cache = new LRUCacheByLinkedHashMap(5);
        for (Integer i = 0; i < 5; i ++) {
            cache.put(i, i);
        }
        System.out.println("插入后:");
        System.out.println(cache);
        cache.get(2);
        System.out.println("读取2后:");
        System.out.println(cache);
        cache.put(5,5);
        System.out.println("插入5后:");
        System.out.println(cache);
    }
}

测试结果:

插入后:
{0=0, 1=1, 2=2, 3=3, 4=4}
获取2后:
{0=0, 1=1, 3=3, 4=4, 2=2}
插入5后:
{1=1, 3=3, 4=4, 2=2, 5=5}

从结果可以看出:

  1. LinkedHashMap时有序的
  2. 基于读取的顺序
  3. 如果长度超过了指定的长度,那么会删除最老的数据

参考:
https://segmentfault.com/a/1190000012964859
https://www.cnblogs.com/xrq730/p/5052323.html
https://blog.csdn.net/qq_19431333/article/details/73927738

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值