1. 链表综述
LinkedList是基于链表实现的,所以先讲解一下什么是链表。链表原先是C/C++的概念,是一种线性的存储结构,意思是将要存储的数据存在一个存储单元里面,这个存储单元里面除了存放有待存储的数据以外,还存储有其下一个存储单元的地址(下一个存储单元的地址是必要的,有些存储结构还存放有其前一个存储单元的地址),每次查找数据的时候,通过某个存储单元中的下一个存储单元的地址寻找其后面的那个存储单元。
这么讲可能有点抽象,先提一句,LinkedList是一种双向链表,双向链表我认为有两点含义:
-
链表中任意一个存储单元都可以通过向前或者向后寻址的方式获取到其前一个存储单元和其后一个存储单元
-
链表的尾节点的后一个节点是链表的头结点,链表的头结点的前一个节点是链表的尾节点
2. 链表定义
2.1 继承和实现:
- 继承了AbstractSequentialList抽象类:在遍历LinkedList的时候,官方更推荐使用顺序访问,也就是使用我们的迭代器。(因为LinkedList底层是通过一个链表来实现的)(虽然LinkedList也提供了get(int index)方法,但是底层的实现是:每次调用get(int index)方法的时候,都需要从链表的头部或者尾部进行遍历,每一的遍历时间复杂度是O(index),而相对比ArrayList的底层实现,每次遍历的时间复杂度都是O(1)。所以不推荐通过get(int index)遍历LinkedList。至于上面的说从链表的头部后尾部进行遍历:官方源码对遍历进行了优化:通过判断索引index更靠近链表的头部还是尾部来选择遍历的方向)(所以这里遍历LinkedList推荐使用迭代器)。
- 实现了List接口。(提供List接口中所有方法的实现)
- 实现了Cloneable接口,它支持克隆(浅克隆),底层实现:LinkedList节点并没有被克隆,只是通过Object的clone()方法得到的Object对象强制转化为了LinkedList,然后把它内部的实例域都置空,然后把被拷贝的LinkedList节点中的每一个值都拷贝到clone中。(后面有源码解析)
- 实现了Deque接口。实现了Deque所有的可选的操作。
- 实现了Serializable接口。表明它支持序列化。(和ArrayList一样,底层都提供了两个方法:readObject(ObjectInputStream o)、writeObject(ObjectOutputStream o),用于实现序列化,底层只序列化节点的个数和节点的值)。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
2.2 数据结构:
LinkedList是一个双向链表,则一定有存储单元,LinkedList的存储单元是它的一个静态内部类Node:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
其中item是真正存储的数据,next和prev分别是前后个和前一个存储单元的引用地址。用图表示:
3. 初始化
初始化有2种方式:
- 初始化一个空链表
transient int size = 0;
transient Node<E> first; // 头单元
transient Node<E> last; // 尾单元
// 空的构造函数,初始化prev和next
public LinkedList() {
}
- 根据一个集合初始化,将集合种的数据存放到链表中
// 含参构造,根据一个Collection初始化
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
3. 链表操作
3.1 添加操作:
因为LinkedList即实现了List接口,又实现了Deque接口,所以LinkedList既可以添加将元素添加到尾部,也可以将元素添加到指定索引位置,还可以添加整个集合;另外既可以在头部添加,又可以在尾部添加。下面我们分别从List接口和Deque接口分别介绍。
3.1.1 List接口添加
3.1.1.1 末尾添加add(E e)
假如我在链表中添加元素做了哪些事情?首先我们看看插入代码:
List<String> list = new LinkedList<String>();
list.add("111");
list.add("222");
-
第一句初始化调用无参构造,做的事情可以看成:
初始化一个空的item,值为null,同时将它的prev设置为first的引用地址,next设置为last的引用地址,而此时first和last都是null。
debug展示: -
第二句,插入111做了什么事情,我们看看相关的源码:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last; // 定义一个node指向链表尾部
// 初始化成员变量:this.item = e; this.next = null; this.prev = l;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode; // 将new的节点设置为尾节点
if (l == null)
first = newNode; // 如果之前的为节点是空,则头节点是新节点
else
l.next = newNode; // 否则,上一个节点的next指向新节点
size++;
modCount++;
}
根据源码,
2.1.首先定义一个Node,并赋值为last,用于判断
2.2.然后new一个新的Node假设地址为0x00000001,node的item赋值为111,next存的地址为null,prev存的地址为l即last,此时last是null,如图所示即:
2.3.最后分别是将链表的last指向当前new的Node,如果之前的last为空的话,那么first也指向new的Node,如果不为空,则上一个Node的next指向new出来的地址,如图表示即:
2.4.debug展示:
3. 第三句,插入222
代码解释就不说了,我们画图直接来看插入222的图解,我们假设222的地址是0x00000002:
首先new一个新的Node并设值:
然后重新设置first和last及上一个节点的尾节点:
debug展示:
3.1.1.2 中间插入add(int index, E e)
在原来代码基础上执行如下代码做了哪些事情?
list.add(1, 333);
首先来看源码:
public void add(int index, E element) {
checkPositionIndex(index); // 查看index是否越界或小于0
if (index == size)
linkLast(element); // 相当于在最后添加
else
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;