1. 前言
LinkedList
解析本文将分为如下几部分:
- 类结构
- 存储结构
- 常用方法及执行效率
- 线程安全
- ArrayList 优缺点
- 使用场景
注:本文大部分分析都围绕着源码,源码基于
JDK 1.8
。
2. 类结构
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {}
AbstractSequentialList
:其内部的代码是用于减少重复性。相较于 AbstractList
,此抽象类适用于没有随机访问性的 List
。
List
:List
的标准规范。
Deque
:此接口继承自 Queue
,在其基础上扩展了双端队列的标准。
Cloneable
:赋予了 LinkedList
对象拷贝功能
Serializable
:赋予 LinkedList
序列化功能。
2. 存储结构
public class LinkedLise {
transient Node<E> first;
transient Node<E> last;
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;
}
}
}
first
,last
两个属性是用于存储数据,其类型为 Node
。
Node
的结构也很直白,大致如下图:
注意:
- 当Node作为根节点时,Node的
prev
为null
,因为根节点无前驱节点! - 当Node作为尾节点时,Node的
next
为null
,因为尾节点无后继节点!
3. 常用方法及执行效率
3.1 无参构造
public LinkedList() {}
emm,啥也没有。。。无意水了一下🌚
3.2 add()
public boolean add(E e) {
// 见下文 3.2.1
linkLast(e);
return true;
}
3.2.1 linkLast()
void linkLast(E e) {
// 此行代码无实际意义
final Node<E> l = last;
/*
新建节点,调用 Node 类型的全参构造。
Node(Node<E> prev, E element, Node<E> next)
new Node<>(l, e, null),将 last 作为新节点的前驱节点,e 是数据,后继节点为 null 前文已经介绍过了,此处不再叙述。
*/
final Node<E> newNode = new Node<>(l, e, null);
// 将 last 属性更新为新增节点(newNode)
last = newNode;
/*
需要注意的是,当 last 为 null 时,此时 last 中是一个节点都没有的。
也就直接证明了该节点为第一个节点,也是 LinkedList 根(root)节点。
反之,当 last 中存在节点时,将新增节点作为后继节点插入。
*/
if (l == null)
first = newNode;
else
l.next = newNode;
// LinkedList 的元素个数 + 1
size++;
/*
该变量主要是用于记录结构变化的次数(添加数据,删除数据,调整数组大小等)。
其该变量作用体现在迭代时,比较前后的 modCount 是否变化。
如果 modeCount 前后不一致,则抛出ConcurrentModificationException(并发修改异常)!
*/
modCount++;
}
3.3 remove()
public boolean remove(Object o) {
// 如果入参 o 为空,那么将会删除该 LinkedList 第一个为 null 的元素
if (o == null) {
// 遍历所有元素
for (Node<E> x = first; x != null; x = x.next) {
// 如果该元素等于 null,则 if 语句条件成立,执行 if 代码块
if (x.item == null) {
// 详见下文 3.2.1 unlink() 解析
unlink(x);
return true;
}
}
} else {
// 遍历所有元素
for (Node<E> x = first; x != null; x = x.next) {
// 使用 equals 判断两者相等性,如果相等则删除
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
时间复杂度
O(n)
3.2.1 unlink();
E unlink(Node<E> x) {
// 取出被删除节点的值
final E element = x.item;
// 被删除节点的下一个元素
final Node<E> next = x.next;
// 被删除节点的上一个元素
final Node<E> prev = x.prev;
// 因为根节点没有前驱,所以当删除的节点为根节点时,if 语句成立,执行代码块
// 反之,删除的不是根(首)节点
if (prev == null) {
// 更新根(首)节点,将根节点替换为删除元素的下一个元素
first = next;
} else {
// x.prev.next 改变引用(不再指向被删除元素)
prev.next = next;
// 将元素指向置空
x.prev = null;
}
// 如果删除的是尾节点,则尾节点没有后继,if 条件成立,执行 if 代码块
// 反之,删除的不是尾节点
if (next == null) {
// 更新尾节点,将尾节点替换为删除元素的上一个元素
last = prev;
} else {
// x.next.prev 改变引用(不再指向被删除元素)
next.prev = prev;
// 将元素指向置空
x.next = null;
}
// 将元素值置空
x.item = null;
// LinkedList 元素个数 - 1
size--;
// 上文讲过,可以去看看
modCount++;
// 返回元素值
return element;
}
3.4 set()
public E set(int index, E element) {
// 详见下文 3.4.1 checkElementIndex()
checkElementIndex(index);
// 详见下文 3.4.2 node()
Node<E> x = node(index);
// 取出旧值保存,用作返回值
E oldVal = x.item;
// 替换节点值
x.item = element;
return oldVal;
}
3.4.1 checkElementIndex()
private void checkElementIndex(int index) {
// 如果给定索引无效,则抛出索引越界异常
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
3.4.1.1 isElementIndex()
private boolean isElementIndex(int index) {
// 判断给定索引是否有效
return index >= 0 && index < size;
}
3.4.2 node()
// 获取指定节点(元素)
Node<E> node(int index) {
// 采用了对半查找
// 如果 index 小于长度的一半(size >> 1),那么 if 条件成立,执行 if 代码块
// 反之
if (index < (size >> 1)) {
// 从根(首)节点开始查找
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 从尾节点查找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
3.5 get()
public E get(int index) {
// 详见 3.4.1 checkElementIndex()
checkElementIndex(index);
// 详见 3.4.2 node()
return node(index).item;
}
3.5 indexOf()
简单明了,此方法不解析
public int indexOf(Object o) {
int index = 0;
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++;
}
}
return -1;
}
3.6 其它方法
由于 LinkedList
实现了 Deque
接口,所以 LinkedList
支持双端队列操作。
具体的操作方法可以查阅 LinkedList
源码。
4. 线程安全
在上述方法中,并没有见到如 synchronized
,ReentrantLock
等来维护线程安全的操作,也就直接证明了该方法是非线程安全的。
5. LinkedList 优缺点
优点:
- 适用于高效的增删操作
- 因实现了双端队列,具有更高灵活性
缺点:
- 因为存储结构影响,获取指定元素的时间复杂度为O(n)
- 没有随机访问特性
6. 使用场景
- 需要经常增删
- 元素访问不频繁
- 对元素顺序有要求
- 有双端队列特性的需求
至此,文章到此就结束了。
博主水平有限,博文有错误的地方可以私信或评论指出。
Bye.