LinkedList简介
上次咱们分析了ArrayList的源码,知道ArrayList查找元素的速度特别快,时间复杂度是O(1),但是如果不是在数组末尾插入元素或者插入时触发了扩容机制,这时侯的时间复杂度为O(n),今天我们来看另一个list类型的集合对象LinkedList,它对元素的插入的时间复杂度可以达到O(1),同样,我们先来看下LinkedList的继承关系图,如下所示:
可以看到LinkedList和ArrayList一样都实现了java.io.Serializable,Cloneable和List接口,因此它也拥有克隆,实现序列化等特性,提供了相关的添加、删除、修改、遍历等功能。
需要注意的是LinkedList继承了AbstractSequentialList,所以LinkedList只支持顺序访问,并不具备ArrayList随机访问的特性。接下来我们就准备开始分析LinkedList的相关源码。
LinkedList数据结构
在分析源码之前,先展示出LinkedList的底层数据结构长啥样,如下图所示:
LinkedList底层是一个双向链表的结构,每个节点包含两个引用,prev指向当前节点前一个节点,next指向当前节点后一个节点,可以从头结点遍历到尾结点,也可以从尾结点遍历到头结点。
LinkedList核心源码
LinkedList类字段参数
- size:链表的元素个数。
- first:链表的头节点引用。
- last:链表的尾节点的引用。
Node是LinkedList的静态内部类,是链表组成的核心结构。 - item:存放元素数据。
- next:指向下一个节点的地址引用。
- prev:指向前一个节点的地址引用。
LinkedList构造方法
LinkedList有两个构造方法,一个是无参构造,没有任何代码实现,因为底层数据结构是双向链表,无需像ArrayList那样指定初始长度。
第二个构造方法,可以传入Collection接口实现类的集合对象,将集合内的元素使用addAll添加到LinkedList内部。
LinkedList常用方法源码
add(E e)(在链表末尾添加元素)
在add()方法里调用linkLast(e)方法在尾部添加元素,然后直接返回true了,我们来看下linkLast()方法。
linkLast(E e)(在链表尾部添加元素)
/**
* Links e as last element.
*/
void linkLast(E e) {
//定义l变量指向linkedList的last尾部节点地址
final Node<E> l = last;
//新建一个node节点对象,将新节点的prev指针指向last节点地址引用,当前元素值为e,next节点为空
final Node<E> newNode = new Node<>(l, e, null);
//将last变量重新指向新节点的地址引用,last永远指向尾部节点
last = newNode;
//判断如果以前的尾部节点为空,就意味着当前链表没有节点
if (l == null)
//此时就直接将first指向新节点的地址引用,新节点就作为头节点
//这种就是只有一个节点,头尾都指向这个节点
first = newNode;
else
//之前的尾部节点不为空的话,就将之前尾部节点的next指针指向新节点地址引用
//因为是双向链表,之前已经将新节点的prev指向旧的尾节点,
//在这里是将旧的尾节点的next指针指向新节点
l.next = newNode;
//链表长度加一
size++;
//结构修改次数加一 快速失败机制
modCount++;
}
接下来我们画图来展示下这个过程:
add(int index, E element)(在index索引位置添加元素)
在这个方法里先检查index是否越界(index >= 0 && index <= size),如果越界直接抛异常,没有越界接着判断index == size,如果index与链表大小size相等就是在链表末尾添加元素,直接调用linkLast()方法即可,上面已经分析过了,否则就调用linkBefore(element, node(index))方法在index位置添加元素。这里又涉及到两个新方法,我们一个一个的看。
node(int index)(寻找index位置对应的节点)
我们先来看下node(index)寻找节点方法是怎么实现的,看下面的代码部分。
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
//判断index是否小于链表长度的一半
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;
}
}
这里有一个需要关注的地方,我们知道双向链表的遍历,只能从头节点一直寻找next指针来往后遍历,或者从尾节点通过prev指针往前遍历,也就是说链表只能顺序遍历,不支持随机访问,这样就有一个问题,如果我们要找的元素下标index在很后的位置时,从头节点一直往后遍历寻找,相当于找遍整个链表才结束,时间复杂度为O(n),如果链表很长的话,查询效率肯定不高,jdk设计者们肯定也想到这一点,所以他们对此作了优化,先判断index在链表的前半部分还是后半部分,如果比较靠前就通过first头节点往后遍历,如果比较靠后就通过last尾节点往前遍历,将时间复杂度变为O(n/2),提高了查询效率。
linkBefore(E e, Node succ)(在succ节点前添加新节点元素)
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//定义pred变量指向succ的prev前驱节点
final Node<E> pred = succ.prev;
//新建一个节点,让其prev指针指向succ的前驱节点pred,next指针指向succ节点
//当前元素值为e
final Node<E> newNode = new Node<>(pred, e, succ);
//将succ节点的prev指针断开之前指向的前驱节点,重新指向新节点
succ.prev = newNode;
//判断succ的旧前驱节点是否为空
if (pred == null
//如果是空,意味着之前的first就指向的是succ节点,也就是succ是头节点
//此时在succ节点前插入一个新节点了,那么first指针就指向这个新节点
first = newNode;
else
//如果不为空,就将旧的前驱节点的next指向新节点
pred.next = newNode;
//链表长度加一
size++;
//结构修改次数加一 快速失败机制
modCount++;
}
同样,我们画图展示下这个插入的过程:
get(int index)(获取index位置上的元素值)
先检查一下index是否越界,然后再调用node(index)方法获取index位置上的节点,该节点的item就是元素值。node(index)方法在上面已经分析过,就不再看了。
indexOf(Object o)(寻找对象o在链表中第一次出现的下标索引)
public int indexOf(Object o) {
//定义index变量初始化为0
int index = 0;
//判断对象o是否为null
if (o == null) {
//如果为null,从链表头节点往后依次遍历,
for (Node<E> x = first; x != null; x = x.next) {
//如果发现节点的item值为null就直接返回index
if (x.item == null)
return index;
//每遍历一次,index下标加一
index++;
}
} else {
//不为空,从链表头节点往后遍历
for (Node<E> x = first; x != null; x = x.next) {
//调用equals()方法判断节点的item是否与对象o相等
//相等直接返回index
if (o.equals(x.item))
return index;
//每遍历一次,index下标加一
index++;
}
}
//遍历结束都没有找到,就返回-1
return -1;
}
lastIndexOf(Object o)(寻找对象o在链表中最后一次出现的下标索引)
与indexOf()方法类似,区别在于lastIndexof()方法是从链表尾节点往前遍历寻找,其余逻辑一样。
public int lastIndexOf(Object o) {
//定义index变量初始化为链表的末尾
int index = size;
//判断对象o是否为null
if (o == null) {
//如果为null,从链表尾节点往前依次遍历,
for (Node<E> x = last; x != null; x = x.prev) {
//每遍历一次,index下标往前移动一位
index--;
//如果发现节点的item值为null就直接返回index
if (x.item == null)
return index;
}
} else {
//不为空,从链表尾节点往前遍历
for (Node<E> x = last; x != null; x = x.prev) {
//每遍历一次,index下标往前移动一位
index--;
//调用equals()方法判断节点的item是否与对象o相等
//相等直接返回index
if (o.equals(x.item))
return index;
}
}
//遍历结束都没有找到,就返回-1
return -1;
}
remove(Object o)(在链表中删除对象o)
public boolean remove(Object o)
//判断对象o是否为null
if (o == null) {
//如果为空,从链表头节点往后遍历
for (Node<E> x = first; x != null; x = x.next) {
//如果找到节点x的item为null
if (x.item == null) {
//就调用unlink()方法将节点从链表中移除
unlink(x);
//移除成功返回true
return true;
}
}
} else {
//不为空,就通过equals()方法比对是否相等
for (Node<E> x = first; x != null; x = x.next) {
//如果找到x节点的item与对象o相等
if (o.equals(x.item)) {
//就调用unlink()方法将节点从链表中移除
unlink(x);
//移除成功返回true
return true;
}
}
}
//遍历结束没有找到,返回false
return false;
}
可以看到LinkedList的remove(o)方法会在内部调用unlink(x)方法移除节点x,我们接下来看下unlink(x)方法的具体实现。
unlink(Node x)(在链表中移除节点x)
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
//定义element变量保存节点x的item元素,便于方法返回
final E element = x.item;
//定义一个next变量引用指向节点x的next节点(后继节点)
final Node<E> next = x.next;
//定义一个prev变量引用指向节点x的prev节点(前驱节点)
final Node<E> prev = x.prev;
//判断节点x的prev节点是否为空
if (prev == null) {
//如果为空的话说明节点x就是链表的头节点,first一开始指向节点x
//现在要删除节点x,那么就将first指针重新指向节点x的next节点,让其变为新的头节点
first = next;
} else {
//如果不为空,就将节点x的prev节点(前驱节点)的next指针指向节点x的next节点(后继节点)
prev.next = next;
//再将节点x的prev指针指向null
//让节点x与它之前的prev节点断开
x.prev = null;
}
//判断节点x的next节点是否为空
if (next == null)
//如果为空说明节点x就是链表的尾节点,last一开始指向节点x
//现在要删除节点x,那么就将last指针重新指向节点x的prev节点,让其变为新的尾节点
last = prev;
} else {
//如果不为空,将节点x的next节点(后继节点)的prev指针指向节点x的prev节点(前驱节点)
next.prev = prev;
//再将节点x的next指针指向null
//让节点x与它之前的next节点断开
x.next = null;
}
//将节点x的item置为null
x.item = null;
//链表数量size减一
size--;
//结构修改次数加一
modCount++;
//返回旧元素值
return element;
}
老规矩,我们用画图展示下这个删除节点的流程:
上面都是实现的list类型接口的一些方法,接下来我们再来看下LinkedList作为Deque双端队列的一些方法实现。
getFirst()(获取队列头节点元素)
方法里直接返回first指针指向的节点的元素值。
getLast()(获取队列尾部节点元素)
同理,getLast()方法返回last指针指向的节点的元素值。
peek()/peekFirst()(获取队列头部的节点元素,不会删除节点)
在方法里判断first指针是否指向null,不为null返回first指针指向节点item元素值,否则返回null。
peekLast()(获取队列尾部的节点元素,不会删除节点)
在方法里判断last指针是否指向null,不为null返回last指针指向节点item元素值,否则返回null。
offer(E e)(往队列尾部添加元素)
方法内部调用add(e)方法往队列尾部添加元素,add()方法上面已经讲过了就不再叙述了。
offerFirst(E e)/push(E e)(往队列头部添加元素)
方法内部调用addFirst(e)方法,addFirst(e)方法内部又调用了linkFirst(e)方法,我们直接来看下linkFirst(e)方法的具体实现。
linkFirst(E e)(在链表头部添加元素)
/**
* Links e as first element.
*/
private void linkFirst(E e) {
//定义f变量引用指向first指针指向的节点地址
final Node<E> f = first;
//新建一个节点,新节点的prev指针指向null,next指针指向first指针指向的节点地址,
//当前元素值为e
final Node<E> newNode = new Node<>(null, e, f);
//将first指针重新指向新节点的地址,让新节点变为链表的头节点
first = newNode;
//判断first之前指向的节点,即之前的头节点是否为空
if (f == null)
//如果之前的头节点为空,说明整个链表一开始是没有节点的
//现在新建的节点就是链表唯一的节点
//上一步已经把first指向newNode了,现在只需要把last也指向新节点的地址就行了
last = newNode;
else
//如果不为空的话,就将之前头节点的prev指针重新指向新节点的地址
//让新节点在旧节点的前面
f.prev = newNode;
//链表长度加一
size++;
//结构修改次数加一
modCount++;
}
我们再画一张图展示下这个添加节点的过程:
offerLast(E e)(往队列尾部添加节点元素)
方法内部调用addLast(e)方法,addLast(e)方法内部又调用了linkLast(e)方法,linkLast(e)方法上面已经讲过就不再说了。
pollFirst()/poll()(从队列头部移除一个节点并返回节点元素值,会移除节点)
方法里判断头节点是否为空,如果为空直接返回null,否则调用unlinkFirst(f)方法将头节点从队列(链表)中移除。接下来分析一下unlinkFirst(f)这个方法。
unlinkFirst(Node f)(从链表头部移除头节点)
/**
* Unlinks non-null first node f.
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//定义element变量存储节点f的元素值,便于方法返回
final E element = f.item;
//定义next变量指向节点f的next指针指向的节点
final Node<E> next = f.next;
//将节点f的元素清空
f.item = null;
//将节点f的next指针指向null,
//帮助gc回收,把节点f移除后,后续gc垃圾收集器会回收节点f占用的内存
//回收的条件就是从gc root根都没有任何引用指向节点f,并且节点f也没有指向任何别的节点
//不然的话,收集器会任务节点f不是垃圾对象就不会回收了。
f.next = null; // help GC
//将first指针重新指向为next变量指向的节点地址,就是节点f的下一个节点,让其变为新的头节点
first = next;
//判断旧的头节点f的下一个节点是否为空
if (next == null)
//如果为空,说明当时链表只有一个节点f,first和last都指向的节点f,
//现在要把节点f删除了,链表就没有节点了,first和last指针都需要指向null
last = null;
else
//如果不为空,就将新的头节点的prev指针指向null
next.prev = null;
//链表长度减一
size--;
//结构修改次数加一
modCount++;
//返回老的元素值
return element;
}
一样,我们画图理解下这个流程:
pollLast()(从队列尾部移除节点元素)
方法里判断尾节点是否为空,如果为空直接返回null,否则调用unlinkLast(l)方法将尾节点从队列(链表)中移除。接下来分析一下unlinkLast(l)这个方法。
unlinkLast(Node l)(从链表尾部移除尾节点)
/**
* Unlinks non-null last node l.
*/
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
//定义element变量保存节点l的item元素值,便于方法返回
final E element = l.item;
//定义prev变量指向节点了的prev指针指向的节点地址
final Node<E> prev = l.prev;
//清空节点了的item数据
l.item = null;
//将节点l的prev指针指向null
l.prev = null; // help
//将last指针重新指向到节点l的前一个节点,让其变为新的尾节点
last = prev;
//判断节点l的前一个节点是否为空
if (prev == null)
//如果为空,说明链表当时只有节点l一个节点存在
//first和last指针都指向了节点l
//现在要删除节点l了,链表就为空了,就要将first和last指针都指向null
first = null;
else
//如果不为空就将节点l的前一个节点的next指针指向null
prev.next = null;
//链表长度减一
size--;
//结构修改次数加一
modCount++;
//返回旧元素值
return element;
}
下图展示了上述代码的逻辑流程:
pop()/remove()(从队列头部移除一个元素)
方法内部调用removeFirst()方法,removeFirst()内部调用unlinkFirst(f)方法从队列头部移除节点元素,unlinkFirst(f)这个方法已经分析过了。
LinkedList总结
- LinkedList与ArrayList一样都是线程不安全的。
- LinkedList底层数据结构是链表,是通过prev和next指针引用寻找前后节点,因此LinkedList的存储空间可以是不连续的,而ArrayList的必须是连续空间的一个数组。
- 相比ArrayList而言,LinkedList的查找效率要比ArrayList的效率低,链表是需要从头节点或者尾节点依次遍历得到目标节点,而数组直接可以通过下标索引得到,但是LinkedList的插入往往会ArrayList更快,链表的插入只要修改prev和next等指针的引用地址就行,而数组是需要重新拷贝一份数据的。