一、LinkedList简介
1. LinkedList概述
- LinkedList是基于双向链表实现的List,也可以当做双端队列(Deque)来使用。
- LinkedList由于底层基于链表实现,删除和插入操作效率较高,查询和修改操作效率较低。
- LinkedList不是一个线程安全类,与之相似的是JUC中的线程安全类ConcurrentLinkedDeque,他们同样基于双向链表实现。
- LinkedList是顺序存取结构,也就是说LinkedList在执行任何操作的时候,都必须遍历该列表才能找到需要的值。
- LinkedList因为是链表结构,所以不需要像数组那样连续的堆内存空间,可以分散存储于堆内存中的不同的地方
- LinkedList的继承关系如下图所示
2. 链表的概念
-
链表
- 链表由一般数据域和指针两部分组成
- 一个是存储数据元素的数据域,也就是储存对应节点数据的element对象
- 另一个是存储下一个或前一个结点地址的指针,即当前节点的prev或next分别指向前一个或后一个节点
-
单向链表
- 单向链表就是通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。
- 单向链表就是通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。
-
单向循环链表
- 单向链表的最后一个节点的next会指向头节点,而不是指向null,这样存成一个环
- 单向链表的最后一个节点的next会指向头节点,而不是指向null,这样存成一个环
-
双向链表
- 双向链表与单向链表相比,新增了一个prev指针,指向前一个节点的element。
- 双向链表的prev指针指向前一个节点,而他的next指针指向后一个节点
- 第一个节点的prev和最后一个一个节点的next都指向null
- LinkedList就是基于双向链表实现
-
双向循环链表
- 双向链表的最后一个节点的next会指向头节点,开始一个节点的prev会指向尾节点,而不是指向null,这样存成一个环
- 双向链表的最后一个节点的next会指向头节点,开始一个节点的prev会指向尾节点,而不是指向null,这样存成一个环
3. 双向链表的常用操作
-
插入
- 假设双向链表中有相邻节点AB,要在这两个节点之间插入节点C,形成ABC结构
- C.prev = A; C.next = B;
- B.prev = C;
- A.next = C;
- 假设双向链表中有相邻节点AB,要在这两个节点之间插入节点C,形成ABC结构
-
删除
- 假设双向链表中有相邻节点ABC,要在删除AB节点之间的节点C,形成AC结构
- A.next = C;
- C.prev = A;
- B = null;
- 假设双向链表中有相邻节点ABC,要在删除AB节点之间的节点C,形成AC结构
二、LinkedList源码解析
1. 继承结构
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable
-
AbstractSequentialList
- AbstractSequentialList是AbstractList的子类,他在AbstractList的基础上,对于链表形式(顺序存取结构)类的一些共同的方法进行了实现。
-
List
- 查看继承结构发现AbstractSequentialList继承的AbstractList也实现了List接口,和ArayList一样,也是一个没影响的遗留问题,但沿用至今
-
Deque
- Deque接口表示是一个双端队列,LinkedList可以使用一系列双端队列的方法进行操作
-
Cloneable
- 标明其可以实现克隆操作,主要为Object.clone()方法
-
Serializable
- 序列化接口,标明其可以被序列化,主要用于字节流的传输
2. 内部类
- 内部类是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;
}
}
3. 属性
- LinkedList的属性比较简单,由长度、头结点和尾结点组成,由于LinkedList基于双向链表实现,所以头结点和尾结点分别用作于正向和反向链表的头结点信息
// 链表长度
transient int size = 0;
// 头节点
transient Node<E> first;
// 尾节点
transient Node<E> last;
//记录修改的次数,继承自AbstractList
protected transient int modCount = 0;
4. 构造方法
/**
* 空构造,仅初始化对象,节点会在后续方法中创建
*/
public LinkedList() {}
/**
* 有参构造,通过现有集合来初始化LinkedList
* 集合中指定泛型的类型必须为LinkedList指定类型的子类
*/
public LinkedList(Collection<? extends E> c) {
this();
// 调用addAll方法将集合元素添加到链表末尾,详见后续addAll方法整理
addAll(c);
}
5. 常用方法
5.1 add方法
/**
* 在List末尾插入数据
*/
public boolean add(E e) {
// 调用linkLast方法在链表末尾添加数据
linkLast(e);
return true;
}
/**
* 内部方法,用于在List末尾插入数据
*/
void linkLast(E e) {
// 创建一个临时节点储存last,也就是l指向最后一个节点
final Node<E> l = last;
// 为当前元素创建新的节点newNode,并将prev指向l
final Node<E> newNode = new Node<>(l, e, null);
// last指向newNode,现在newNode成为最后一个节点
last = newNode;
// 判断l是否为空,即链表是否初始化,如果没有,newNode成为第一个节点,first和last都指向它
if (l == null)
first = newNode;
// 如果l不为空,即链表已经初始化,以前最后一个节点的next指向newNode
// 以前的最后一个节点现在是倒数第二个节点,最后一个节点是newNode
else
l.next = newNode;
// 节点添加成功后,size自增
size++;
// 修改次数+1
modCount++;
}
-
void add(int index, E element):在指定位置插入元素
- 判断index是否合理,主要是正数且不大于size
- 找出当前位置的元素,在该节点前插入当前元素
- 双向链表中相邻的节点AB之间插入节点C,形成ACB的操作
- C.prev = A; C.next = B;
- B.prev = C;
- A.next = C;
/**
* 用于在指定位置插入元素
*/
public void add(int index, E element) {
// 确定index的值是否合理,确保index不小于0,不大于size
checkPositionIndex(index);
// 如果index和size相等,则调用linkLast方法把新元素添加到最后
if (index == size)
linkLast(element);
// 如果不相等,则添加到相应位置
else
// 先用node方法,获取到相应位置的元素
// 调用linkBefore在获取到的节点前插入当前元素
linkBefore(element, node(index));
}
/**
* 获取index位置的节点对象
* 这个方法是LinkedList核心方法之一,之后很多方法都有调用到,要多加注意,后续遇到也就不再累述
*/
Node<E> node(int index) {
// 因为是双向链表,可以判断index在前半段还是后半段,以便于判断从前方还是后方开始遍历,提高效率
// 如果在前半段,从头结点开始遍历
if (index < (size >> 1)) {
Node<E> x = first;
// 从头遍历链表,经过i(index)个next得到的node即为当前节点
for (int i = 0; i < index; i++)
x = x.next;
return x;
// 如果在后半段,从尾结点开始遍历
} else {
Node<E> x = last;
// 从尾遍历链表,经过size-i(index)个next得到的node即为当前节点
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* 将传入元素添加到传入指定节点的前一位
*/
void linkBefore(E e, Node<E> succ) {
// 执行该方法要确定succ!=null,在这个方法之前调用了node方法,可以确定succ!=null
// 取出当前链表传入节点的前一个节点,主要用于后续再这两个节点前之间插入节点的操作
final Node<E> pred = succ.prev;
// 创建一个新的节点,即现在想要添加的节点
// 该节点的前节点(prev)指向pred,后节点(next)指向succ
final Node<E> newNode = new Node<>(pred, e, succ);
// succ的前节点指向prev
succ.prev = newNode;
// 如果succ为原链表的头结点,让newNode成为新的头结点
if (pred == null)
first = newNode;
// 如果succ不是原链表的头结点,pred的后节点指向newNode
else
pred.next = newNode;
// 链表长度+1
size++;
// 修改次数+1
modCount++;
}
/**
* 用于在List末尾添加一个集合
*/
public boolean addAll(Collection<? extends E> c) {
// 调用带位置的addAll方法,指定位置为最后一位,在链表末尾添加集合
return addAll(size, c);
}
/**
* 在指定位置将传入集合的元素添加到链表
*/
public boolean addAll(int index, Collection<? extends E> c) {
// 确定index的值是否合理,确保index不小于0,不大于size
checkPositionIndex(index);
// 将c转换为Object数组
Object[] a = c.toArray();
// 获取数组长度,再通过长度进行判断传入集合是否为空
int numNew = a.length;
// 如果为空,当然就不用执行操作了
if (numNew == 0)
return false;
// 如果不为空,执行后续操作
// 创建两个临时节点,这两个节点,用于储存插入位置两端的两个节点,
// 将传入的集合转换成双向链表,插入这两个节点之间,prev和next分别作为这个链表的头结点和尾结点
Node<E> pred, succ;
// 情况1,执行该方法的时候链表是空的,first = last = null,构造方法中调用addAll就是这种情况
// 这个时候succ = null, pred = null, size = 0
// 情况2,链表中有节点,first和last分别指代第一个节点和最后一个节点,但index == size
// 这个时候需要在最后一个节点之后追加指定元素,所以要用pred记录最后一个节点,succ == null
if (index == size) {
succ = null;
pred = last;
// 情况3,链表中有节点,first和last分别指代第一个节点和最后一个节点,但index != size
// 这时候就需要在链表中间指定位置插入节点,这时候就要用pred和succ储存这个位置两边的节点
// 这时候pred和succ分别作为插入链表的头结点和尾结点
} else {
// 调用node方法获取index位置的节点储存于临时节点succ中
succ = node(index);
// 用pred储存succ前一个节点
pred = succ.prev;
}
// 遍历Object数组,将其转换为双向链表结构
for (Object o : a) {
@SuppressWarnings("unchecked")
// 由于传入时已经规定了<? extends E>,所以o必然可以强转为E类型,用注解关闭警告
E e = (E) o;
// 新建节点,储存e,并前节点指向pred
Node<E> newNode = new Node<>(pred, e, null);
// pred == null说明是情况1,构造方法刚创建出来的新链表,这时候初始化first(List头结点)
if (pred == null)
first = newNode;
// pred != null说明是情况2或者情况3
// 如果是情况2,pred是尾结点,直接在链表尾部一个个往后添加
// 如果是情况3,pred中间节点,在该节点位置往后一个个插入节点,需在后续更新链表结构
else
pred.next = newNode;
// 每添加完一次,pred指向新添加的节点,确保pred是新添加的链表尾部,以便于在其后面不断添加元素
pred = newNode;
}
// succ == null说明是情况1或者情况2,pred都指向新添加链表的最后一位,也就是last
if (succ == null) {
last = pred;
// 如果succ != null说明是情况3,在中断插入链表,需要更新链表信息
// succ储存的是插入点后的第一个节点,需要将succ.prev指向新增链表的最后一个节点,也就是pred
} else {
pred.next = succ;
succ.prev = pred;
}
// 链表长度+传入集合长度
size += numNew;
// 修改次数+1
modCount++;
return true;
}
5.2 remove方法
-
E remove(int index):移除指定下标元素,并返回该元素
- 通过node方法获取相应下标的节点并调用unlink执行删除操作
- unlink移除完成后将节点中prev、item、next都为null以便于GC回收
/**
* 用于移除指定下标的元素,并返回该元素
*/
public E remove(int index) {
// 确定index的值是否合理,确保index不小于0,不大于size
checkElementIndex(index);
// 先通过node方法获取index下标的节点,然后通过unlink方法移除该节点
return unlink(node(index));
}
/**
* 用于移除传入节点
*/
E unlink(Node<E> x) {
// 该节点元素内容
final E element = x.item;
// 临时节点,用于储存下一个节点
final Node<E> next = x.next;
// 临时节点,用于储存上一个节点
final Node<E> prev = x.prev;
// 接下来就是通过链表的指向操作进行删除节点操作
// 说明该节点是头结点,first指向该节点的后一个节点
if (prev == null) {
first = next;
// 该节点是中间节点,断开对前节点的指向,前节点.next指向后节点
} else {
prev.next = next;
x.prev = null;
}
// 说明该节点是尾结点,last指向该节点的前一个节点
if (next == null) {
last = prev;
// 该节点是中间节点,断开对后节点的指向,后节点.prev指向前节点
} else {
next.prev = prev;
x.next = null;
}
// 至此,x.prev = null、x.next = null、x.item = null,GC可以准确判断并进行回收
x.item = null;
// size - 1
size--;
// 修改次数 + 1
modCount++;
return element;
}
-
boolean remove(Object o):移除指定元素,如果有重复,移除第一个
- 通过遍历找到相应元素所属的节点并删除
- 由于链表的遍历是线性的,找到了相应节点就执行删除,所有总是删除重复节点的第一个
- unlink移除完成后将节点中prev、item、next都为null以便于GC回收
/**
* 用于移除指定元素,如果有重复,移除第一个
*/
public boolean remove(Object o) {
// 从这里可以看出,LinkedList可以储存null
// 如果o == null的时候,x.item == null操作会比o.equals(x.item)操作更高效,所以分开执行
if (o == null) {
// 从头遍历链表,找到第一个x.item == null的节点,调用unlink删除该节点
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
// 这个之前讲过,参考之前的代码
unlink(x);
return true;
}
}
} else {
// 和o == null相同,找到item == o的节点并删除
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
5.3 get方法
/**
* 用于取出指定位置的元素
*/
public E get(int index) {
// 确定index的值是否合理,确保index不小于0,不大于size
checkElementIndex(index);
// 通过node方法找到相应的节点,并返回其item属性,即对应元素值
return node(index).item;
}
5.4 indexOf方法
/**
* 用于找到指定元素第一次出现的下标
*/
public int indexOf(Object o) {
int index = 0;
// 和remove方法和node方法类似,也是分为o == null和o != null
if (o == null) {
// 遍历找出相应的下标节点,并返回位置
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
// 与上边一致
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
// 如果没找到,返回-1
return -1;
}
/**
* 用于找到指定元素最后一次出现的下标
* 方法和indexOf原理相同,不作累述
*/
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
5.5 Queue系列方法
/**
* 用于取出队列第一个元素,不删除
*/
public E peek() {
// 找到头结点
final Node<E> f = first;
// 头结点为null则返回null,否则返回其item,即对应元素的值
return (f == null) ? null : f.item;
}
/**
* 用于弹出第一个元素,并删除
*/
public E poll() {
// 找到头结点
final Node<E> f = first;
// 头结点为null则返回null,否则调用unlinkFirst删除头结点并返回头结点的元素值
// unlinkFirst源码和unlinkLast基本相同,可自行查看
return (f == null) ? null : unlinkFirst(f);
}
/**
* 用于移除第一个元素
*/
public E remove() {
// 直接调用removeFirst进行删除
return removeFirst();
}
/**
* 非队列专用方法,用于移除第一个元素
*/
public E removeFirst() {
// 找到头结点
final Node<E> f = first;
// 如果头结点为null,则表示队列为空,抛出异常
if (f == null)
throw new NoSuchElementException();
// 调用unlinkFirst删除第一个节点
return unlinkFirst(f);
}
/**
* 用于在队列尾部添加元素
*/
public boolean offer(E e) {
// 直接调用add方法在队列末尾添加元素
return add(e);
}
5.4 Deque系列方法
- Deque系列方法与Queue系列方法类似,不过多阐述
三、总结
-
LinkedList可以存放重复元素,也可以存放空值,但是执行remove、indexOf等一系列方法时,都只会操作重复元素的第一个
-
LinkedList本质上就是一个双向链表,通过Node内部类将元素包装成节点存储在Java堆中,通过指向将这些节点串起来形成链表
-
LinkedList得益于其链表结构,储存的时候相较于基于数组实现的ArrayList不需要连续的堆空间,可以储存在细碎的堆空间中,储存效率更高
-
LinkedList没有默认长度、最大长度、增长因子的概念,其长度增长随着元素的个数增加相应增长,其最大长度受堆内存大小控制
-
LinkedList不仅能够向前迭代,还能像后迭代,并且在迭代的过程中,可以修改值、添加值、还能移除值
-
LinkedList不仅能当链表,还能当队列使用,这个就是因为实现了Deque接口 ,具备了链表的特性
-
LinkedList的核心是链表,这也就导致了它在插入和修改方面优势明显,也正是因为其双向链表结构,每次查询都要从头(尾)开始遍历,所以查询速度方面有较大劣势