文章结构
一、概述
日常开发中,相较于 ArrayList
,LinkedList
的使用频率是相对较少的,但是使用较少绝对不代表着它不重要,ArrayList
在访问元素的时候速度很快,而 LinkedList
的优势则展现在它添加和删除元素的速度上,所以在添加、删除元素频繁而访问较少的场景中,我们可以考虑使用 LinkedList
作为容器储存元素。
本篇文章基于源码层面,对 LinkedList
的常用方法进行一些源码的简要分析,了解其内部的工作原理,并将它和 ArrayList
做一个简要的比较。
注意:本篇文章的源码基于 JDK1.8,可能会和之前的版本有所不同。
二、LinkedList 的源码分析
在源码分析中笔者分为以下 5 部分内容进行展开:
- 双向链表
- 类的继承关系
- 类的属性
- 类的构造方法
- 常用方法
1. 双向链表
LinkedList
内部维护了一个双向链表来储存数据,而双向链表中每个节点的结构示意图如下:
对应于 LinkedList
中的类就是 Node
了,它是 LinkedList
的一个内部类,定义如下:
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;
}
}
而一条完整的双向链表如下所示:
其中红色箭头代表的是 prev
的指向,黑色箭头代表的是 next
的指向。需要特别注意的一点是 JDK1.8 中 LinkedList
维护的不再是一个双向循环链表而只是一个普通的双向链表!
2. 类的继承关系
LinkedList
在源码中的定义如下:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable
- 继承自
AbstractSequentialList
抽象类,这是一个双向循环结构链表类。 - 实现了
List
接口,规定了List
相关的操作规范。 - 实现了
Deque
接口,能将LinkedList
当作双端队列使用。 - 实现了
Cloneable
接口,可拷贝。 - 实现了
Serializable
接口,可序列化。
3. 类的属性
了解了 LinkedList
的继承关系后,接下来看到类内部维护的成员:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
// 版本号
private static final long serialVersionUID = 876323262645176354L;
......
}
LinkedList
维护的成员非常少,它们的含义如下:
- size:
LinkedList
元素的大小。 - first:指向
LinkedList
的第一个元素,默认为空。 - last:指向
LinkedList
的最后一个元素,默认为空。
注意到 size
、first
、last
都添加了 transient
关键字,也就是说它们不参与序列化的过程。而 first
、last
我们可以当成一个游标,分别指向双向链表的头部和尾部,如下图所示:
4. 类的构造方法
LinkedList
的构造方法有 2 个,如下所示:
LinkedList()
的实现如下:
public LinkedList() {
}
无参构造方法不做任何事,相当于创建了一个空链表。
有参的构造方法 LinkedList(Collection<? extends E> c)
的实现如下:
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
调用了 addAll
方法,它的实现如下:
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
调用的是它的重载方法,实现如下:
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); // 检测index是否合法
Object[] a = c.toArray();
int numNew = a.length; // 需要添加的元素数量
if (numNew == 0)
return false;
Node<E> pred, succ;
// 如果index为size,说明是从尾部开始添加元素的
// 此时pred指向last,succ指向null
if (index == size) {
succ = null;
pred = last;
} else {
// 如果是从非尾部插入的话,succ指向索引值所在节点
// pred指向索引值所在节点的前一个节点
succ = node(index);
pred = succ.prev;
}
// 通过遍历的方式添加元素
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// newNode前节点指向pred,后节点为空
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
// pred为空时说明newNode为第一个节点,将first指向该节点
first = newNode;
else
// 不为空时将pred的后节点指向newNode
pred.next = newNode;
pred = newNode; // pred重新指向newNode,以便进行下一次的元素添加
}
// 判断succ是否为null,为null说明pred为尾节点
// 此种情况发生在从尾部添加元素的时候,即index=size
if (succ == null) {
// 如果pred为尾节点将last指向pred
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
其中,检测 index
是否合法的方法 checkPositionIndex
的实现如下:
private void checkPositionIndex(int index) {
if (!this.isPositionIndex(index)) {
throw new IndexOutOfBoundsException(this.outOfBoundsMsg(index));
}
}
private boolean isPositionIndex(int index) {
return index>= 0 && index<= this.size;
}
addAll
方法添加元素的示意图如下,分两种情况讨论:
- 向空链表中添加元素:
- 向非空链表的中间添加元素:
在了解了 2 个构造方法的原理后,接下来看看LinkedList
的常用方法。
5. 类的常用方法
LinkedList
的常用方法和 ArrayList
非常类似,有如下几个常用方法:
- add:往
LinkedList
中添加元素,它有两个重载方法。 - remove:从
LinkedList
中删除元素,它同样也有两个重载方法。 - get:获取
LinkedList
中的元素。 - indexOf:获取
LinkedList
中指定元素的索引值。 - set:更新指定位置的元素值。
- contains:判断
LinkedList
中是否包含参数中的元素。
5.1 add
add(E e)
的源码如下:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
// 新元素prev指向l,next指向null
final Node<E> newNode = new Node<>(l, e, null);
last = newNode; // 将游标last重新定位
if (l == null)
// 如果l为空说明该节点为首节点,将游标first定位到newBode
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
它的示意图和前面的 addAll
方法非常类似,如下所示:
add(int index, E element)
方法的实现如下:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
// 相等时直接调用linkLast在尾部添加元素,效果和上面add(e)一样
linkLast(element);
else
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 找出插入位置元素的前节点
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
// 如果pred为空说明插入位置即为头节点
if (pred == null)
first = newNode;
else
// 不为空说明pred为普通节点,将它的next指向newNode
pred.next = newNode;
size++;
modCount++;
}
示意图如下所示:
除了上述 3 个 add
方法之外,还有 addAll
、addFirst
、addLast
等方法进行元素的添加,这些方法的实现大同小异,这里就留给大家自己研究。
5.2 remove
remove(Object o)
的实现如下:
public boolean remove(Object o) {
if (o == null) {
// 如果o为空,从头节点开始遍历,寻找空节点进行删除
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 如果o不为空,从头节点开始遍历,寻找与0相匹配的节点进行删除
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
// 找出要删除元素的后结点和前结点
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
// 如果前结点为空说明要删除的结点为首结点,直接
// 将first游标指向next
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
// 如果后结点为空说明要删除的结点为尾结点,直接
// 将last游标指向prev
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
示意图如下所示:
remove(int index)
的实现如下:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
可以看到 remove(int)
方法也是调用 unlink
方法进行元素的删除,删除的过程和上述过程一致,这里不再赘述。
remove()
的实现如下:
public E remove() {
// 移除首元素
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
找出首元素的下一个元素
final Node<E> next = f.next;
// 将f的成员设置为null方便GC
f.item = null;
f.next = null;
// 将next设置为首元素
first = next;
if (next == null)
// 如果next为空说明该链表为空,last直接指向null
last = null;
else
// 否则将next的前结点指为null,因为此时next为首结点
next.prev = null;
size--;
modCount++;
return element;
}
除了上述 3 个 remove
方法之外,还有 removeAll
、removeFirst
、removeLast
等方法进行元素的删除,这些方法的实现同样都是大同小异,就留给大家自行研究吧。
5.3 get & getFirst & getLast
这 3 个方法的实现如下所示:
public E get(int index) {
// 检查索引值是否合法
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// 如果index小于size/2,从首结点往后遍历找到index所在结点
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 否则的话就从尾结点往前遍历找到index所在结点
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
// 首结点不为空直接返回首结点的元素值
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
// 尾结点不为空直接返回尾结点的元素值
return l.item;
}
5.4 indexOf
indexOf(Object o)
的实现如下所示:
public int indexOf(Object o) {
int index = 0;
if (o == null) {
// 如果o为null就从首结点开始遍历寻找空元素所在的结点
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
// 如果o不为null就从首结点开始遍历寻找与0相匹配的元素所在的结点
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
// 链表中不存在o返回-1
return -1;
}
5.5 set
set(int index, E element)
的实现如下:
public E set(int index, E element) {
// 检查索引值是否合法
checkElementIndex(index);
// 通过node方法获取index对应的结点
// node方法的分析情况get方法部分对应的介绍
Node<E> x = node(index);
E oldVal = x.item;
// 更新结点的元素
x.item = element;
return oldVal;
}
5.6 contains & isEmpty
public boolean contains(Object o) {
// 调用indexOf方法查看是否存在o
return this.indexOf(o) != -1;
}
public boolean isEmpty() {
return this.size() == 0;
}
public int size() {
return this.size;
}
三、遍历 LinkedList
LinkedList
在遍历元素的时候有两种方式:
- 使用 for 循环进行遍历。
for (int i = 0; i < list.size(); i++){
result = list.get(i);
}
- 使用 foreach 进行遍历。
for(Integer number : list){
result = number;
}
因为 LinkedList
内部实现了 iterator 的集合类,所以使用 foreach 编译器会默认使用 iterator 进行遍历,下面做一个简单的测试:
public class Test {
public static void main(String[] args) throws InterruptedException {
long startTime;
int result;
LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 100000; i++){
list.add(i);
}
startTime = System.currentTimeMillis();
for(Integer number : list){
result = number;
}
System.out.println("foreach cost " + (System.currentTimeMillis() - startTime) + " ms");
startTime = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++){
result = list.get(i);
}
System.out.println("for loop cost " + (System.currentTimeMillis() - startTime) + " ms");
}
}
输出结果为:
foreach cost 3 ms
for loop cost 5003 ms
可以看到,在使用 get
方法遍历链表的时候,效率非常的低下。而同样为 100,000 个数,在用 ArrayList
的 get
方法进行遍历的时候,花费时间如下:
public class Test {
public static void main(String[] args) throws InterruptedException {
long startTime;
int result;
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++){
list.add(i);
}
startTime = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++){
result = list.get(i);
}
System.out.println("for loop cost " + (System.currentTimeMillis() - startTime) + " ms");
}
}
输出结果:
for loop cost 1 ms
可以看到 ArrayList
只花费了 1ms 就完成了对整个数组的遍历,所以我们可以归纳出以下结论:
LinkedList
使用 foreach 进行遍历比 for 循环进行遍历快很多,所以更加推荐使用 foreach 对LinkedList
进行遍历。- 相比于
LinkedList
,ArrayList
在遍历上的效率要更加高。
四、与 ArrayList 的比较
虽然在日常开发中 ArrayList
的使用场景比较多,但绝对没有孰优孰劣之分,ArrayList
和 LinkedList
有各自不同的使用场景。
ArrayList
的优点在于读取元素的速度快,因为其内部维护的是一个数组,但是它的缺点也同样明显,那就是在添加、删除元素的时候需要进行数组内元素的移动,当数组过大的时候这会造成很大的开销。
LinkedList
的优点则在于添加、删除元素的速度快,因为其内部维护的是一个双向链表,添加和删除的操作对链表来说开销很小,而它的缺点则在于读取元素的速度比较慢,特别是在使用 for 循环进行数据读取的时候,速度非常的慢,LinkedList
更加推荐使用 foreach 进行遍历。
所以在添加、删除数据较少而读取数据较多的场景下,更加推荐使用 ArrayList
进行数据的存储,而相反的在添加、删除操作频繁而读取偏少的时候,更加推荐使用 LinkedList
进行数据的存储。
参考
本篇文章属于笔者的学习笔记,参考自以下博客:
https://www.cnblogs.com/leesf456/p/5308843.html
https://www.jianshu.com/p/d5ec2ff72b33
如果对于本篇博客有疑惑的可以在下方评论区给我留言,希望这篇博客对您有所帮助~