概述
LinkedList 是 Java 集合框架中一个重要的实现,其底层采用的双向链表结构。
1 | transient int size = 0; |
和 ArrayList 一样,LinkedList 也支持空值和重复值。
由于 LinkedList 基于链表实现,存储元素过程中,无需像 ArrayList 那样进行扩容。但有得必有失,LinkedList 存储元素的节点需要额外的空间存储前驱和后继的引用。另一方面,LinkedList 在链表头部和尾部插入效率比较高,但在指定位置进行插入时,效率一般。原因是,在指定位置插入需要定位到该位置处的节点,此操作的时间复杂度为O(N)
。
最后,LinkedList 是非线程安全的集合类,并发环境下,多个线程同时操作 LinkedList,会引发不可预知的错误。如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:
1 | List list=Collections.synchronizedList(new LinkedList(...)); |
内部结构分析
LinkedList类中的一个内部私有类Node,这个类就代表双端链表的节点Node:
1 | private static class Node<E> { |
继承体系
LinkedList的继承体系较为复杂,继承自 AbstractSequentialList,同时又实现了 List 和 Deque 接口。
LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有
队列的特性,我们可以这样使用:
1 | Queue<T> queue = new LinkedList<>(); |
继承体系图如下(删除了部分实现的接口):
LinkedList继承自 AbstractSequentialList ,AbstractSequentialList 又是什么呢?从实现上,AbstractSequentialList提供了一套基于顺序访问的接口。通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问某种序列表(比如链表)的接口。深入源码,AbstractSequentialList 提供的方法基本上都是通过 ListIterator实现的,比如:
1 | public abstract class AbstractSequentialList<E> extends AbstractList<E> { |
所以只要继承类实现了 listIterator 方法,它不需要再额外实现什么即可使用。
对于随机访问集合类一般建议继承 AbstractList 而不是 AbstractSequentialList。
LinkedList和其父类一样,也是基于顺序访问。所以 LinkedList 继承了AbstractSequentialList,但 LinkedList 并没有直接使用父类的方法,而是重新实现了一套的方法。
源码分析
构造方法
LinkedList有两个构造方法,一个是空构造方法,一个是用已有的集合创建链表的构造方法:
1 | /** |
查找
LinkedList 底层基于链表结构,无法向 ArrayList 那样随机访问指定位置的元素。
LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后(向前)查找,时间复杂度为 `O(N)`。相关源码如下:
根据指定位置取数据的方法
get(int index): 根据指定索引返回数据
1 | public E get(int index) { |
上面的代码比较简单,主要是通过遍历的方式定位目标位置的节点。获取到节点后,取出节点存储的值返回即可。这里面有个小优化,即通过比较 index 与节点数量 size/2 的大小,决定从头结点还是尾节点进行查找。
获取头结点(index = 0)
1 | public E getFirst() { |
区别: getFirst(),element(),peek(),peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null。
其中getFirst() 和element() 方法将会在链表为空时,抛出NoSuchElementException。
获取尾结点(index = -1)
1 | public E getLast() { |
两者区别: getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只是会返回 null。
根据对象得到索引的方法
int indexOf(Object o): 从头遍历找
1 | public int indexOf(Object o) { |
int lastIndexOf(Object o): 从尾遍历找
1 | public int lastIndexOf(Object o) { |
检查链表是否包含某对象
contains(Object o): 检查对象o是否存在于链表中
1 | /** |
遍历
链表的遍历过程也很简单,和上面查找过程类似,我们从头节点往后遍历就行了。但对于 LinkedList 的遍历还是需要注意一些,不然可能会导致代码效率低下。通常情况下,我们会使用 for each 遍历 LinkedList,而 for each 最终转换成迭代器形式。所以分析 LinkedList 的遍历的核心就是它的迭代器实现,相关代码如下:
1 | public ListIterator<E> listIterator(int index) { |
插入
LinkedList 除了实现了 List 接口相关方法,还实现了 Deque 接口的很多方法,所以我们有很多种方式插入元素。
LinkedList 插入元素的过程实际上就是链表链入节点的过程,下面我们分析 List 接口中相关的插入方法:
将元素添加到链表尾部
add(E e) 方法:将元素添加到链表尾部
在指定位置添加元素
add(int index,E e):在指定位置添加元素
1 | /** 在链表尾部插入元素 */ |
上面是插入过程的源码,我对源码进行了比较详细的注释,应该不难看懂。上面两个 add 方法只是对操作链表的方法做了一层包装,核心逻辑在 linkBefore 和 linkLast 中。
这里以 linkBefore 为例,linkBefore方法需要给定两个参数,一个插入节点的值,一个指定的node,所以我们又调用了Node(index)去找到index对应的node,它的逻辑流程如下:
- 创建新节点,并指明新节点的前驱和后继
- 将 succ 的前驱引用指向新节点
- 如果 succ 的前驱不为空,则将 succ 前驱的后继引用指向新节点
对应于下图:
将集合插到链表尾部
addAll(Collection c ):将集合插入到链表尾部
1 | public boolean addAll(Collection<? extends E> c) { |
将集合从指定位置开始插入
addAll(int index, Collection c): 将集合从指定位置开始插入
1 | public boolean addAll(int index, Collection<? extends E> c) { |
上面可以看出addAll()方法通常包括下面四个步骤:
- 检查index范围是否在size之内
- toArray()方法把集合的数据存到对象数组中
- 得到插入位置的前驱和后继节点
- 遍历数据,将数据插入到指定位置
将元素添加到链表头部
addFirst(E e): 将元素添加到链表头部
1 | public void addFirst(E e) { |
将元素添加到链表尾部
addLast(E e): 将元素添加到链表尾部,与 add(E e) 方法一样
1 | public void addLast(E e) { |
删除
如果大家看懂了上面的插入源码分析,那么再看删除操作实际上也很简单了。删除操作通过解除待删除节点与前后节点的链接,即可完成任务。过程比较简单,看源码吧:
删除指定元素
remove(Object o): 删除指定元素
删除指定位置的元素
remove(int index):删除指定位置的元素
1 | //删除指定元素 |
和插入操作一样,删除操作方法也是对底层方法的一层保证,核心逻辑在底层 unlink 方法中。所以长驱直入,直接分析 unlink 方法吧。unlink 方法的逻辑如下(假设删除的节点既不是头节点,也不是尾节点):
- 将待删除节点 x 的前驱的后继指向 x 的后继
- 将待删除节点 x 的前驱引用置空,断开与前驱的链接
- 将待删除节点 x 的后继的前驱指向 x 的前驱
- 将待删除节点 x 的后继引用置空,断开与后继的链接
对应下图:
删除头节点
remove() ,removeFirst(),pop(): 删除头节点
1 | public E pop() { |
删除尾节点
removeLast(),pollLast(): 删除尾节点
1 | public E removeLast() { |
区别: 区别在于对于链表为空时的处理,removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()方法返回null。
参考:
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/LinkedList.md