JDK 集合LinkedHashMap源码解析

LinkedHashMap 简介

  • LinkedHashMap 内部维护了一个双向链表,能保证元素按照插入顺序访问,也能以访问顺序访问。
  • LinkedHashMap 可以看成是 LinkedList + HashMap
  • LinkedHashMap 继承 HashMap,拥有 HashMap 的所有特性,并且额外增加了按照一定顺序访问的特性LinkedHashMap 默认存储顺序为插入顺序,也可以按照访问顺序存储元素。

案例

@Test
public void test01(){
    HashMap hashMap = new HashMap();
    hashMap.put("name","gsh");
    hashMap.put("name","gsh2000");
    hashMap.put("age","21");
    hashMap.put("sex","man");
    hashMap.put("school","haust");
    hashMap.put("class","1814");
    System.out.println(hashMap);// key不允许重复,无序集合{school=haust, sex=man, name=gsh2000, class=1814, age=21}
    
    LinkedHashMap linkedHashMap = new LinkedHashMap();
    linkedHashMap.put("name","gsh");
    linkedHashMap.put("name","gsh2000");
    linkedHashMap.put("age","21");
    linkedHashMap.put("sex","man");
    linkedHashMap.put("school","haust");
    linkedHashMap.put("class","1814");
    System.out.println(linkedHashMap);// key不允许重复,有序集合{name=gsh2000, age=21, sex=man, school=haust, class=1814}
    // LinkedHashMap 默认存储顺序为插入顺序,也可按照访问顺序存储元素
}

LinkedHashMap 继承体系

TXDOzT.png

HashMap 使用的是(数组 + 单链表 + 红黑树)的存储结构,通过上面的继承体系,我们知道LinkedHashMap 继承了 HashMap,所以它的内部也是有这三种结构,但是它还额外的添加了一种双向链表的结构存储所有元素的顺序

添加删除元素的时候需要同时维护在 HashMap 中的存储,也要维护在 LinkedList 中的存储,所以性能上来说会比 HashMap 稍微慢一些

LinkedHashMap 源码分析

属性

// 序列化版本号
private static final long serialVersionUID = 3801124242820219131L;

/**
 * 双向链表的头结点
 * 被transient关键字修饰的对象不能被序列化
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * 双向链表的尾节点
 */
transient LinkedHashMap.Entry<K,V> tail;

/**
 * 是否按照访问顺序排序
 * true:按照访问顺序排序
 * false:按照插入顺序排序
 */
final boolean accessOrder;
  • head:双向链表的头结点,旧数据存在头节点
  • tail:双向链表的尾节点,新数据存在尾节点
  • accessOrder:是否需要按照访问顺序排序,如果为false则按照插入顺序排序

内部类

/**
 * 位于 LinkedHashMap 中 HashMap的Node的子类
 * 增加了前驱节点和后继节点q
 */
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);
    }
}

// HashMap 中的节点类
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key; // 会经过一个f(key)转化为我们的hash值,转化为哈希值之后会经过一个扰动函数来使得哈希值更加的散列
    V value;
    // 为什么Node节点内部会出现一个Node对象,因为Hash会出现碰撞,碰撞之后就会形成链表,所以就出现一个Node对象的next来碰撞之后形成链表
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    ...
}

存储节点,继承自 HashMap 的 Node 类,next 用于哈希冲突的时候单链表存储于桶中,beforeafter 用于双向链表存储所有元素。

构造方法

// 默认为false,则迭代的时候输出的顺序是插入节点的顺序。若为true,则输出的顺序是按照访问节点的顺序
// 为true的时候,可以在这基础之上构建一个 LRUCache
final boolean accessOrder;

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

// 指定初始化时的容量
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

//指定初始化时的容量,和扩容的加载因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

//指定初始化时的容量,和扩容的加载因子,以及迭代输出节点的顺序
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

//利用另一个Map 来构建,
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    //该方法上文分析过,批量插入一个map中的所有数据到 本集合中。
    putMapEntries(m, false);
}

前四个构造方法 accessOrder 都等于 false,说明双向链表是按照插入顺序存储元素的

最后一个构造方法 accessOrder 从构造方法参数传入,如果传入 true,则就实现了按照访问顺序存储元素,这也是实现 LRU 缓存策略的关键。

成员方法

LinkedHashMap 并没有重写任何 pit 方法,但是其重写了构建新结点的 newNode()方法。

newNode()会在HashMap 中的 putVal()方法里面被调用,putVal()方法会在批量插入数据putMapEntries(Map<? extends K,? extends V> m, boolean evict)或者插入单个数据 public V put(K key, V value)的时候被调用。

LinkedHashMap 重写了 newNode() ,在每次构建新节点的时候,通过 linkNodeLast(p);新结点链接在内部双向链表的尾部

// 在构建新结点的时候,构建的是 LinkedHashMap.Entry 不再是 HashMap.Node
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;
    }
}

以及 HashMap 专门预留给 LinkedHashMapafterNodeAccess()、afterNodeInsertion()、afterNodeRemoval() 方法。

// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
// 回调函数,新节点插入之后回调,根据 evict(表示是否要实现LRUCache) 和判断当前双向链表是否为空
// 来达到是否需要删除最老插入的节点,如果实现 LRUCache会用到这个方法
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    // removeEldestEntry方法 LinkedHashMap 默认返回 false,则不删除节点
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

// LinkedHashMap 默认返回 false,则不删除节点,返回true,代表要删除最早的节点。
// 通常构建一个 LRUCache 会在达到Cache的上限的时候返回true
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

void afterNodeInsertion(boolean evict)以及boolean removeEldestEntry(Map.Entry<K,V> eldest)是构建LruCache需要的回调,在LinkedHashMap里可以忽略它们。

LinkeedHashMap 也没有重写 remove() 方法,因为它的删除逻辑和 HashMap 并无区别

但是它重写了 afterNodeRemoval() 这个回调方法,该方法会在 Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) 方法中回调,removeNode() 会在所有涉及到删除节点的方法中被调用,是删除节点操作的真正执行者。

// 删除节点的时候的回调方法
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的后置节点指向a
        b.after = a;
    // 如果后置节点a为空,说明待删除节点是尾节点
    if (a == null)
        tail = b;
    else
        // 否则的话将其后置节点的前置节点指向b
        a.before = b;
}

LinkedHashMap 重写了 get() 和 getOrDefault() 方法:

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

public V getOrDefault(Object key, V defaultValue) {
   Node<K,V> e;
   if ((e = getNode(hash(key), key)) == null)
       return defaultValue;
   if (accessOrder)
       afterNodeAccess(e);
   return e.value;
}

对比 HashMap 中的实现,LinkedHashMap 只是增加了在成员变量(构造函数时赋值) accessOrder 为 true 的情况下,要去回调 void afterNodeAccess(Node<K,V> e) 函数。

// 根据对应的key值获取对应的value
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

afterNodeAccess() 函数中,会将当前被访问到的节点 e,移动至内部的双向链表的尾部

// 将当前被访问到的节点 e 移动到双向链表的尾部
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last; // 原双向链表尾节点
    // 如果 accessOrder是true,且原尾节点不等于当前访问的节点e
    if (accessOrder && (last = tail) != e) {
        // 节点e强转成双向链表节点p
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // p是要加入到双向链表尾部的,后置节点一定是null
        p.after = null;
        // 如果p的前置节点为null,说明p以前是头结点,所以更新现在的头结点是p的后置节点a
        if (b == null)
            head = a;
        else
            // 否则更新 p的前置节点b的后置节点为a
            b.after = a;
        // 如果p的后置节点a不是null,则更新后置节点a的前置节点为b
        if (a != null)
            a.before = b;
        else
            // 如果原本p的后置节点是null,则p就是尾节点,此时,更新last的引用为p的前置节点b
            last = b;
        // 如果尾节点为null,则链表中就只有一个节点
        if (last == null)
            head = p;
        else {
            // 否则更新当前节点p的前置节点为原尾节点 last,last的后置节点为p
            p.before = last;
            last.after = p;
        }
        // 尾节点的引用赋值为p
        tail = p;
        // 修改modCount
        ++modCount;
    }
}

值得注意的是,afterNodeAccess()函数中,会修改 modCount,因此当你正在 accessOrder=true的模式下,迭代 LinkedHashMap的时候,如果同时查询访问数据,也会导致 fail - fast,因为迭代的顺序已经改变。

fail-fast 机制

即快速失败机制,即 java 集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生 fail-fast 机制,即抛出 ConcurrentModificationException 异常。fail-fast 机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅仅用于检测 bug。

遍历

重写了 entrySet()如下:

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    // 返回 LinkedEntrySet
    return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}

final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
    ...
}

最终的 EntryInterator

final class LinkedEntryIterator extends LinkedHashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

// 迭代器 LinkedHashMap 的迭代器
abstract class LinkedHashIterator {
    // 下一个节点
    LinkedHashMap.Entry<K,V> next;
    // 当前节点
    LinkedHashMap.Entry<K,V> current;
    // 希望修改的结构次数
    int expectedModCount;

    // 从构造方法里面我们知道,为什么每次我们使用迭代器都要从iterator.next()来作为第一个元素了
    // 因为空构造方法里面当前节点默认为空,next存储的是头结点,所以要获取next
    LinkedHashIterator() {
        // 初始化的时候,next为 LinkedHashMap 内部维护的双向链表的头结点
        next = head;
        // 记录当前modCount,以满足fail-fast机制
        expectedModCount = modCount;
        // 当前节点为null
        current = null;
    }

    // 判断是否还有next
    public final boolean hasNext() {
        // 就是判断next是否为null,默认next是head,表头
        return next != null;
    }

    // nextNode() 就是迭代器中的next方法
    // 该方法的实现可以看出,迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出
    final LinkedHashMap.Entry<K,V> nextNode() {
        LinkedHashMap.Entry<K,V> e = next;
        // 保证fail-fast机制,保证在迭代的时候集合结构不会发生变化
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        // 如果要返回的节点是null,异常
        if (e == null)
            throw new NoSuchElementException();
        // 更新当前节点为e
        current = e;
        // 更新下一个节点为e的后置节点
        next = e.after;
        // 返回e
        return e;
    }

    // 删除方法,最终还是调用了HashMap的removeNode方法
    public final void remove() {
        Node<K,V> p = current;
        //如果当前节点为null,则抛出异常
        if (p == null)
            throw new IllegalStateException();
        // 保证了fail-fast
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        // 删除节点的时候集合结构发生了变化,则修改expectedModCount的值来保证之后的操作的fail-fast
        expectedModCount = modCount;
    }
}

值得注意的就是:nextNode()就是迭代器里面的 next()方法。

该方法的实现可以看出,迭代 LinkedHashMap,就是从内部维护的双链表的表头开始循环输出

而双链表节点的顺序在 LinkedHashMap增、删、改、查时都会更新,以满足按照插入顺序输出,还是访问顺序输出

afterNodeInsertion(boolean evict) 方法

在节点插入之后做些什么,在 HashMap 中的 putVal() 方法中被调用,可以看到 HashMap 中这个方法的实现为空。

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

evict,驱逐的意思。

  • 如果 evixt 为 true,且头结点不为空,且确定移除最老的元素,那么就调用 HashMap.removeNode() 把头结点移除(这里的头结点是双向链表的头结点,而不是某个桶中的第一个元素);
  • HashMap.removeNode() 从HashMap 中把这个节点移除之后,会调用 afterNodeRemoval() 方法;
  • afterNodeRemoval()方法在LinkedHashMap中也有实现,用来在移除元素后修改双向链表
  • 默认removeEldestEntry()方法返回false,也就是不删除元素。

总结

  • LinkedHashMap 继承自 HashMap ,具有 HashMap 的所有特性
  • LinkedHashMap 内部维护了一个双向链表存储所有的元素
  • 如果 accessOrder 为 false,则可以按插入顺序来遍历元素
  • 如果 accessOrder 为 true,则可以按照访问元素的顺序遍历元素
  • LinkedHashMap 的实现非常精妙,很多方法都是在HashMap 中留的钩子(Hook),直接实现这些 Hook 就可以实现对应的功能了,并不需要再重写 put() 等方法;
  • 默认的 LinkedHashMap 并不会移除旧元素,如果需要移除旧元素,则需要重写 removeEldesEntry() 方法设定移除策略
  • LinkedHashMap 可以用来实现 LRU 缓存淘汰策略

LinkedHashMap 相对于 HashMap 的源码比,是很简单的,因为大树底下好乘凉。它继承了 HashMap,仅仅重写了几个方法,以 改变它迭代遍历时的顺序,这也是其与 HashMap相比最大的不同。

在每次插入数据、或者访问、修改数据 的时候,会增加节点、或者调整链表的节点顺序。以决定迭代时输出的顺序。

  • accessOrder,默认为 false,则迭代的时候输出的顺序是插入节点的顺序,若为true,则输出的顺序是按照访问节点的顺序,为 true 的时候,可以再这基础之上构建一个 LRUCache

  • LinkedHashMap 并没有重写任何 put 方法。但是其重写构建了新节点的 newNode()方法,在每次构建新节点的时候,将新节点链接在内部双向链表的尾部

  • accessOrder = true的模式下,在 afterAccessNode()函数中,会将当前被访问到的节点 e,移动至内部的双向链表的尾部。值得注意的是,afterAccessNode()函数中,会修改 modCount,因此当你正在 accessOrder = true的模式下,迭代LinkedHashMap的时候,如果同时查询访问数据,也会导致 fail-fast机制,因为迭代的顺序已经改变。

  • nextNode() 就是 迭代器里面的 next()方法

    该方法的实现可以看出,迭代LinkedHashMap,就是从内部维护的双向链表的表头开始循环输出

    而双向链表的节点的顺序在 LinkedHashMap增、删、改、查的时候都会更新,以满足按照插入顺序输出,还是访问顺序输出

  • 它与 HashMap 比,还有一个小小的优化,重写了 containsValue()方法,直接遍历内部链表去比对 value 值是否相等。

扩展

LinkedHashMap 如何实现 LRU 缓存淘汰策略的呢

首先,我们先来看看 LRU 缓存淘汰策略,LRU,Least Recently Userd,最近最少使用,也就是优先淘汰最近最少使用的元素。

使用 LinkedHashMap 实现 LRU 的必要前提就是将 accessOrder 标志位设置为 true 以便开启按照访问顺序排序的模式。我们可以看到,无论是 put() 方法(当Hash碰撞的时候且key值相同,替换value的情况)还是 get() 方法,都会导致Entry 成为最近访问的 Entry,因此就把该 Entry 加入到了双向链表的尾部。get() 方法 通过if (accessOrder)afterNodeAccess(e);来实现,put() 方法在覆盖已有 key 的情况下也通过 afterNodeAccess(e) 实现。这样,我们把最近使用的 Entry 放入到了双向链表的后面。多次操作后,双向链表前面的 Entry 便是最近没有使用,这样当节点个数满的时候,删除最前面的 Entry 即可,因为它就是最近最少使用的 Entry。

/**
 * @author wcc
 * @date 2022/1/5 16:11
 */
public class LRUTest {
  @Test
  public void test() {
    // 创建一个只有5个元素的缓存
    LRU<Integer,Integer> lru = new LRU<>(5, 0.75f);
    lru.put(1,1);
    lru.put(2,2);
    lru.put(3,3);
    lru.put(4,4);
    lru.put(5,5);
    lru.put(6,6);
    lru.put(7,7);

    System.out.println(lru.get(4));

    lru.put(6,666);

    Iterator iterator = lru.entrySet().iterator();
    while (iterator.hasNext()){
      // 输出:{3=3  5=5  7=7  4=4  6=666}
        // 可以看到最旧的元素被删除了
        // 且最近访问的4和key值覆盖的6被移动到了双向链表的末尾
      System.out.print(iterator.next() + "  ");
       
    }
  }
}
class LRU<K,V> extends LinkedHashMap<K,V> {
  // 保存缓存的容量
  private int capacity;

  public LRU(int capacity, float loadFactor){
    super(capacity, loadFactor,true);
    this.capacity = capacity;
  }

  /**
   * LinkedHashMap 的LRU淘汰策略要重写该方法来设置何时移除旧元素
   */
  @Override
  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    // 当元素的个数大于缓存的容量,就移除旧元素
    return size() > this.capacity;
  }
}

本文参考:https://blog.csdn.net/weixin_43591980/article/details/112510142?spm=1001.2014.3001.5501

https://blog.csdn.net/justloveyou_/article/details/71713781

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值