LinkedHashMap源码分析

本文深入剖析了LinkedHashMap的工作原理,包括其内部结构、如何保持插入顺序以及实现方式等关键信息。

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

LinkedHashMap是HashMap的一个子类,底层存储结构和HashMap一样,也是数组+Entry链表,不过这个Entry和HashMap中的Entry稍微有点区别,后面会详细介绍。put操作和get的大部分操作都是调用的HashMap方法来完成的.LinkedHashMap比HashMap多了什么功能呢?多了一个通过迭代器迭代所有元素时可以按照插入时的顺序进行迭代的功能。这个维持插入顺序的功能依赖了LinkedHashMap的一个header属性,这个属性是一个双向链表结构,迭代时就从header开始往后一个个的迭代。我们可以通过源码来一步步分析LinkedHashMap的主要功能。
先看类层次结构和主要属性

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
    //双向链表的头 header.after就是第一个元素,header本身是不存元素的,java中的链表的header应该都是这种结构 傀儡节点
    private transient Entry<K,V> header;
    private final boolean accessOrder;
 }

Entry是一个内部类,是HashMap.Entry的一个子类,是LinkedHashMap双向链表实现的数据结构,主要的代码如下:

    private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // 前驱和后继指针,为了迭代时保持插入顺序
        Entry<K,V> before, after;
        //调用HashMap.Entry的构造方法
        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }

        //每次调用put方法新增元素时,在指定的元素前面添加元素
        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }
    }

了解数据结构,存元素和取元素的方法必须要了解的,我们先看存元素的方法:put方法,这个方法是在父类HashMap中定义的,关于我的一篇HashMap文章:http://blog.youkuaiyun.com/wangjiang87/article/details/78669217,这里为了方便源码分析,还是把涉及到HashMap中的代码也复制了出来,put方法源码如下:

public V put(K key, V value) {
    //null会存放到数组下标为0的位置
    if (key == null)
        return putForNullKey(value);
    //根据hashCode算出一个hash值
    int hash = hash(key.hashCode());
    //根据hash和数组长度计算出元素要存放的下标
    int i = indexFor(hash, table.length);
    //这个循环是遍历下标i处的元素e,如果e.next不为null说明有hash冲突,形成了一个链表
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //hash值一样并且key一样,说明要放的元素之前已经放过了,这时就替换而不执行下面的addEntry方法
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    //新创建一个Entry对象放到下标i处,这个方法在LinkedHashMap中进行了重写
    addEntry(hash, key, value, i);
    return null;
}

接着看addEntry方法在LinkedHashMap中是如何进行重写的,源码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
    //根据参数创建一个Entry对象放到数组的bucketIndex处
    createEntry(hash, key, value, bucketIndex);

    // Remove eldest entry if instructed, else grow capacity if appropriate
    Entry<K,V> eldest = header.after;
    //这个LinkedHashMap中直接返回的false,不需要删除最老的元素,有些实现LRU功能的LinkedeHashMap子类可能会重写此方法
    if (removeEldestEntry(eldest)) {
        removeEntryForKey(eldest.key);
    } else {
        //判断是否需要扩容
        if (size >= threshold)
            resize(2 * table.length);
    }
}

接着分析addEntry方法中调用到的createEntry方法,看看这个方法中做了什么操作:

void createEntry(int hash, K key, V value, int bucketIndex) {
    //先把指定下标处的元素取出来,如果hash冲突时,这个下标位置会已经有值,old元素会放在链表尾部,若hash没冲突,old肯定为null
    HashMap.Entry<K,V> old = table[bucketIndex];
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
    //将新创建的Entry对象e放到指定下标处
    table[bucketIndex] = e;
    //将新创建的元素添加到头为header的这个链表的尾部,就是这个链表维护了元素的插入顺序,遍历的时候才能通过header一个个的按照插入顺序读取元素
    e.addBefore(header);
    size++;
}

addBefore方法是LinkedHashMap.Entry中的方法:

private void addBefore(Entry<K,V> existingEntry) {
    after  = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
}

这个方法的作用就是在header之前插入一个元素,也即是在链表尾部插入元素,自己可以画下这个指针的指向情况。

以上方法就将LinkedHashMap存元素进行的操作方法介绍完了,取元素的get方法相对来说就简单多了,主要就调用了HashMap中的getEntry方法,这个可以参考HashMap文章中的介绍,这里就不在赘述了。

public V get(Object key) {
    Entry<K,V> e = (Entry<K,V>)getEntry(key);
    if (e == null)
        return null;
    e.recordAccess(this);
    return e.value;
}

上面的如果都理解了,LinkedHashMap的迭代器也就容易理解多了。

 private abstract class LinkedHashIterator<T> implements Iterator<T> {
    //header.after是LinkedHashMap中的第一个元素,header是傀儡节点,不存放真正的数据
    Entry<K,V> nextEntry    = header.after;
    Entry<K,V> lastReturned = null;

    //用来快速失败
    int expectedModCount = modCount;

    public boolean hasNext() {
            return nextEntry != header;
    }

    public void remove() {
        if (lastReturned == null)
        throw new IllegalStateException();
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();

            LinkedHashMap.this.remove(lastReturned.key);
            lastReturned = null;
            expectedModCount = modCount;
    }
    //从header.after开始,知道最后一个元素
    Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
            if (nextEntry == header)
                throw new NoSuchElementException();

            Entry<K,V> e = lastReturned = nextEntry;
            nextEntry = e.after;
            return e;
    }
}

从源码可以看出来,遍历的时候就跟LinkedHashMap中的数组Entry[]没任何关系了,使用的是header属性进行遍历,这个header就是保存了插入顺序的双向链表,从header.after开始,直到遍历完。

行文至此,就将LinkedHashMap的主要功能的代码介绍完了。再次强调下保持顺序的主要是持有了一个Entry类型的header,这个Entry有before和after两个指针,每次新增元素时,都会在after上进行重新指向,遍历的时候就是通过header开始找after,然后找after的after,以此类推,就做到了有序性,而put和get的时候还是和HashMap一样,都是存放在了Entry[]中。

资源下载链接为: https://pan.quark.cn/s/22ca96b7bd39 在当今的软件开发领域,自动化构建与发布是提升开发效率和项目质量的关键环节。Jenkins Pipeline作为一种强大的自动化工具,能够有效助力Java项目的快速构建、测试及部署。本文将详细介绍如何利用Jenkins Pipeline实现Java项目的自动化构建与发布。 Jenkins Pipeline简介 Jenkins Pipeline是运行在Jenkins上的一套工作流框架,它将原本分散在单个或多个节点上独立运行的任务串联起来,实现复杂流程的编排与可视化。它是Jenkins 2.X的核心特性之一,推动了Jenkins从持续集成(CI)向持续交付(CD)及DevOps的转变。 创建Pipeline项目 要使用Jenkins Pipeline自动化构建发布Java项目,首先需要创建Pipeline项目。具体步骤如下: 登录Jenkins,点击“新建项”,选择“Pipeline”。 输入项目名称和描述,点击“确定”。 在Pipeline脚本中定义项目字典、发版脚本和预发布脚本。 编写Pipeline脚本 Pipeline脚本是Jenkins Pipeline的核心,用于定义自动化构建和发布的流程。以下是一个简单的Pipeline脚本示例: 在上述脚本中,定义了四个阶段:Checkout、Build、Push package和Deploy/Rollback。每个阶段都可以根据实际需求进行配置和调整。 通过Jenkins Pipeline自动化构建发布Java项目,可以显著提升开发效率和项目质量。借助Pipeline,我们能够轻松实现自动化构建、测试和部署,从而提高项目的整体质量和可靠性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值