容器类源码解析系列(二)—— LinkedList 集合源码分析(最新版)
前言
上篇文章 容器类源码解析系列(一) ArrayList 源码分析——基于最新Android9.0源码 对ArrayList集合的源码进行了详细的分析,通过源码分析,使得我们对ArrayList的理解更加深刻,使用起来会更得心应手。本文将要讲解的是LinkedList的底层原理。建议在阅读本文之前,先回顾一下上篇介绍ArrayList的文章,毕竟它们都是开发中的常用的集合类。
要点
- LinkedList底层是通过基于双向链表实现数据持久化存储的。
-
LinkedList可以存储Null值,可以存储重复的值。
-
LinkedList是非线程安全的,在多线程并发环境下,需要加锁来做同步处理,也可通过Collections.synchronizedList(new LinkedList(…))方法,获得一个加过锁的List。
-
LinkedList也是具有fail-fast特性的集合类。关于fail-fast机制,容器类源码解析系列(一) ArrayList 源码分析——基于最新Android9.0源码 这篇文章有详细讲解。
-
current节点的prev等于null,则该节点是first节点;current节点的next等于null,则该节点是last节点。
Node 节点
分析LinkedList源码,Node这个类,我们必要要知道。Node(节点)是在链表结构中的基本组成单位。我们看一下LinkedList类中,对节点的定义。
|
|
根据代码再结合图片,很容易知道Node的含义,element表示存储的Value值,pre、next分别指向当前节点的上一个节点和下一个节点。
构造
|
|
先看上面的成员变量,他们都被transient关键字修饰,表示不会被序列化。size是容器大小;first,last分别表示开始的节点和最后一个节点。我们发现first和last上面还有两段说明。这两段说明,是很有必要注意一下的,因为后面的一些add,remove等常规操作,其中对节点的处理逻辑,是和这两段说明有对应联系的。
|
|
这段说明是形容first节点的,如果first是null,那么last节点也应该是null;first.prev等于null,first.item不能等于null。这两个条件至少有一个成立,这是对first节点的定理约束。第一个判断还比较好理解,第二个判断可能不好理解,我们来通过一些例子感受一下。
|
|
上面的代码是LinkedList源码中把一个value值插入链表尾部的方法,实现逻辑如下:
- 首先把旧的last节点引用指向l;
- 创建新的节点来,pre指向旧的last节点,那么不言而喻,newNode节点就是新的last节点。
- 如果l节点,也就是pre指向的节点等于null,这个newNode同是也被指向first节点,成为first节点。
后面的就不分析了,此时我们看,newNode节点(即first节点),它的pre(l)是等于null的,newNode节点的next也是null,如果此刻e也等于null,则第二个条件(first.prev == null && first.item != null) 没有满足,但是不要忘了newNode既是first节点也是last节点,满足了第一个条件(first == null && last == null) 所以,定理还是满足的。
第二个不变的定理是指向last节点的Invariant: (first == null && last == null) || (last.next == null && last.item != null) 就不在细说了,这两个不变的定理,在LinkedList的源码实现逻辑中,会表现出现。
LinkedList不像ArrayList默认有10个capacity,linkedlist的size默认是0。所以LinkedList每次add数据都需要创建Node节点。
LinkedList有两个构造方法:
|
|
我们主要看第二个构造函数。传入有个list集合,把它放在当前LinkedList的尾部。
|
|
主要调用这个方法,表示吧指定集合添加到index对应的节点之前。通过构造函数调用这个方法的话,index是size值的大小,表示,把指定集合添加到当前链表的尾部。
(注释1)会先对index做一个数组越界的异常检测。
(注释2)如果size = index,表示吧传进来的指定集合add到链表尾部。不相等的话,就先通过node(index)拿到对应的节点,然后把pred = succ.prev;表示,之后添加的节点是插在index对应节点之前的地方。
接下来就比较简单了,就是循环创建节点,然后是修改next,prev指向问题。
常规操作
get,set操作
|
|
先看get方法
|
|
在做一个判断来确定区间后,接着用for循环,遍历查找出目标节点。而ArrayList是通过数组直接拿到的。我们看set方法。
|
|
和get方法一样,也是要通过node(index)方法,遍历获取目标节点,然后替换目标节点的element的值。所以相较于ArrayList,对于get,set操作,还是ArrayList更方便,效率些。
Add,Remove操作
|
|
添加元素,默认是添加到尾部,linkLast()这个方法前文有介绍,这里就不在重复了,内部逻辑就是new一个Node节点,然后赋给last,再把旧的last指向新的节点的prev。
|
|
我们着重看linkBefore方法,该方法的第二个参数是调用了之前说的node(index)方法,找到对应index的目标节点。
|
|
总的来说就是new一个节点,然后设置其的prev,next,以及它前一个节点的确定。add操作系列的方法还有addAll、addFirst、等等。addAll方法上面已经介绍过了。它们的逻辑都大差不差,有index要求的,就先遍历确定index对应的节点,然后创建Node节点,再做pre,next这些引用的调整,没有index要求的,就创建节点,默认放在最前面,或者最后面,最后重新设置一下first,last的引用。
|
|
|
|
先遍历找到目标节点,然后重新指定目标节点的prev,next指向关系,把目标节点的prev,element,next值置为null,方便回收。
|
|
这个方法,是根据index,先找到目标节点,然后在对目标节点进行处理。通过分析ArrayList和LinkedList的插入,删除源码实现,可以得出如下结论。
总结:
有些说法认为LinkedList做插入和删除更快,这种说法其实不是绝对的:
(1)LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Node节点的prev,next的引用地址。
(2)ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址。
顺序插入(add)的话,建议选择LinkedList,因为ArrayList有扩容和批量copy的消耗。
随机插入,如果插入点在集合的前面部分靠近头,后面部分靠近尾端的部分,LinkedList更合适。
对于ArrayList,插入的越靠后,即index越大,size-index越小,copy的数量就越小。会更快些。(但是存在扩容的损耗)
删除操作,如果是remove(Object o),删除节点的位置越靠后,效率越差,如果是remove(int index)操作,则删除节点的位置在两端的效率高,中部的效率差。
ArrayList的删除操作效率表现和插入类似,越往后,copy的数量越少,越快些。
上面说的是在大量数据下,数据量少的情况下,其实差别不大。随便用!(ps:ArrayList用的多点)。
队列操作
因为LinkedList实现了Deque接口,所以也就有了队列的操作。比如:
offer(E e)
poll()
peek()
…
等等,比较简单,就不说了,知道有这回事。

