LinkedList源码专题

本文详细解析了LinkedList的继承结构、数据模型(双向链表)、构造方法、add()和remove()方法,以及迭代器的工作原理。强调了LinkedList是非线程安全的,并讨论了其在不同场景下的适用性。

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

一、LinkedList的继承结构

LinkedList除了继承AbstractSequentialList实现连续访问,同时还实现了Deque接口,也就是说LinkedList可以被当做队列使用。

二、数据结构

LinkedList 的底层数据结构为双向链表,每个节点包含两个引用,prev指向当前节点前一个节点,next指向当前节点后一个节点,可以从头结点遍历到尾结点,也可以从尾结点遍历到头结点。

三、构造方法

LinkedList有两个构造方法,首先是无参的构造方法,没有任何的实现代码,因为底层数据结构使用的双向链表,所以无需像ArrayList一样指定初始长度。

第二个构造方法,可以传入Collection接口实现类的集合对象,将集合内的元素使用addAll添加到LinkedList内部。

四、add()方法

在方法内部,将元素添加到集合中,在这个方法中调用了linkLast()方法。

在linkLast()方法内部,使用了last节点,last节点表示链表的最后一个节点,之后将要添加的元素包装成一个Node节点,将last赋值为新的节点,如果最后一个节点为null,则说明这将是链表的第一个节点,则将first即链表第一个节点也赋值为newNode,否则将之前最后一个节点l的next引用指向新的节点,并且将LinkedList元素个数加1,将modCount并发修改数量加1,modCount的作用是用来实现快速失败机制。

上面使用的Node类就是对元素的包装类,表示LinkedList中的一个一个元素,

它的结构是如下,item用来保存元素,prev、next分别保存了前一个节点和后一个节点,这也验证了之前所描述的数据结构。

上文还提到了LinkedList成员变量first和last,保存链表头节点和尾节点。

通过add()方法,可以看出LinkedList的数据结构,并且元素是顺序存储的,在方法中,没有看到任何线程同步措施,因此LinkedList不是线程安全的。

再来看add(int index, E element),这个方法的作用是在指定位置插入元素,首先确认要插入位置的索引是否正确。

如果插入的位置等于当前元素个数,说明是直接放在链表最后面,直接调用linkLast()方法,上面已经说过这个方法。

如果不相等,通过linkBefore方法保存在指定元素的位置,参数传入要保存的元素,以及通过node()方法获得指定位置的Node节点。

checkPositionIndex()方法中,调用checkPositionIndex()方法,如果index不合法,则抛出越界异常。

要插入的位置必须大于等于0,且小于等于list元素个数。

linkBefore方法中,succ表示的就是要插入位置的元素,首先新建一个Node节点,并指定newNode的前驱结点为pred后继结点为succ,让目标节点的前驱节点的引用指向新的节点,如果succ节点没有前驱结点,则说明是第一个节点,否则,将pred的后继结点引用指向新的节点。

最后同样增加list元素个数,size加1,集合的并发修改次数modCount加1。

五、get()方法

get方法通过索引去获取指定位置的元素,首先使用checkElementIndex方法去检查索引的合法性。

如果index合法,则使用node方法找到目标位置节点,并且获得节点保存的元素。

在checkElementIndex方法内部,继续调用isElementIndex()方法,确定是否是一个合法的index,不合法则抛出越界异常。

要查找元素的索引必须大于等于0, 小于当前链表元素个数。

node()方法中,首先判断索引是否小于当前元素个数的二分之一,如果是,则说明目标元素,在链表的前半部分,从first节点开始遍历,直到遍历到index ,将目标元素返回。

如果不在前二分之一,则说明在后二分之一,因为链表是双向的,所以从last节点向前遍历更快,向前遍历直到找到index索引对应的节点。

六、remove()方法

remove方法是根据index索引,删除指定位置的元素,首先判断索引值是否正确,和get()方法中调用的是一样的。

如果正确,则使用node()方法找到目标位置元素,传给unlink函数进行元素的删除。

unlink()方法,将要删除的目标元素的前驱结点和后继结点取出,如果没有前驱结点,说明是第一个节点,那么将后继结点的next设置为第一个节点,如果有前驱结点,则将前驱结点的next从原来指向x到指向next,并将当x的prev设置为空。

如果当前元素的后继结点为空,则说明是最后一个节点,只需将最后一个节点设置为prev,如果后继结点不为空,则将后继结点的prev从原来指向x的前驱结点prev,并将x的next设置为null。

最后,将x的保存元素的item属性置为null,如果没有其他地方引用这个元素,就可能会被GC回收,之后size减1,将并发修改次数加1,返回删除的元素。

七、LinkedList迭代器

LinkedList迭代器和其他List接口的实现类,如ArrayList容器没什么区别。

public static void main(String[] args) {
    List<String> personList = new LinkedList<>();
    personList.add("张三");
    personList.add("汤姆");
    personList.add("杰瑞");
    Iterator<String> iterator = personList.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

但是在LinkedList中,并没有iterator()方法,其实是调用抽象类AbstractList的iterator()方法,使用AbstractList定义的迭代器,而不像ArrayList自己重写了iterator,并且内部创建了自己的迭代器。

AbstractList内部的迭代器Itr,它的成员变量包括cursor游标,表示迭代器遍历的位置,lastRet表示最近一次调用next方法或previoous返回的元素的索引,expectedModCount表示创建迭代器时记录的修改次数,用于快速失败机制。

hasNext()用于判断是否遍历到最后,如果游标cursor不等于元素个数,说明还可以继续向后遍历。

next()方法用于获取下一个元素,首先检查并发修改数是否正确,之后将当前游标对应位置的元素调用get()方法,将取出元素的索引赋值给lastRet,游标加1,最后返回元素。

在checkForComodification()方法中,判断当前对象的modCount是否等于创建迭代器时的修改数量,如果不等于,说明在创建迭代器之后,当前线程或者其他线程修改过list,则抛出并发修改异常,这就是快速失败机制。

get()方法是一个抽象方法,就是上面我们分析过的get()方法,定义好算法骨架具体实现交给子类去实现,这是典型的模板模式的应用。

那么如何在迭代的过程中删除元素呢,迭代器提供了一个remove()方法,并且只能删除当前元素。

如果lastRet小于0,说明当前的元素已经被删除,直接抛出IllegalStateException异常。

如果当前元素没有删除,则去检查并发修改次数,之后调用remove()方法去删除指定位置元素,如果lastRet小于游标的话,就将游标减1,并将lastRet置为-1,保证不能重复删除,因为调用了List的remove()方法,LinkedList中的modCount已经变了,所以将新的modCount赋值给迭代器中的expectedModCount,这样就完成了移除元素。

八、总结

以上就是对LinkedList源码的分析,LinkedList是一个常用的List容器类,它是一个线程不安全的容器类,内部基于双向链表实现,插入性能优于ArrayList,因为不需要做元素的移动,但是查找效率不如ArrayList,需要遍历链表,不如数组支持随机访问查找效率高,由于基于双向链表,无需进行扩容,可以使用不连续的内存空间,ArrayList相关源码分析可以查看手把手深度分析ArrayList源码这篇文章。

如果是读多写少的场景,则优先使用ArrayList,如果是写多读少,则优先使用LinkedList。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值