本文基于jdk8
一、概述
1. 简介
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList实现了所有的列表操作,允许所有的元素(包括空元素)。LinkedList所有的操作都是在对双向链表操作,LinkedList不是线程安全的。Collections.synchronizedList方法可以实现线程安全的操作。
2. 继承体系
通过继承体系,我们可以看到LinkedList不仅实现了List接口,还实现了Queue和Deque接口,所以它既能作为List使用,也能作为双端队列使用,当然也可以作为栈使用。
3. 数据结构
LinkedList与Collection关系如下图:
LinkedList的本质是双向链表。
(1) LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口。
(2) LinkedList包含两个重要的成员:header 和 size。
header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。
size是双向链表中节点的个数。
二、源码解析
1. 属性
/**
* 链表的节点个数
*/
transient int size = 0;
/**
* 双向链表的首个节点
*/
transient Node<E> first;
/**
* 双向链表的最后一个节点
*/
transient Node<E> last;
内部类Node
内部类为双向链表的每个节点。item用于保存数据,有一个prev指针和next指针,分别指向链表当前节点的前一个节点和后一个节点
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;
}
}
2. 构造函数
/**
* 构造一个空列表。
*/
public LinkedList() {
}
/**
* C构造一个包含指定 collection 中的元素的列表,
* 这些元素按其 collection 的迭代器返回的顺序排列。
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
LinkedList()构造一个空列表,里面没有任何元素。
LinkedList(Collection<? extends E> c): 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。该构造函数首先会调用LinkedList(),构造一个空列表,然后调用了addAll()方法将Collection中的所有元素添加到列表中。以下是addAll()的源代码:
/**
* 添加指定 collection 中的所有元素到此列表的结尾,
* 顺序是指定 collection 的迭代器返回这些元素的顺序。
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
/**
* 将指定 collection 中的所有元素从指定位置开始插入此列表。
* 其中index表示在其中插入指定collection中第一个元素的索引
*/
//以index为插入下标,插入集合c中所有元素
public boolean addAll(int index, Collection<? extends E> c) {
//检查越界 [0,size] 闭区间
checkPositionIndex(index);
//拿到目标集合数组
Object[] a = c.toArray();
//新增元素的数量
int numNew = a.length;
//如果新增元素数量为0,则不增加,并返回false
if (numNew == 0)
return false;
//index节点的前置节点,后置节点
Node<E> pred, succ;
if (index == size) {
//在链表尾部追加数据
//size节点(队尾)的后置节点一定是null
succ = null;
//前置节点是队尾
pred = last;
} else {
//取出index节点,作为后置节点
succ = node(index);
//前置节点是,index节点的前一个节点
pred = succ.prev;
}
//链表批量增加,是靠for循环遍历原数组,依次执行插入节点操作。
//对比ArrayList是通过System.arraycopy完成批量增加的
for (Object o : a) {//遍历要添加的节点。
@SuppressWarnings("unchecked") E e = (E) o;
//以前置节点 和 元素值e,构建new一个新节点,
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null) //如果前置节点是空,说明是头结点
first = newNode;
else//否则 前置节点的后置节点设置问新节点
pred.next = newNode;
//步进,当前的节点为前置节点了,为下次添加节点做准备
pred = newNode;
}
//循环结束后,判断,如果后置节点是null。 说明此时是在队尾append的。
if (succ == null) {
last = pred; //则设置尾节点
} else {
// 否则是在队中插入的节点 ,更新前置节点 后置节点
pred.next = succ;
//更新后置节点的前置节点
succ.prev = pred;
}
size += numNew; // 修改数量size
modCount++; //修改modCount
return true;
}
//根据index 查询出Node,
Node<E> node(int index) {
// assert isElementIndex(index);
// size >> 1 表示size除以2
//通过下标获取某个node 的时候,(增、查 ),
//会根据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;
}
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
//插入时的检查,下标可以是size [0,size]
return index >= 0 && index <= size;
}
执行逻辑:
(1) 使用 this() 调用默认的无参构造函数。
(2) 调用 addAll() 方法,传入当前的节点个数size,此时size为0,并将collection对象传递进去
(3) 检查index有没有数组越界的嫌疑
(4) 将collection转换成数组对象a
(5) 循环遍历a数组,然后将a数组里面的元素创建成拥有前后连接的节点,然后一个个按照顺序连起来。
(6) 修改当前的节点个数size的值
(7) 操作次数modCount自增1
通过下标获取某个node 的时候,(add select),会根据index处于前半段还是后半段 进行一个折半,以提升查询效率
3. 插入
LinkedList 除了实现了 List 接口相关方法,还实现了 Deque 接口的很多方法,所以我们有很多种方式插入元素。LinkedList 插入元素的过程实际上就是链表链入节点的过程,比较简单。
/** 在链表尾部插入元素 */
public boolean add(E e) {
linkLast(e);
return true;
}
/** 在链表指定位置插入元素 */
public void add(int index, E element) {
checkPositionIndex(index);
// 判断 index 是不是链表尾部位置,
// 如果是,直接将元素节点插入链表尾部即可
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
/** 将元素节点插入到链表尾部 */
void linkLast(E e) {
final Node<E> l = last;
// 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
final Node<E> newNode = new Node<>(l, e, null);
// 将 last 引用指向新节点
last = newNode;
// 判断尾节点是否为空,为空表示当前链表还没有节点
if (l == null)
first = newNode;
else
// 让原尾节点后继引用 next 指向新的尾节点
l.next = newNode;
size++;
modCount++;
}
/** 将元素节点插入到 succ 之前的位置 */
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
// 1. 初始化节点,并指明前驱和后继节点
final Node<E> newNode = new Node<>(pred, e, succ);
// 2. 将 succ 节点前驱引用 prev 指向新节点
succ.prev = newNode;
// 判断尾节点是否为空,为空表示当前链表还没有节点
if (pred == null)
first = newNode;
else
// 3. succ 节点前驱的后继引用指向新节点
pred.next = newNode;
size++;
modCount++;
}
上面两个 add 方法只是对操作链表的方法做了一层包装,核心逻辑在 linkBefore 和 linkLast 中。这里以 linkBefore 为例,它的逻辑流程如下:
创建新节点,并指明新节点的前驱和后继
将 succ 的前驱引用指向新节点
如果 succ 的前驱不为空,则将 succ 前驱的后继引用指向新节点
4. 删除
删除操作通过解除待删除节点与前后节点的链接。
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 遍历链表,找到要删除的节点
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x); // 将节点从链表中移除
return true;
}
}
}
return false;
}
public E remove(int index) {
checkElementIndex(index);
// 通过 node 方法定位节点,并调用 unlink 将节点从链表中移除
return unlink(node(index));
}
/** 将某个节点从链表中移除 */
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;
// prev 为空,表明删除的是头节点
if (prev == null) {
first = next;
} else {
// 将 x 的前驱的后继指向 x 的后继
prev.next = next;
// 将 x 的前驱引用置空,断开与前驱的链接
x.prev = null;
}
// next 为空,表明删除的是尾节点
if (next == null) {
last = prev;
} else {
// 将 x 的后继的前驱指向 x 的前驱
next.prev = prev;
// 将 x 的后继引用置空,断开与后继的链接
x.next = null;
}
// 将 item 置空,方便 GC 回收
x.item = null;
size--;
modCount++;
return element;
}
和插入操作一样,删除操作方法也是对底层方法的一层保证,删也一定会修改modCount。 按下标删,也是先根据index找到Node,然后去链表上unlink掉这个Node。 按元素删,会先去遍历链表寻找是否有该Node,考虑到允许null值,所以会遍历两遍,然后再去unlink它。
核心逻辑在底层 unlink 方法中,分析下 unlink 方法的逻辑,如下(假设删除的节点既不是头节点,也不是尾节点):
将待删除节点 x 的前驱的后继指向 x 的后继
将待删除节点 x 的前驱引用置空,断开与前驱的链接
将待删除节点 x 的后继的前驱指向 x 的前驱
将待删除节点 x 的后继引用置空,断开与后继的链接
5. 查询获取get
get方法
首先是判断索引位置有没有越界,确定完成之后开始遍历链表的元素,那么从头开始遍历还是从结尾开始遍历呢,这里其实是要索引的位置与当前链表长度的一半去做对比,如果索引位置小于当前链表长度的一半,否则从结尾开始遍历
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
getfirst方法
直接将第一个元素返回
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
getlast方法
直接将最后一个元素返回
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
6. 修改set方法
检查设置元素位然后置是否越界,如果没有,则索引到index位置的节点,将index位置的节点内容替换成新的内容element,同时返回旧值。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
7.push和pop方法
push其实就是调用addFirst(e)方法,pop调用的就是removeFirst()方法。
8. Deque接口
接口Deque的各个方法:
add(E e):队尾插入新节点,如果队列空间不足,抛出异常;LinkedList没有空间限制,所以可以无限添加。
offer(E e):队尾插入新节点,空间不足,返回false,在LinkedList中和add方法同样效果。
remove():移除队头节点,如果队列为空(没有节点,first为null),抛出异常。LinkedList中就是first节点(链表头)
poll():同remove,不同点:队列为空,返回null
element():查询队头节点(不移除),如果队列为空,抛出异常。
peek():同element,不同点:队列为空,返回null。
三、总结
LinkedList是基于双端链表的List,保留了头尾两个指针 ,其内部的实现源于对链表的操作,所以适用于频繁增加、删除的情况;该类不是线程安全的;另外,由于LinkedList实现了Queue接口,所以LinkedList不止有队列的接口,还有栈的接口,可以使用LinkedList作为队列和栈的实现。另外LinkedList也有 failFast 机制,这个机制主要在迭代器中使用。
1. 特点
链表批量增加,是靠for循环遍历原数组,依次执行插入节点操作。对比ArrayList是通过System.arraycopy完成批量增加的。增加一定会修改modCount。
通过下标获取某个node 的时候,(add select),会根据index处于前半段还是后半段 进行一个折半,以提升查询效率
删也一定会修改modCount。 按下标删,也是先根据index找到Node,然后去链表上unlink掉这个Node。 按元素删,会先去遍历链表寻找是否有该Node,如果有,去链表上unlink掉这个Node。
改也是先根据index找到Node,然后替换值。改不修改modCount。
查本身就是根据index找到Node。所以它的CRUD操作里,都涉及到根据index去找到Node的操作。
2. ArrayList与LinkedList的区别
ArrayList本质是数组, LinkedList本质是链表,数组和链表各自的特性 数组和链表的特性差异,本质是:连续空间存储和非连续空间存储的差异。
区别主要有下面两点:
ArrayList:底层是Object数组实现的:由于数组的地址是连续的,数组支持O(1)随机访问;数组在初始化时需要指定容量;数组不支持动态扩容,像ArrayList、Vector和Stack使用的时候看似不用考虑容量问题(因为可以一直往里面存放数据);但是它们的底层实际做了扩容;数组扩容代价比较大,需要开辟一个新数组将数据拷贝进去,数组扩容效率低;适合读数据较多的场合。
LinkedList:底层使用一个Node数据结构,有前后两个指针,双向链表实现的。相对数组,链表插入效率较高,只需要更改前后两个指针即可;另外链表不存在扩容问题,因为链表不要求存储空间连续,每次插入数据都只是改变last指针;另外,链表所需要的内存比数组要多,因为他要维护前后两个指针;它适合删除,插入较多的场景LinkedList还实现了Deque接口。
四、参考
JDK8:LinkedList源码分析:
https://blog.csdn.net/dabusiGin/article/details/102470126