Java小白集合源码的学习系列:LinkedList
LinkedList 源码学习
前文传送门:Java小白集合源码的学习系列:ArrayList
本篇为集合源码学习系列的LinkedList
学习部分,如有叙述不当之处,还望评论区批评指正!
LinkedList继承体系
LinkedList和ArrayList一样,都实现了List接口,都代表着列表结构,都有着类似的add,remove,clear等操作。与ArrayList不同的是,LinkedList底层基于双向链表,允许不连续地址的存储,通过节点之间的相互引用建立联系,通过节点存储数据。
LinkedList核心源码
既然是基于节点的,那么我们来看看节点在LinkedList中是怎样的存在:
//Node作为LinkedList的静态内部类
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;
}
}
我们发现,Node作为其内部类,拥有三个属性,一个是用来指向前一节点的指针prev,一个是指向后一节点的指针next,还有存储的元素值item。
我们来看看LinkedList的几个基本属性:
/*用transient关键字标记的成员变量不参与序列化过程*/
transient int size = 0;//记录节点个数
/**
* first是指向第一个节点的指针。永远只有下面两种情况:
* 1、链表为空,此时first和last同时为空。
* 2、链表不为空,此时第一个节点不为空,第一个节点的prev指针指向空
*/
transient Node<E> first;
/**
* last是指向最后一个节点的指针,同样地,也只有两种情况:
* 1、链表为空,first和last同时为空
* 2、链表不为空,此时最后一个节点不为空,其next指向空
*/
transient Node<E> last;
//需要注意的是,当first和last指向同一节点时,表明链表中只有一个节点。
了解基本属性之后,我们看看它的构造方法,由于不必在乎它存储的位置,它的构造器也是相当简单的:
//创建一个空链表
public LinkedList() {
}
//创建一个链表,包含指定传入的所有元素,这些元素按照迭代顺序排列
public LinkedList(Collection<? extends E> c) {
this();
//添加操作
addAll(c);
}
其中addAll(c)其实调用了addAll(size,c),由于这里size=0,所以相当于从头开始一一添加。至于addAll方法,我们暂时不提,当我们总结完普通的添加操作,也就自然明了这个全部添加的操作。
//把e作为链表的第一个元素
private void linkFirst(E e) {
//建立临时节点指向first
final Node<E> f = first;
//创建存储e的新节点,prev指向null,next指向临时节点
final Node<E> newNode = new Node<>(null, e, f);
//这时newNode变成了第一个节点,将first指向它
first = newNode;
//对原来的first,也就是现在的临时节点f进行判断
if (f == null)
//原来的first为null,说明原来没有节点,现在的newNode
//是唯一的节点,所以让last也只想newNode
last = newNode;
else
//原来链表不为空,让原来头节点的prev指向newNode
f.prev = newNode;
//节点数量加一
size++;
//对列表进行改动,modCount计数加一
modCount++;
}
相应的,把元素作为链表的最后一个元素添加和第一个元素添加方法类似,就不赘述了。我们来看看我们一开始遇到的addAll操作,感觉有一点点麻烦的哦:
//在指定位置把另一个集合中的所有元素按照迭代顺序添加进来,如果发生改变,返回true
public boolean addAll(int index, Collection<? extends E> c) {
//范围判断
checkPositionIndex(index);
//将集合转换为数组,果传入集合为null,会出现空指针异常
Object[] a = c.toArray();
//传入集合元素个数为0,没有改变原集合,返回false
int numNew = a.length;
if (numNew == 0)
return false;
//创建两个临时节点,暂时表示新表的头和尾
Node<E> pred, succ;
//相当于从原集合的尾部添加
if (index == size) {
//暂时让succ置空
succ = null;
//让pred指向原集合的最后一个节点
pred = last;
} else {
//如果从中间插入,则让succ指向指定索引位置上的节点
succ = node(index);
//让succ的prev指向pred
pred = succ.prev;
}
//增强for循环遍历赋值
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//创建存储值尾e的新节点,前向指针指向pred,后向指针指向null
Node<E> newNode = new Node<>(pred, e, null);
//表明原链表为空,此时让first指向新节点
if (pred == null);
first = newNode;
else
//原链表不为空,就让临时节点pred节点向后移动
pred.next = newNode;
//更新新表的头节点为当前新创建的节点
pred = newNode;
}
//这种情况出现在原链表后面插入
if (succ == null) {
//此时pred就是最终链表的last
last = pred;
} else {
//在index处插入的情况
//由于succ是node(index)的临时节点,pred因为遍历也到了插入链表的最后一个节点
//让最后位置的pred和succ建立联系
pred.next = succ;
succ.prev = pred;
}
//新长度为原长+增长
size += numNew;
modCount++;
return true;
}
- 注意:遍历赋值的过程相当于从pred这个临时节点开始,依次向后创建新节点,并将pred向后移动,直到新传入集合的最后一个元素,这时再将pred和succ两个建立联系,实现无缝链接。
再来看看,在链表中普通删除元素的操作是怎么样的:
//取消一个非空节点x的连结,并返回它
E unlink(Node<E> x) {
//同样的,在调用这个方法之前,需要确保x不为空
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//明确x与上一节点的联系,更新并删除无用联系
//x为头节点
if (prev == null) {
//让first指向x.next的临时节点next,宣布从下一节点开始才是头
first = next;
} else {
//x不是头节点的情况
//让x.prev的临时节点prev的next指向x.next的临时节点
prev.next = next;
//删除x的前向引用,即让x.prev置空
x.prev = null;
}
//明确x与下一节点的联系,更新并删除无用联系
//x为尾节点
if (next == null) {
//让last指向x.prev的临时节点prev,宣布上一节点是最后的尾
last = prev;
} else {
//x不是尾节点的情况
//让x.next的临时节点next的prev指向x.prev的临时节点
next.prev = prev;
//删除x的后向引用,让x.next置空
x.next = null;
}
//让x存储元素置空,等待GC宠信
x.item = null;
size--;
modCount++;
return element;
}
总结来说,删除操作无非就是,消除该节点与另外两个节点的联系,并让与它相邻的两个节点之间建立联系。如果考虑边界条件的话,比如为头节点和尾节点的情况,需要再另加分析。总之,它不需要向ArrayList一样,拷贝数组,而是改变节点间的地址引用。但是,删除之前需要找到这个节点,我们还是需要遍历滴,就像下面这样:
//移除第一次出现的元素o,找到并移除返回true,否则false
public boolean remove(Object o) {
//传入元素本身就为null
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) {
//删除的元素不为null,比较值的大小
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
总结一下从前向后遍历的过程:
- 创建一个临时节点指向first。
- 向后遍历,让临时节点指向它的下一位。
- 直到临时节点指向last的下一位(即x==null)为止。
当然特殊情况特殊考虑,上面的remove方法目的是找到对应的元素,只需要在循环中加入相应的逻辑判断即可。下面这个相当重要的辅助方法就是通过遍历获取指定位置上的节点:有了这个方法,我们就可以同过它的前后位置,推导出其他不同的方法:
//获得指定位置上的非空节点
Node<E> node(int index) {
//在调用这个方法之前会确保0<=inedx<size
//index和size>>1比较,如果index比size的一半小,从前向后遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
//退出循环的条件,i==indx,此时x为当前节点
return x;
} else {
//从后向前遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
与此同时还有indexOf和lastIndexOf方法也是通过上面总结的遍历过程,加上计数条件,计算出指定元素第一次或者最后一次出现的索引,这里以indexOf为例:
//返回元素第一次出现的位置,没找到就返回-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;
}
其实就是我们上面讲的遍历操作嘛,大差不差。有了这个方法,我们还是可以很轻松地推导出另外的contains方法。
public boolean contains(Object o) {
return indexOf(o) != -1;
}
然后还是那对基佬方法:get和set。
//获取元素值
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
//用新值替换旧值,返回旧值
public E set(int index, E element) {
checkElementIndex(index);
//获取节点
Node<E> x = node(index);
//存取旧值
E oldVal = x.item;
//替换旧值
x.item = element;
//返回旧值
return oldVal;
}
接下来是我们的clear方法,移除所有的元素,将表置空。虽然写法有所不同,但是基本思想是不变的:创建节点,并移动,删除不要的,或者找到需要的,就行了。
public void clear() {
for (Node<E> x = first; x != null; ) {
//创建临时节点指向当前节点的下一位
Node<E> next = x.next;
//下面就可以安心地把当前节点有关的全部清除
x.item = null;
x.next = null;
x.prev = null;
//x向后移动
x = next;
}
//回到最初的起点
first = last = null;
size = 0;
modCount++;
}
Deque相关操作
我们还知道,LinkedList还继承了Deque接口,让我们能够操作队列一样操作它,下面是截取不完全的一些方法:
我们从中挑选几个分析一下,几个具有迷惑性方法的差异,比如下面这四个:
public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
//如果头节点为空,抛出异常
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
- element:调用getFirst方法,如果头节点为空,抛出异常。
- getFirst:如果头节点为空,抛出异常。
- peek:头节点为空,返回null。
- peekFirst:头节点为空,返回null。
与之类似的还有:
- pollFirst和pollLast方法删除头和尾节点,如果为空,返回null。
- removeFirst和removeFirst如果为空,抛异常。
如果有兴趣的话,可以研究一下,总之还是相对简单的。
总结
而LinkedList底层基于双向链表实现,不需要连续的内存存储,通过节点之间相互引用地址形成联系。
对于无索引位置的插入来说,例如向后插入,时间复杂度近似为O(1),体现出增删操作较快。但是如果要在指定的位置上插入,还是需要移动到当前指定索引位置,才可以进行操作,时间复杂度近似为O(n)。
Linkedlist不支持快速随机访问,查询较慢。
线程不安全,同样的,关于线程方面,以后学习时再进行总结。
Java小白集合源码的学习系列:LinkedList的更多相关文章
- Java小白集合源码的学习系列:Vector
目录 Vector源码学习 Vector继承体系 Vector核心源码 基本属性 构造器 扩容机制 Enumeration 概述 源码描述 具体操作 Vector总结 Vector源码学习 前文传送门 ...
- Java小白集合源码的学习系列:ArrayList
ArrayList源码学习 本文基于JDK1.8版本,对集合中的巨头ArrayList做一定的源码学习,将会参考大量资料,在文章后面都将会给出参考文章链接,本文用以巩固学习知识. ArrayList的 ...
- 【JDK1.8】 Java小白的源码学习系列:HashMap
目录 Java小白的源码学习系列:HashMap 官方文档解读 基本数据结构 基本源码解读 基本成员变量 构造器 巧妙的tableSizeFor put方法 巧妙的hash方法 JDK1.8的putV ...
- JAVA常用集合源码解析系列-ArrayList源码解析(基于JDK8)
文章系作者原创,如有转载请注明出处,如有雷同,那就雷同吧~(who care!) 一.写在前面 这是源码分析计划的第一篇,博主准备把一些常用的集合源码过一遍,比如:ArrayList.HashMap及 ...
- java集合 源码解析 学习手册
学习路线: http://www.cnblogs.com/skywang12345/ 总结 1 总体框架 2 Collection架构 3 ArrayList详细介绍(源码解析)和使用示例 4 fai ...
- Java基础——集合源码解析 List List 接口
今天我们来学习集合的第一大体系 List. List 是一个接口,定义了一组元素是有序的.可重复的集合. List 继承自 Collection,较之 Collection,List 还添加了以下操作 ...
- Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析
目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...
- Java并发包源码学习系列:阻塞队列实现之ArrayBlockingQueue源码解析
目录 ArrayBlockingQueue概述 类图结构及重要字段 构造器 出队和入队操作 入队enqueue 出队dequeue 阻塞式操作 E take() 阻塞式获取 void put(E e) ...
- Java并发包源码学习系列:阻塞队列实现之LinkedBlockingQueue源码解析
目录 LinkedBlockingQueue概述 类图结构及重要字段 构造器 出队和入队操作 入队enqueue 出队dequeue 阻塞式操作 E take() 阻塞式获取 void put(E e ...
随机推荐
- Innodb_large_prefix
innodb_large_prefix Prefixes, defined by the length attribute, can be up to 767 bytes long for InnoD ...
- 模板——伸展树 splay 实现快速分裂合并的序列
伸展操作:将treap中特定的结点旋转到根 //将序列中从左数第k个元素伸展到根,注意结点键值保存的是原序列id void splay(Node* &o, int k) { ] == NULL ...
- AWS Credentials 使用
AWS的文档系统真是烂到家了!!!!! To connect to any of the supported services with the AWS SDK for Java, you must ...
- H3C RIP基本配置举例
- WPF 使用 Composition API 做高性能渲染
在 WPF 中很多小伙伴都会遇到渲染性能的问题,虽然 WPF 的渲染可以甩浏览器渲染几条街,但是还是支持不了游戏级的渲染.在 WPF 使用的 DX 只是优化等级为 9 和 DX 9 差不多的性能,微软 ...
- H3C FTP其他常用命令
- activiti工作流引擎学习(三)
5.接收任务活动(receiveTask,即等待活动)不是一个任务节点 接收任务是一个简单任务,他会等待回应消息的到达,当前,官方只实现了这个任务的java语义,当流程达到接受任务,流程状态会保存到数 ...
- dotnet 使用 System.CommandLine 写命令行程序
在写命令行程序的时候,会遇到命令行解析的问题,以及参数的使用和规范化等坑.现在社区开源了命令行项目,可以帮助小伙伴快速开发命令行程序,支持自动的命令行解析和规范的参数 我写过一篇关于命令行解析的博客C ...
- onload事件属性,JQ中的load,ready方法
onload事件属性,JQ中的load,ready方法 前言 页面中的很多操作,需要我们在所需资源下载完成后,才可以进行操作,而资源没有及时下载,我们进行操作的话,是会报错.因此我们需要熟练掌握哪些事 ...
- UE4 C++ 代码编译方式
Unreal 有一个非常酷的特性 —> 不必关闭编辑器就可以编译 C++ 更改! 有两种方法可以达到这个目的: 1.直接点击编辑器主工具栏中的 编译(Compile) 按钮. 2.在编辑器继续运 ...