【JDK1.8源码阅读】 LinkHashMap源码实现分析整理(二)

本文详细分析了LinkHashMap的数据结构,包括双向链表的实现,以及类定义、节点插入、访问和移除的回调机制。通过对`linkNodeLast`、`afterNodeAccess`、`afterNodeRemoval`和`afterNodeInsertion`方法的探讨,揭示了LinkHashMap如何维持元素的插入顺序,并可应用于LRU算法。同时,还介绍了`entrySet`的遍历方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LinkHashMap数据结构分析

LinkHashMap相对HashMap,记录了元素的插入顺序,在遍历时,可以根据元素的插入顺序进行反问,LinkHashMap的顺序是基于双向链表实现的,因而LinkHashMap同时具备栈和队列的基础功能。

数据结构如下:
image

LinkedHashMap会将元素串起来,形成一个双链表结构。可以看到,其结构在HashMap结构上增加了链表结构。数据结构为(数组 + 单链表 + 红黑树 + 双链表),图中的标号是结点插入的顺序。注意,在图中未展现出红黑树的特征,这个可以参照HashMap的数据结构。

类定义

下面先看看LinkedHashMap的基本成员属性,具体源码如下:

public class LinkedHashMap<K,V>  extends HashMap<K,V> implements Map<K,V> {
    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);
        }
    }

    // 链表头结点
    transient LinkedHashMap.Entry<K,V> head;

    // 链表尾结点
    transient LinkedHashMap.Entry<K,V> tail;

    // 访问顺序,如果为false,表示根据插入顺序进行访问
    // 如果为true表示之后访问顺序按照元素的访问顺序进行,不按照之前的插入顺序。
    final boolean accessOrder;
}

从源码中看到,LinkedHashMap中定义了Entry内部类,继承自HashMap.Node节点,具备Node的相关特性,同时还定义了Entry<K,V> before, after;,根据插入顺序用于索引遍历成员元素,在LinkedHashMap内部还定义了head和tail成员属性,用于记录整个双向链表的收尾节点,用于实现双向遍历。

LinkedHashMap继承自HashMap,具有HashMap的全部特性,下面结合源码分析LinkedHashMap的双向链表特性

linkNodeLast 插入双链表尾部

LinkedHashMap重写了HashMap的newNode和newTreeNode方法:

// 当桶中结点类型为HashMap.Node类型时,调用此函数
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    // 生成Node结点
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 将该结点插入双链表末尾
    linkNodeLast(p);
    return p;
}

// 当桶中结点类型为HashMap.TreeNode时,调用此函数
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    // 生成TreeNode结点
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    // 将该结点插入双链表末尾
    linkNodeLast(p);
    return p;
}

在生成新节点后,通过linkNodeLast建立维护新节点的双向链表关系。linkNodeLast的实现较为简单,主要用于更新tail节点为新节点

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    // 记录原来的尾节点
    LinkedHashMap.Entry<K,V> last = tail;
    // 记录新尾节点为p
    tail = p;
    if (last == null)
        // 仅第一次插入或元素清空后,原来的尾节点为空,此时同时记录首节点
        head = p;
    else {
        // 建立原来尾节点和新尾节点的双向链表关系
        p.before = last;
        last.after = p;
    }
}

afterNodeAccess 节点访问回调,设为尾节点

回调时机

在HashMap中,很多操作后都会回调这个方法,主要聚焦在需要访问而非移除这个节点的时候,除此之外,还有一个核心使用是在LinkedHashMap重写的get方法中:

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        // 如果获取是空节点,直接返回
        return null;
    if (accessOrder)
        // 如果accessOrder为true,回调
        afterNodeAccess(e);
    return e.value;
}

从上面看到可见仅有accessOrder为true时,且访问节点不等于尾节点时,在每次获取该节点时,都会回调。

用途原理分析

afterNodeAccess的主要用途是将访问的节点移到链表尾部,可以借此实现LRU(Least recently used,最近最少使用)算法

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    // 若访问顺序为true,且访问的对象不是尾结点
    if (accessOrder && (last = tail) != e) {
        // 向下转型,记录p的前后结点
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // p的后结点为空,因为p要移动到尾部
        p.after = null;
        // 维护->关系,如果p的前结点为空
        if (b == null)
            // 说明p是头节点,将p的后一节点a设为头结点
            head = a;
        else // p的前结点不为空
            // b的后结点为a,即移除对p的引用
            b.after = a;
        // 维护<-关系,p的后结点不为空
        if (a != null)
            // a的前结点为b
            a.before = b;
        else // p的后结点为空,p为尾节点,记录last为p的前一个节点
            last = b;
        // last=tail || last=b,分析last=null,则p为唯一有效节点
        if (last == null)
            // 记录头节点为p
            head = p;
        else { // p链入最后一个结点后面
            p.before = last;
            last.after = p;
        }
        // 尾结点为p
        tail = p;
        // 增加结构性修改数量
        ++modCount;
    }
}

以上函数的意义在于,加入链式关系为a->b->c->d,访问b后,会变成a->c->d->b。

afterNodeRemoval 节点移除回调

afterNodeRemoval在HashMap#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的双向引用
    p.before = p.after = null;
    // 没有前置节点,设头节点为p的后置节点
    if (b == null)
        head = a;
    else
        // 更新前置节点b对p的后置引用关系
        b.after = a;
    // 没有后置节点,设尾节点为p的后置节点
    if (a == null)
        tail = b;
    else
        // 更新后置节点a对p的前置引用关系
        a.before = b;
}

afterNodeInsertion 节点插入回调

afterNodeInsertion用于处理新元素插入后的回调,如果入参evict=true、存在元素且满足removeEldestEntry移除规则,则删除头部元素。
具体实现如下:

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    // 如果入参evict=true、存在元素且满足removeEldestEntry移除规则
    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;
}
    

在LinkedHashMap中,removeEldestEntry默认返回false,即不移除元素,我们可以在子类中拓展这个方法实现对头部元素的移除,如实现LRU算法时使用。

entrySet 遍历元素

遍历时,主要关注遍历顺序的相关实现:

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

final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
        if (action == null)
            throw new NullPointerException();
        // 缓存修改记录
        int mc = modCount;
        // 通过after进行顺序遍历
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e);
        // 并发修改检测
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

从上面看到,遍历时是从head开始,调用LinkedHashMap.Entry的after引用来实现顺序遍历。

参考

  1. https://www.cnblogs.com/leesf456/p/5248868.html
  2. https://segmentfault.com/a/1190000009498647
  3. Java并发编程的艺术
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值