当前位置:

首页 > 系统应用 > LinkedList源码理解

LinkedList源码理解

LinkedList是也是非常常见的集合类,LinkedList是基于链表实现的集合。它拥有List集合的特点:存取有序带索引允许重复元素还拥有Deque集合的特点:先入先出双端操作它本身的特点是:对元素进行插入或者删除,只需要更改一些数据,不需要元素进行移动。依然是通过源码来看看LinkedLis

LinkedList是也是非常常见的集合类,LinkedList是基于链表实现的集合。它拥有List集合的特点:

  • 存取有序
  • 带索引
  • 允许重复元素

还拥有Deque集合的特点:

  • 先入先出
  • 双端操作

它本身的特点是:

  • 对元素进行插入或者删除,只需要更改一些数据,不需要元素进行移动。

依然是通过源码来看看LinkedList如何实现自己的特性的。

Doubly-linked list implementation of the {@code List} and {@code Deque} interfaces. Implements all optional list operations,and permits all elements (including {@code null}).

对于List接口和Deque接口的双链表实现。实现了所有List接口的操作并且能存储所有的元素。

public class LinkedList extends AbstractSequentialList
implements List, Deque, Cloneable, ja va.io.Serializable

可以看到LinkedList实现了一个Deque接口,其实是说,LinkedList除了有List的特性,还有Deque的特性,那么Deque是什么呢?

public interface Deque extends Queue

public interface Queue extends Collection

原来是继承了Collection集合的另一个接口。

Queue就是我们常说的队列,它的特性是FIFO( First In First Out )先进先出,它的操作只有两个:

  • 把元素存进队列尾部
  • 从头部取出元素

就像排队办事一样的。

而它的子接口Deque除了这两操作以外,还能比Queue队列有更多的功能

  • 既可以添加元素到队尾,也可以添加元素到队头
  • 既可以从队尾取元素,也可以从队头取元素

如此看来就像两边都可以当队头和队尾一样,所以Deque又叫双端队列 。

理所应当的,LinkedLisk也实现了这些特性,并且有Doubly-linked(双链表的特性)。

那么什么又是链表呢?

其实链表是一种线性的存储结构,意思是将要存储的数据存在一个存储单元里面,这个存储单元里面除了存放有待存储的数据以外,还存储有其下一个存储单元的地址。

双链表顾名思义,存储单元除了存储其下一个存储单元的地址,还存储了上一个存储单元的地址。每次查找数据的时候,就通过存储单元里存储的地址信息进行查找。

成员变量:

transient int size = 0;

transient Node first;

transient Node last;

只有三个,size代表LinkedList存储的元素个数。那这个Node是什么?

    private static class Node {
E item;
Node next;
Node prev;

Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

它是LinkedList内部的数据结构Node,作为LinkedList的基本存储单元,也最能体现LinkedList双链表的特性。

像这样的。

其中prev存储上一个节点的引用(地址),next存储下一个单元的引用,item就是具体要存的数据。

First和Last用来标明队头跟队尾。

添加数据:

public boolean add(E e) {
linkLast(e);
return true;
}


void linkLast(E e) {
final Node l = last;
final Node newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

默认是调用添加到尾部的方法。前面说过,LinkedList的基本存储单元是Node,所以添加进来的数据会被封装进Node的item属性里,而且这个新Node的prev会指向前一个Node,前一个Node的next会指向这个新Node。

类似这样,但是注意画线只是一种形象的表达方法,就如上面所说,在Node里的prev属性和next属性是用来存储引用的,通过这个引用就能找到前一个Node或者后一个Node。

public void addFirst(E e) {
linkFirst(e);
}

private void linkFirst(E e) {
final Node f = first;
final Node newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

public void addLast(E e) {
linkLast(e);
}

public boolean offerLast(E e) {
addLast(e);
return true;
}

实际上,LinkedList有不少不同名称的方法,但其实现方式大多相似。这是因为LinkedList可用于表示多种不同的数据结构,即便都是在队首或队尾添加元素,清晰的方法描述对提升代码可读性也大有益处。例如,当使用LinkedList表示Stack(栈)数据结构时,可使用push()、pop()、peek()等方法来描述操作;而表示Queue(队列)数据结构时,则使用add()、offer()等方法来描述。(当然,采用多态的方式来实现会更好。)

删除数据:

//删除头Node
public E removeFirst() {
final Node f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}

//删除操作
private E unlinkFirst(Node f) {
// assert f == first && f != null;
final E element = f.item;
final Node 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;
}
//删除尾Node
public E removeLast() {
final Node l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}

//删除操作
private E unlinkLast(Node l) {
// assert l == last && l != null;
//拿到最后一个元素存放的数据
final E element = l.item;
//拿到最后一个元素的prev前元素的引用
final Node prev = l.prev;
//将它们赋值为null
l.item = null;
l.prev = null; // help GC
//现在前元素是list(最后一个Node)
last = prev;
//如果前元素已经是null说明没有Node了
if (prev == null)
first = null;
else
//说明前面还有元素,那么前元素的next就存放null
prev.next = null;
size--;
modCount++;
return element;
}

先看看简单的删除, 这里是指定删除最前跟最后的元素,所以只要判断删除后Node的prev或者next是否还有值,有就说明还有Node,没有就说明LinkedList已经为空了。

怎样才算删除了头/尾Node,只要它的next/prev为空,说明没有引用指向它了,那么我们就认为它从LinkedList里删除了,因为我们无法通过存储单元的引用找到这个Node,所以GC很快也会来回收掉这个Node。

这只是删除头尾Node,那要是删除中间的Node呢?这要跟下面的查找和插入一起看。

查找元素:

public E get(int index) {
checkElementIndex(index);
return node(index).item;
}


Node node(int index) {
// assert isElementIndex(index);

//如果索引小于元素个数的一半,就从前遍历
if (index < (size >> 1)) {
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {//否则从后遍历
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

数组默认是有下标的,可以一次就取出所在位置的元素,但是LinkedList底层可没有维护这么一个数组,那怎么知道第几个元素是什么呢?

笨方法,我有size个元素,我不知道你指定的index在哪,那我一个一个找过去不就完事了?毕竟我的存储单元Node记得它旁边的单元的引用(地址)。

如果你的index比我size的一半还大,那我就从后面找,因为我是双端队列,有Last的引用(地址),所以可以调换两头。

所以,在LinkedList里面找元素可不容易,最多可能要找size/2次才能找到。

只要找到了想要的位置,那么插入和删除指定的这个Node就很简单了。

public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}

E unlink(Node x) {
// assert x != null;
//拿到所要删除的Node的item
final E element = x.item;
//后一个Node
final Node next = x.next;
//前一个Node
final Node prev = x.prev;

//如果前一个Node为null(说明是第一个Node)
if (prev == null) {
//那么后一个Node作为first
first = next;
} else {//否则说明前面有Node
//那前一个Node的下一个Node引用变为后一个Node
prev.next = next;
//当前的前引用变成null
x.prev = null;
}

//如果后一个Node为null(说明是最后一个Node)
if (next == null) {
//那么前一个Node作为last
last = prev;
} else {//否则说明后面还有Node
//那后一个Node的下一个Node引用变为前一个Node
next.prev = prev;
//当前的后引用变为null
x.next = null;
}

//保存的元素也设为null
x.item = null;
//元素-1
size--;
//修改次数+1
modCount++;
return element;
}

public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

void linkBefore(E e, Node succ) {
// assert succ != null;
//要插入位置的前一个Node
final Node pred = succ.prev;
//新Node,前引用是前一个Node,后引用是当前位置的Node
final Node newNode = new Node<>(pred, e, succ);
//后一个Node的前引用变为这个新Node
succ.prev = newNode;
//如果没有前一个Node
if (pred == null)
//说明添加的就是第一个Node了
first = newNode;
else//说明前面还有Node
//将前一个Node的后引用变为这个新的Node
pred.next = newNode;
//元素+1
size++;
modCount++;
}

只是改变了存储单元Node里的prev和next,我们就可以认为这个Node被插入/删除了。

代码的注释配合着下图看,就会方便理解很多,其中注意区分源代码中的命名,最好拿笔记一下容易区分一些。

如果是插入元素,倒着看就可以了。

关于遍历:

我们可以了解到,LinkedList最大的性能消耗就在node(index)这步,这会需要去查找大量的元素。但是只要找到了这个元素所在的Node,插入跟删除就非常的方便了。

所以对于get(index)这个方法,我们需要非常小心的去使用,如果只想看一看这个位置的元素,可以用这个方法,但是如果是遍历LinkedList,千万不可以这样写:

for (int i = 0; i < linkedList.size(); i++) {
linkedList.get(i).equals(Obj);
}

这样对于每次循环,get总会从前或者从后走i次,不考虑get方法中>>1的优化的话,这是一种O(n^2)时间复杂度的做法,效率十分低下。

所以LinkedList提供了内部的Iterator迭代器供我们使用:

private class ListItr implements ListIterator {
private Node lastReturned;
private Node next;
private int nextIndex;
private int expectedModCount = modCount;

ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}

public boolean hasNext() {
return nextIndex < size;
}

public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();

lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}

其实就是通过不断调用next()方法取得Node,然后再对Node做操作,这样时间复杂度就是O(n)了,不会有大量重复无用的遍历。

总结:其实LinkedList的特点插入、删除快,只是针对这次的操作而言的。

LinkedList做插入、删除的时候,慢在要找到具体的位置,快在只需要改变前后Node的引用地址

ArrayList做插入、删除的时候,慢在数组元素的批量赋值(前文里的System.arraycopy),快在搜索

当待插入或删除的元素位于数据结构的前半段,尤其是非常靠前的位置时,LinkedList的效率会远超ArrayList。这是因为ArrayList需要批量复制大量元素。不过,越往后,对于LinkedList而言,由于它是双向链表,在第2个元素后面插入一个数据与在倒数第2个元素后面插入一个元素,效率上几乎没有差别。而ArrayList由于要批量复制的元素越来越少,操作速度必然会逐渐追上甚至超过LinkedList。

不论怎么说,需要根据具体情况来选择对应的集合,最好做一下性能测试,这样才能有更高的效率。

本文内容来源于网友投稿,如有侵权请联系删除。
作者最新文章
系统应用
相关文章 更多
除了界面,Windows11和Win10还有哪些实质差异
除了界面,Windows11和Win10还有哪些实质差异

除了开始菜单的变化,Windows 11在TPM 2.0安全要求、窗口贴靠布局、驱动兼容性以及系统更新策略上与Windows 10有显著不同。本文详解两者实质差异,助你判断是否值得升级。

win10专业版和家庭版关闭更新方法差别在哪
win10专业版和家庭版关闭更新方法差别在哪

详解Windows 10家庭版和专业版在关闭或暂停自动更新时的操作区别。涵盖通用的暂停更新、活动时间设置,以及专业版独有的组策略管理入口,帮助不同版本用户合理控制更新节奏,避免系统安全风险。

win10暂停更新最长可以设置多少天怎么操作
win10暂停更新最长可以设置多少天怎么操作

想知道Win10暂停更新最长能设多久?官方支持最长暂停35天。本文图文演示如何在设置中开启暂停、确认生效日期,以及到期后如何恢复更新或调整活动时间以避免打扰。

win10怎么屏蔽win10系统更新的弹窗提醒
win10怎么屏蔽win10系统更新的弹窗提醒

本教程介绍如何在Windows 10中通过暂停更新、设置活动时间及安排重启时间来减少更新弹窗提醒。包含通知隐藏技巧及更新失败排查步骤,帮助你在保持系统安全的同时减少工作打扰。

win10更新后台占用CPU过高怎么关闭自动更新
win10更新后台占用CPU过高怎么关闭自动更新

Windows 10 更新时 CPU 占用过高怎么办?本教程演示如何通过任务管理器确认更新进程,使用“暂停更新”功能临时停止后台活动,并设置“活动时间”防止自动重启干扰工作。提供安全的故障排查步骤,避免直接禁用系统服务带来的风险。

win10正在玩游戏弹出更新重启怎么禁止
win10正在玩游戏弹出更新重启怎么禁止

Win10玩游戏时突然弹出更新重启提示?不要强制关机。本文教你如何通过设置“活动时间”避免自动重启,利用“安排重启”规划空闲时间,以及合理使用“暂停更新”功能。区分不同状态下的应对策略,既保护游戏进度又维持系统安全。

win10自动更新抢占网络带宽该怎么处理
win10自动更新抢占网络带宽该怎么处理

Win10自动更新抢占带宽导致游戏卡顿或网页打不开?本教程教你通过任务管理器确认更新进程,利用暂停更新、按流量计费连接和传递优化带宽限制,精准控制Windows Update下载速度,解决网络拥堵问题。

win10家庭版有没有简单办法阻止强制更新
win10家庭版有没有简单办法阻止强制更新

Win10家庭版用户常受强制更新困扰。本文详解如何利用系统自带的暂停更新、活动时间和安排重启功能,在不破坏系统稳定性的前提下减少更新打扰,并分析注册表修改的风险及系统支持现状。

win10升级新版本后取消开机密码失效怎么修复
win10升级新版本后取消开机密码失效怎么修复

Windows 10更新后开机突然要求输入密码?本文解析自动登录失效的真实原因,提供通过netplwiz重新保存凭据、关闭Windows Hello干扰及排查账户策略的完整步骤,助你恢复免密进入桌面。

win10更新没下载完反复重试该怎么停止任务
win10更新没下载完反复重试该怎么停止任务

Windows 10更新下载失败反复重试怎么办?不要直接停用服务。本文教你通过设置页面暂停更新、启用按流量计费连接以及调整活动时间,安全地暂时停止下载任务并避免意外重启。

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
photoshop
photoshop
Windows、macOS 、 iPad

Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。

Blender
Blender
Windows、macOS 和 Linux

Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。

灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

WINDOWS 更多
3dmax(3ds max)
3dmax(3ds max)
Windows

Autodesk 3ds Max 是一款专业的三维建模、动画与渲染软件,广泛应用于建筑可视化、游戏开发、影视动画、广告设计和产品展示等领域。

photoshop
photoshop
Windows、macOS 、 iPad

Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。

Blender
Blender
Windows、macOS 和 Linux

Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。