目录
一、概述
Java中LinkedList是一个双向链表实现的集合类,它其中每一个元素都是一个节点,每个节点都包含一个数据元素和一个指向下一个节点的引用,因此LinkedList的元素是通过节点之间的链接来组织的。
双向链表
双向链表是一种常见的数据结构,它由一系列节点组成,每个节点包含两个指针,分别指向前一个节点和后一个节点。双向链表中的节点通常包含三个字段:数据元素、指向前一个节点的指针、指向后一个节点的指针。
在双向链表中,插入和删除元素的操作效率非常高,因为只需要修改相对应节点的指针即可。因此双向链表非常适合在需要频繁插入、删除的场景下使用。
双向链表中插入节点时:只需要将其前一个节点的指针和后一个节点的指针都指向新节点。
双向链表中删除节点时:只需要修改前一个节点和后一个节点的指针,将其指向链表的下一个节点即可。
类图
看向上图:LinkedList
实现了4个接口和继承1个抽象类
List
:主要提供添加、删除、修改、遍历等操作Cloneable
:支持克隆Serializable
:支持序列化Deque
:提供双端队列的功能,支持快速在头尾添加和读取元素
LinkedList
所实现的接口相比于ArrayList
是少一个RandomAccess
接口,说明LinkedList是不支持随机访问的,但同时多了一个Deque
接口,新增了双端队列的能力。
源码如下:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// .......
}
二、源码解读
注:不同jdk版本的源码会有一定差异,不过大致上是相同的,我使用的是 openjdk version “1.8.0_342”。
成员变量
// 头节点
transient Node<E> first;
// 尾节点
transient Node<E> last;
// 数量
transient int size = 0;
// 底层实现是 双向链表
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;
}
}
看向上述源码:LinkedList的节点实现完全符合双向链表的数据结构要求。
构造函数
/**
* 空参数的构造由于生成一个空链表 first = last = null
*/
public LinkedList() {
}
/**
* 传入一个集合类,来构造一个包含此集合所有元素的 LinkedList 集合
* Collection c 其内部的元素将按顺序作为 LinkedList 节点
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
相对于ArrayList
来说,LinkedList
没有类似于指定初始化容量的构造方法。造成这个区别的原因是它们底层的实现方式不一样,LinkedList
是基于节点实现的双向链表,每一个添加元素的时候,只需要new
一个节点即可
注:addAll(); 后文新增元素会详细介绍
新增元素
新增元素的方法主要有如下4个:
public boolean add(E e)
:在链表节点后顺序添加一个元素(默认是在末尾)public void add(int index, E element)
:在指定下标位置添加元素public boolean addAll(Collection<? extends E> c)
:在链表节点后顺序添加此集合 c 的所有元素(默认是在末尾)public boolean addAll(int index, Collection<? extends E> c)
:在指定下标位置添加此集合 c 的所有元素
第一种public boolean add(E e)
在链表节点后顺序添加一个元素(默认是在末尾)
public boolean add(E e) {
// 在链表的末尾插入指定元素
linkLast(e);
// 添加成功 返回true
return true;
}
继续向下解读:
// 在链表的末尾插入指定元素
void linkLast(E e) {
// 保存之前的last节点
final Node<E> l = last;
// 创建一个新的节点,并将新节点prev指针指向之前的last节点
final Node<E> newNode = new Node<>(l, e, null);
// 将last索引指向新的last节点
last = newNode;
// 如果之前的last节点为空,则表示此链表为空链表。那么将first索引指向新节点
// 此时 first last 指针都会指向此新节点
if (l == null)
first = newNode;
// 否则,将之前的last节点的next指针指向新节点
else
l.next = newNode;
// 数量+1
size++;
// 这是一个自增操作,增加了列表的修改计数。这个计数是用来跟踪列表被修改了多少次,这对于一些并发控制是非常有用的。(可以暂时不用关注)
modCount++;
}
一句话概括:首先是根据此元素创建一个新节点;其次将链表的last
指针指向此新节点(此处两种情况)。情况1、如果之前的last节点为空,则表示此链表为空链表。那么将first索引指向新节点,此时 first last 指针都会指向此新节点。情况2、之前的last节点不为空,,则表示此链表有元素且不为空,那么将之前的last节点的next指针指向新节点。
第二种public void add(int index, E element)
:
在指定下标位置添加元素
public void add(int index, E element) {
// 检查index的合法性
// index必须在此范围内(index >= 0 && index <= size)
checkPositionIndex(index);
// 如果指定下标位置index == size,则表示在末尾添加此元素
if (index == size)
// linkLast上文已解读,不赘述
linkLast(element)
// 在index处节点插入node
else
// node(index) 获取index位置的node节点
linkBefore(element, node(index));
}
继续向下解读
// 找到并返回指定index处的节点
Node<E> node(int index) {
// assert isElementIndex(index);
// 如果 index < size的一半 则从头开始遍历(类似于折半查找)
if (index < (size >> 1)) {
// 先拿到first 也就是第一个节点
Node<E> x = first;
// 再通过循环去 一个一个向后拿节点 最后遍历到index的位置 找到index位置的节点
for (int i = 0; i < index; i++)
x = x.next;
return x;
}
// 如果 index > size的一半 则从末尾开始遍历
else {
// 先拿到last也就是最后节点
Node<E> x = last;
// 再通过循环去 一个一个向前拿节点 最后遍历到index的位置 找到index位置的节点
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
继续向下解读
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 通过node(index)方法找到了index处的节点,在此方法中也就是succ
// 拿到succ的前一个节点
final Node<E> pred = succ.prev;
// 创建一个新节点 也就是我们想要插入的节点,并关联前后节点(前节点:succ的前一个节点;后节点:也就是succ)
final Node<E> newNode = new Node<>(pred, e, succ);
// 再将succ的前节点关联上此新节点
succ.prev = newNode;
// 如果succ前节点为null,则代表succ则是first头结点,也就是第一个节点
if (pred == null)
// first指向newNode作为新的首节点
first = newNode;
else
// 如果不为空,将succ前节点的后节点关联上此新节点
pred.next = newNode;
size++;
modCount++;
}
一句话概括:首先校验index的合法性是否在index >= 0 && index <= size
范围内,否则抛出异常。其次判断此index是否等于size
,如果是则表示是在末尾插入,直接调用linkLast
方法;如果不是则找到index下标处的节点,将新节点插入进去。
第三种public boolean addAll(Collection<? extends E> c)
:
在链表节点后顺序添加此集合 c 的所有元素(默认是在末尾)
此方法是直接调用第四种public boolean addAll(int index, Collection<? extends E> c)
方法,此处不解读,请继续向下阅读。
public boolean addAll(Collection<? extends E> c) {
// 此处是调用`public boolean addAll(int index, Collection<? extends E> c)`方法
// 默认是在末尾,因此index为size
return addAll(size, c);
}
第四种public boolean addAll(int index, Collection<? extends E> c)
:
在指定下标位置添加此集合 c 的所有元素
public boolean addAll(int index, Collection<? extends E> c) {
// 校验下标合法性 上述已解读 此处不再赘述
checkPositionIndex(index);
// 将集合c转为数组 以便遍历集合中的元素
Object[] a = c.toArray();
// 拿到此集合的长度并校验是否为0,如果为0表示集合中没有元素,直接返回false
int numNew = a.length;
if (numNew == 0)
return false;
// 定义变量:index的前一个节点pred、后一个节点的succ
Node<E> pred, succ;
// 如果指定下标位置index == size,则表示在末尾添加此元素
if (index == size) {
// succ 直接赋为null
succ = null;
// pred 则是应该是当前此链表的最后一个节点
pred = last;
}
// 在index处节点插入node
else {
// 拿到当前下标的节点
succ = node(index);
pred = succ.prev;
}
// 遍历数组 依次插入
for (Object o : a) {
@SuppressWarnings("unchecked")
E e = (E) o;
// 此处同void linkBefore(E e, Node<E> succ)方法大同小异
// 创建新节点 并关联上一个节点
Node<E> newNode = new Node<>(pred, e, null);
// 如果succ前节点为null,则代表succ则是first头结点,也就是第一个节点 if (pred == null)
if (pred == null)
// first指向newNode作为新的首节点
first = newNode;
else
// 如果不为空,将succ前节点的后节点关联上此新节点
pred.next = newNode;
// 最后将新节点赋值给上一个节点,进行下一次循环
pred = newNode;
}
// succ为index后的一个节点,如果succ为空,说明插入的位置就是在最后面,则直接设置last为pred
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
一句话概括:首先校验index的合法性是否在index >= 0 && index <= size
范围内,否则抛出异常。其次遍历集合依次插入节点。
删除元素
删除元素的方法主要有如下4个:
public E remove(int index)
:根据下标删除指定元素并返回此元素public boolean remove(Object o)
:删除首个出现的指定元素public E removeFirst()
:删除列表中第一个元素并返回此元素public E removeLast()
:删除列表中最后一个元素并返回此元素
第一种public E remove(int index)
:
根据下标删除指定元素并返回此元素
public E remove(int index) {
// 检查index的合法性 index必须在此范围内(index >= 0 && index <= size)
checkElementIndex(index);
// node(index) 拿到当前下标的节点 上文有解读 此处不赘述
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;
// 如果前一个节点是null 表示此节点x是头结点
if (prev == null) {
// 那么将first 指向后一个节点
first = next;
} else {
// 将前一个节点的next 指向此节点x的下一个节点
prev.next = next;
// 将此节点的前一个节点置为null
x.prev = null;
}
// 如果下一个节点是null 表示此节点x是最后一个节点
if (next == null) {
// 那么将last 指向此节点x的上一个节点
last = prev;
} else {
// 将下一个节点的prev 指向此节点x的上一个节点
next.prev = prev;
// 将此节点x的next置为空(为了垃圾回收)
x.next = null;
}
// 将此节点x的元素置为空(为了垃圾回收)
x.item = null;
size--;
modCount++;
return element;
}
第二种public boolean remove(Object o)
:
删除首个出现的指定元素
public boolean remove(Object o) {
// 说明 LinkedList是支持存null元素的
if (o == null) {
// 找到为 null 的元素 并调用unlink解除关联。
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) {
// 找到为 x.item 的元素 并调用unlink解除关联。
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
第三种public E 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.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
第四种public E removeLast()
:
删除列表中最后一个元素并返回此元素
源码如下 非常简单 不多赘述 自行阅读
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
继续向下:
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
查找元素
查找元素的方法主要有如下2个:
public int indexOf(Object o)
:查找首个为指定元素的位置(如不包含,返回-1;如包含,返回此元素下标)public int lastIndexOf(Object o)
:删除首个出现的指定元素(如不包含,返回-1;如包含,返回此元素下标)
两个方法的源码都很简单,两者的差别在于一个是从头遍历,一个是从尾遍历(因为LinkedList底层实现是双向链表,所以很容易实现这点)。
第一种public int indexOf(Object o)
:
查找首个为指定元素的位置(如不包含,返回-1;如包含,返回此元素下标)
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;
}
第二种public int lastIndexOf(Object o)
:
删除首个出现的指定元素(如不包含,返回-1;如包含,返回此元素下标)
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
其他方法此处不再一一解读,如以上源码都理解清楚的同学,其他方法的代码理解起来很简单。
三、Deque双端队列
从上文类图中所能看出,LinkedList
实现了Deque
双端队列的接口。
Deque
接口继承Queue
接口,因此LinkedList
拥有队列以及双端队列的特性。并且我们知道Java中的Stack是一个栈结构,不过栈并没有单独的Stack
接口,但是相关栈的方法都是放在了Deque
中,因此LinkedList
同时也拥有栈的特性。
换句话说:实现了这个Deque
双端队列的类,既可以作为栈使用也可以作为队列使用还可以作为双端队列使用。
双端队列相关方法
/**
* =======================================双端队列==============================
*/
/**
* 往双端队列 前端添加元素
*/
@Override
public void addFirst(E e) {
linkFirst(e);
}
/**
* 双端队列 末尾添加元素
*/
@Override
public void addLast(E e) {
linkLast(e);
}
/**
* 双端队列前端添加元素
*/
@Override
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
/**
* 双端队列 末尾端添加元素
*/
@Override
public boolean offerLast(E e) {
addLast(e);
return true;
}
/**
* 双端队列 前端移除首个元素
*/
@Override
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/**
* 双端队列 末尾端 移除首个元素
*/
@Override
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
/**
* 双端队列 前端 弹出首个元素
*/
@Override
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
/**
* 双端队列 末尾端 弹出首个元素
*/
@Override
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
/**
* 双端队列 获取首端 首个元素
*/
@Override
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
/**
* 双端队列 获取末尾端 首个元素
*/
@Override
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
@Override
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
@Override
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
@Override
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
@Override
public boolean removeLastOccurrence(Object o) {
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
队列相关方法
/**
* ==================================队列方法========================================
*/
@Override
public boolean add(E e) {
// 顺序添加单个元素到链表的末尾
linkLast(e);
return true;
}
@Override
public boolean offer(E e) {
return add(e);
}
@Override
public E remove() {
// 移除第一个元素,Deque接口的方法
return removeFirst();
}
@Override
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
@Override
public E element() {
return getFirst();
}
@Override
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
栈相关方法
/**
* ==================================队列方法========================================
*/
@Override
public boolean add(E e) {
// 顺序添加单个元素到链表的末尾
linkLast(e);
return true;
}
@Override
public boolean offer(E e) {
return add(e);
}
@Override
public E remove() {
// 移除第一个元素,Deque接口的方法
return removeFirst();
}
@Override
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
@Override
public E element() {
return getFirst();
}
@Override
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
以上源码大致浏览一遍即可,都比较简单。
四、遍历方式
第一种遍历方式:for循环
LinkedList<Integer> list = new LinkedList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
for (int i = 0; i < list.size(); ++i) {
System.out.print(list.get(i));
}
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
底层是调用node(index)
,上文对此方法有解读,采用类似于折半查找的方式。
第二种遍历方式:foreach循环在这里插入代码片
for (Integer x : list) {
System.out.print(x + " ");
}
第三种遍历方式: 迭代器遍历
//迭代器遍历--正向输出
ListIterator<Integer> it = list.listIterator();
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
//迭代器遍历--反向输出
ListIterator<Integer> rit = list.listIterator(list.size());
while (rit.hasPrevious()) {
System.out.print(rit.previous() + " ");
}
五、总结
本文主要解读了LinkedList
属性、构造函数、新增元素、删除元素的源码。其他方法的源码大同小异,感兴趣的同学自行阅读。
属性
LinkedList
只有 3 个属性,分别是代表头节点的 first
、代表尾节点的 last
以及表示数量的 size
构造方法
LinkedList有2个构造方法
- 无参构造
public LinkedList()
- 构造包含此集合c所有元素的 LinkedList 集合
public LinkedList(Collection<? extends E> c)
相对于ArrayList
来说,LinkedList
没有类似于制定初始化容量的构造方法,因为底层是基于双向链表,每添加一个元素的时候只需要new
一个节点即可。
添加、删除元素方法
添加、删除元素方法都是一些对于链表操作,且注意LinkedList
是支持存储 null的。