目录
1.概述
-
LinkedList是一个实现了List接口和Deque接口的双端链表。
-
LinkedList底层的链表结构使它支持高效的插入和删除操作,
-
另外它实现了Deque接口,使得LinkedList类也具有队列的特性,
-
LinkedList适合集合中先入先出,先入后出的场景,在队列源码中频繁使用
-
LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法
图解:
上图代表了一个双向链表结构,链表中的每个节点都可以向前或者向后追溯:
- 链表中每个节点叫Node,Node有prev属性,代表前一个节点的位置,next属性,代表后一个节点的位置
- first是双向链表的头节点,它的前一个节点是null
- last是双向链表的尾节点,它的后一个节点是null
- 当链表中没有数据时,first和last是同一个节点,前后指向都是null
- 因为是个双向链表,只要机器内存足够大,是没有大小限制的
2.源码剖析
2.1 节点类(LinkedList内部类)
上述的Node节点如下
private static class Node<E> {
E item; //元素值
LinkedList.Node<E> next; //指向后继节点的引用
LinkedList.Node<E> prev; //指向前驱结点的引用
//构造方法
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
- 这个类是LinkedList的内部类,代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。
‘2.2 成员变量
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;
}
2.3 构造函数
空构造方法
public LinkedList() { }
用已有的集合创建链表的构造方法:
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
2.4 添加元素
追加节点时,我们可以选择追加到链表头部,还是追加到链表尾部,add 方法默认是从尾部开始追加,addFirst 方法是从头部开始追加,我们分别来看下几种不同的追加方式:
(1)从链表尾部添加元素
/**
* 添加元素,即默认的在末尾添加,添加成功返回true
*/
public boolean add(E e) {
//在末尾添加元素
linkLast(e);
return true;
}
/**
* 在链表的末尾添加元素
*/
public void addLast(E e) {
linkLast(e);
}
/**
* 在链表的末尾添加元素
*/
void linkLast(E e) {
//保存原来的尾节点
final Node<E> l = last;
//创建新的节点,让它的prev指向原来的尾节点,next指向空
final Node<E> newNode = new Node<>(l, e, null);
//这时尾节点就是新的节点
last = newNode;
//但是原来的链表为空,则头节点就是新的节点
if (l == null)
first = newNode;
else
//原来的链表不为空,则让原来尾节点的next指向新节点
l.next = newNode;
//链表大小 + 1
size++;
//LinkedList集合的操作次数 + 1
modCount++;
}
- 可以发现add和addLast方法均是调用的内部默认权限的函数linkLast
- add和addLast的区别就是add添加成功后有返回值true,而addLast没有返回值
(2)从链表头部添加元素
/**
* 在链表的头部添加元素
*/
public void addFirst(E e) {
linkFirst(e);
}
/**
* 在链表的头部添加元素的真正实现
*/
private void linkFirst(E e) {
//在头部插入节点,所以先保存原始头结点
final Node<E> f = first;
//新建一个节点,让它的next指向原来的头结点
final Node<E> newNode = new Node<>(null, e, f);
//头节点为新节点
first = newNode;
if (f == null)
//原来的头结点为null,即链表为空 first=last=null
//此时由于有了新节点,尾节点就变为了新节点
last = newNode;
else
//链表本身不为空
//原来的头结点的prev指向新节点
f.prev = newNode;
//链表大小 + 1
size++;
//LinkedList集合的操作次数 + 1
modCount++;
}
(3)在指定节点的前后添加元素
/**
* 向指定索引处添加元素
*/
public void add(int index, E element) {
/**
* 对索引进行范围检查
*
* checkPositionIndex专门用于确保索引可以为size,即在[0,size]
*
* 此处可以为size是因为为size时,是在尾部添加元素
*/
checkPositionIndex(index);
//在尾部添加员
if (index == size)
linkLast(element);
else
//在第index节点前插入该元素,则插入后,新节点的索引即为index
linkBefore(element, node(index));
}
/**
* 在指定节点前插入元素e
*/
void linkBefore(E e, Node<E> succ) {
/**
* 此处没有判断succ是否为null,因为此方法仅在此类中使用,
* 所以作者的使用会保证它不为null,所以不必要给外界抛出异常
*/
//在链表的插入和删除中一般都要先保存对应位置的节点,如下
//先用临时节点pred 保存succ的前驱结点
final Node<E> pred = succ.prev;
//新建一个结点,值为e,前驱节点为succ的前驱节点,后继节点为succ
final Node<E> newNode = new Node<>(pred, e, succ);
//让succ的prev引用指向新创建的节点,即插入的节点
succ.prev = newNode;
//既然是在前面插入,我们就要考虑边界,即当前节点succ为头结点的情况,即pred=null
if (pred == null)
//succ为头节点,插入后,新的节点变为头节点
first = newNode;
else
//不为头结点的话,将原来的succ的前驱节点的next引用指向新的节点
pred.next = newNode;
//链表大小 + 1
size++;
//LinkedList集合的操作次数 + 1
modCount++;
}
(4)将集合插入链表
/**
* 将集合插入链表尾部
*
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
/**
* 将集合插入链表指定索引处
*
*/
public boolean addAll(int index, Collection<? extends E> c) {
//1.检查index范围是否在size之内
checkPositionIndex(index);
//2.toArray()方法把集合的数据存到对象数组中
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//3.得到插入位置的前驱节点和后继节点
Node<E> pred, succ;
//如果插入位置为尾部,前驱节点为last,后继节点为null
if (index == size) {
succ = null;
pred = last;
}
//否则,调用node()方法得到后继节点,再得到前驱节点
else {
succ = node(index);
pred = succ.prev;
}
// 4:遍历数据将数据插入
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//创建新节点
Node<E> newNode = new Node<>(pred, e, null);
//如果插入位置在链表头部
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
//如果插入位置在尾部,重置last节点
if (succ == null) {
last = pred;
}
//否则,将插入的链表与先前链表连接起来
else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
上面可以看出addAll方法通常包括下面四个步骤:
- 1.检查index范围是否在size之内
- 2.toArray()方法把集合的数据存到对象数组中
- 3.得到插入位置的前驱和后继节点
- 4.遍历数据,将数据插入到指定位置
2.5 删除元素
(1)从头部删除节点
/**
* 从头删除节点
*
* f是链表头节点
*/
private E unlinkFirst(Node<E> f) {
// 拿出头节点的值,作为方法的返回值
final E element = f.item;
// 拿出头节点的下一个节点
final Node<E> next = f.next;
//帮助GC回收头节点
f.item = null;
f.next = null;
// 头节点的下一个节点成为头节点
first = next;
//如果 next 为空,表明链表为空
if (next == null)
last = null;
//链表不为空,头节点的前一个节点指向 null
else
next.prev = null;
//链表大小-1
size--;
//操作LinkedList的次数+1
modCount++;
return element;
}
/**
* 提供给用户调用的方法
* 封装了对链表为空的判断
*
* 内部最终还是调用unlinkFirst
*/
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
(2)从尾部删除元素
/**
* 从尾部删除节点
*
* l是链表尾节点
*/
private E unlinkLast(Node<E> l) {
//保存尾节点的值,作为方法的返回值
final E element = l.item;
//保存尾节点的前一个节点
final Node<E> prev = l.prev;
//帮助GC回收头节点
l.item = null;
l.prev = null; // help GC
//尾节点的前一个节点成为尾节点
last = prev;
//如果 prev 为空,表明移除尾节点后链表为空
if (prev == null)
first = null;
//链表不为空,尾节点的下一个节点指向null
else
prev.next = null;
//链表大小-1
size--;
//操作LinkedList的次数+1
modCount++;
return element;
}
/**
* 提供给用户调用的方法
* 封装了对链表为空的判断
*
* 内部最终还是调用unlinkLast
*/
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
(3)删除指定节点
/**
* 删除指定节点
* @param x
* @return
*/
E unlink(Node<E> x) {
//保存节点x的元素值、前驱节点、后继节点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//x的prev为null,表明节点x是头节点,让头节点后移
if (prev == null) {
first = next;
//x不是头节点,让前驱节点的next指针指向x的下一个节点,
} else {
prev.next = next;
//并把x的字段prev置为null,帮助垃圾回收
x.prev = null;
}
//x的next为null,表明节点x是尾节点,让尾节点前移
if (next == null) {
last = prev;
//x不是尾节点,让后继节点的prev指针指向x的前一个节点,
} else {
next.prev = prev;
//并把x的字段next置为null,帮助垃圾回收
x.next = null;
}
//将节点x的元素值置空
x.item = null;
//链表大小-1
size--;
//操作链表的次数+1
modCount++;
//返回节点的元素值
return element;
}
/**
* 删除指定索引处的节点
* @param index
* @return
*/
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/**
* 删除指定元素值的第一个节点
*
*/
public boolean remove(Object o) {
//如果传入的元素值为null
if (o == null) {
//从头开始遍历查找元素值为null的第一个节点,如果找到调用unlink删除该节点,然后返回true
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
//如果传入的元素值不为null
} else {
//从头开始遍历查找元素值和传入元素值相等的第一个节点,如果找到调用unlink删除该节点,然后返回true
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
//如果没有找到该值,就返回false
return false;
}
2.6 节点查询
链表查询某一个节点是比较慢的,需要挨个循环查找才行,我们看看 LinkedList 的源码是如何寻找节点的?
Node<E> node(int index) {
/**
* 同理,这里也未对index做检查,是因为此方法只供此类使用,作者确保它使用时没有超范围
*/
/**
* 此处使用了一个巧妙的技巧:
* 判断index和(size/2)大小,缩小了一般的循环范围(size >> 1 是 size 除以 2 的意思)
* 如果index在链表的前半部分,就从第一个节点开始循环找到它的位置
* 如果index在链表的后半部分,就从最后一个节点开始循环找到它的位置
*/
if (index < (size >> 1)) {
//index在前半部分
Node<E> x = first;
// 直到 for 循环到 index 的前一个 node 停止
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//index在后半部分
Node<E> x = last;
// 直到 for 循环到 index 的后一个 node 停止
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
- 从源码中我们可以发现,LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找性能
2.7 实现Queue接口和Deque的方法对比
LinkedList 实现了 Queue 接口,在新增、删除、查询等方面增加了很多新的方法,这些方法在平时特别容易混淆,在链表为空的情况下,返回值也不太一样
添加:
/**
* 与add相同
*/
public boolean offer(E e) {
return add(e);
}
删除:
/**
* 与removeFirst相同
*
*/
public E remove() {
return removeFirst();
}
/**
* 如果链表为空返回null
* 否则调用unlinkFirst(f)来删除头节点
*
*/
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
查询:
/**
* 链表为空抛出异常
* 否则返回头节点
*/
public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
/**
* 如果链表为空返回null
* 否则返回头节点
*/
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
列表对比
方法含义 | 返回异常 | 返回特殊值 | 底层实现 |
新增 | add(e)和offer(e) | ||
删除 | remove() | poll(e) | 链表为空时,remove会抛出异常,poll返回null |
查找 | element(e) | peek(e) | 链表为空时,element会抛出异常,peek返回null |
同样,LinkedList 也实现了 Deque 接口,对新增、删除和查找都提供从头开始,还是从尾开始两种方向的方法
原理同上都是简单的封装,请直接阅读源码
2.8 迭代器
因为 LinkedList 要实现双向的迭代访问,所以我们使用 Iterator 接口肯定不行了,因为 Iterator 只支持从头到尾的访问。Java 新增了一个迭代接口,叫做:ListIterator,这个接口提供了向前和向后的迭代方法
LinkedList的迭代器ListItr实现了ListIterator
// 双向迭代器
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;//上一次执行 next() 或者 previos() 方法时的节点位置
private Node<E> next;//下一个节点
private int nextIndex;//下一个节点的位置
//expectedModCount:期望版本号;modCount:目前最新版本号
private int expectedModCount = modCount;
…………
}
从头到尾的迭代:
// 判断还有没有下一个元素
public boolean hasNext() {
return nextIndex < size;// 下一个节点的索引小于链表的大小,就有
}
// 取下一个元素
public E next() {
//检查期望版本号有无发生变化
checkForComodification();
if (!hasNext())//再次检查
throw new NoSuchElementException();
// next 是当前节点,在上一次执行 next() 方法时被赋值的。
// 第一次执行时,是在初始化迭代器的时候,next 被赋值的
lastReturned = next;
// next 是下一个节点了,为下次迭代做准备
next = next.next;
nextIndex++;
return lastReturned.item;
}
从尾到头的迭代:
// 如果上次节点索引位置大于 0,就还有节点可以迭代
public boolean hasPrevious() {
return nextIndex > 0;
}
// 取前一个节点
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// next 为空场景:1:说明是第一次迭代,取尾节点(last);2:上一次操作把尾节点删除掉了
// next 不为空场景:说明已经发生过迭代了,直接取前一个节点即可(next.prev)
lastReturned = next = (next == null) ? last : next.prev;
// 索引位置变化
nextIndex--;
return lastReturned.item;
}
迭代器的删除:
LinkedList删除的时候,推荐通过迭代器删除:
public void remove() {
checkForComodification();
// lastReturned 是本次迭代需要删除的值,分以下空和非空两种情况:
// lastReturned 为空,说明调用者没有主动执行过 next() 或者 previos(),直接报错
// lastReturned 不为空,是在上次执行 next() 或者 previos()方法时赋的值
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
//删除当前节点
unlink(lastReturned);
// next == lastReturned 的场景分析:从尾到头递归顺序,并且是第一次迭代,并且要删除最后一个元素的情况下
// 这种情况下,previous() 方法里面设置了 lastReturned = next = last,所以 next 和 lastReturned会相等
if (next == lastReturned)
// 这时候 lastReturned 是尾节点,lastNext 是 null,所以 next 也是 null,这样在 previous() 执行时,
//发现 next 是 null,就会把尾节点赋值给 next
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
3.ArrayList和LinkedList的区别
-
1. 是否保证线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全; -
2. 底层数据结构:
Arraylist
底层使用的是Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) -
3. 插入和删除是否受元素位置的影响:
-
①
ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候,ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 -
②
LinkedList
采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
-
-
4. 是否支持快速随机访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。 -
5. 内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
4.LinkedList常用方法测试
此段代码来自:https://snailclimb.top/JavaGuide/#/java/collection/LinkedList
package LinkedList;
import java.util.Iterator;
import java.util.LinkedList;
public class LinkedListDemo01 {
public static void main(String[] srgs) {
//创建存放int类型的linkedList
LinkedList<Integer> linkedList = new LinkedList<>();
/************************** linkedList的基本操作 ************************/
linkedList.addFirst(0); // 添加元素到列表开头
linkedList.add(1); // 在列表结尾添加元素
linkedList.add(2, 2); // 在指定位置添加元素
linkedList.addLast(3); // 添加元素到列表结尾
System.out.println("LinkedList(直接输出的): " + linkedList);
System.out.println("getFirst()获得第一个元素: " + linkedList.getFirst()); // 返回此列表的第一个元素
System.out.println("getLast()获得第最后一个元素: " + linkedList.getLast()); // 返回此列表的最后一个元素
System.out.println("removeFirst()删除第一个元素并返回: " + linkedList.removeFirst()); // 移除并返回此列表的第一个元素
System.out.println("removeLast()删除最后一个元素并返回: " + linkedList.removeLast()); // 移除并返回此列表的最后一个元素
System.out.println("After remove:" + linkedList);
System.out.println("contains()方法判断列表是否包含1这个元素:" + linkedList.contains(1)); // 判断此列表包含指定元素,如果是,则返回true
System.out.println("该linkedList的大小 : " + linkedList.size()); // 返回此列表的元素个数
/************************** 位置访问操作 ************************/
System.out.println("-----------------------------------------");
linkedList.set(1, 3); // 将此列表中指定位置的元素替换为指定的元素
System.out.println("After set(1, 3):" + linkedList);
System.out.println("get(1)获得指定位置(这里为1)的元素: " + linkedList.get(1)); // 返回此列表中指定位置处的元素
/************************** Search操作 ************************/
System.out.println("-----------------------------------------");
linkedList.add(3);
System.out.println("indexOf(3): " + linkedList.indexOf(3)); // 返回此列表中首次出现的指定元素的索引
System.out.println("lastIndexOf(3): " + linkedList.lastIndexOf(3));// 返回此列表中最后出现的指定元素的索引
/************************** Queue操作 ************************/
System.out.println("-----------------------------------------");
System.out.println("peek(): " + linkedList.peek()); // 获取但不移除此列表的头
System.out.println("element(): " + linkedList.element()); // 获取但不移除此列表的头
linkedList.poll(); // 获取并移除此列表的头
System.out.println("After poll():" + linkedList);
linkedList.remove();
System.out.println("After remove():" + linkedList); // 获取并移除此列表的头
linkedList.offer(4);
System.out.println("After offer(4):" + linkedList); // 将指定元素添加到此列表的末尾
/************************** Deque操作 ************************/
System.out.println("-----------------------------------------");
linkedList.offerFirst(2); // 在此列表的开头插入指定的元素
System.out.println("After offerFirst(2):" + linkedList);
linkedList.offerLast(5); // 在此列表末尾插入指定的元素
System.out.println("After offerLast(5):" + linkedList);
System.out.println("peekFirst(): " + linkedList.peekFirst()); // 获取但不移除此列表的第一个元素
System.out.println("peekLast(): " + linkedList.peekLast()); // 获取但不移除此列表的第一个元素
linkedList.pollFirst(); // 获取并移除此列表的第一个元素
System.out.println("After pollFirst():" + linkedList);
linkedList.pollLast(); // 获取并移除此列表的最后一个元素
System.out.println("After pollLast():" + linkedList);
linkedList.push(2); // 将元素推入此列表所表示的堆栈(插入到列表的头)
System.out.println("After push(2):" + linkedList);
linkedList.pop(); // 从此列表所表示的堆栈处弹出一个元素(获取并移除列表第一个元素)
System.out.println("After pop():" + linkedList);
linkedList.add(3);
linkedList.removeFirstOccurrence(3); // 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表)
System.out.println("After removeFirstOccurrence(3):" + linkedList);
linkedList.removeLastOccurrence(3); // 从此列表中移除最后一次出现的指定元素(从尾部到头部遍历列表)
System.out.println("After removeLastOccurrence(3):" + linkedList);
/************************** 遍历操作 ************************/
System.out.println("-----------------------------------------");
linkedList.clear();
for (int i = 0; i < 100000; i++) {
linkedList.add(i);
}
// 迭代器遍历
long start = System.currentTimeMillis();
Iterator<Integer> iterator = linkedList.iterator();
while (iterator.hasNext()) {
iterator.next();
}
long end = System.currentTimeMillis();
System.out.println("Iterator:" + (end - start) + " ms");
// 顺序遍历(随机遍历)
start = System.currentTimeMillis();
for (int i = 0; i < linkedList.size(); i++) {
linkedList.get(i);
}
end = System.currentTimeMillis();
System.out.println("for:" + (end - start) + " ms");
// 另一种for循环遍历
start = System.currentTimeMillis();
for (Integer i : linkedList)
;
end = System.currentTimeMillis();
System.out.println("for2:" + (end - start) + " ms");
// 通过pollFirst()或pollLast()来遍历LinkedList
LinkedList<Integer> temp1 = new LinkedList<>();
temp1.addAll(linkedList);
start = System.currentTimeMillis();
while (temp1.size() != 0) {
temp1.pollFirst();
}
end = System.currentTimeMillis();
System.out.println("pollFirst()或pollLast():" + (end - start) + " ms");
// 通过removeFirst()或removeLast()来遍历LinkedList
LinkedList<Integer> temp2 = new LinkedList<>();
temp2.addAll(linkedList);
start = System.currentTimeMillis();
while (temp2.size() != 0) {
temp2.removeFirst();
}
end = System.currentTimeMillis();
System.out.println("removeFirst()或removeLast():" + (end - start) + " ms");
}
}