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;
}
}

与此同时还有indexOflastIndexOf方法也是通过上面总结的遍历过程,加上计数条件,计算出指定元素第一次或者最后一次出现的索引,这里以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;
}

然后还是那对基佬方法:getset

    //获取元素值
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的更多相关文章

  1. Java小白集合源码的学习系列:Vector

    目录 Vector源码学习 Vector继承体系 Vector核心源码 基本属性 构造器 扩容机制 Enumeration 概述 源码描述 具体操作 Vector总结 Vector源码学习 前文传送门 ...

  2. Java小白集合源码的学习系列:ArrayList

    ArrayList源码学习 本文基于JDK1.8版本,对集合中的巨头ArrayList做一定的源码学习,将会参考大量资料,在文章后面都将会给出参考文章链接,本文用以巩固学习知识. ArrayList的 ...

  3. 【JDK1.8】 Java小白的源码学习系列:HashMap

    目录 Java小白的源码学习系列:HashMap 官方文档解读 基本数据结构 基本源码解读 基本成员变量 构造器 巧妙的tableSizeFor put方法 巧妙的hash方法 JDK1.8的putV ...

  4. JAVA常用集合源码解析系列-ArrayList源码解析(基于JDK8)

    文章系作者原创,如有转载请注明出处,如有雷同,那就雷同吧~(who care!) 一.写在前面 这是源码分析计划的第一篇,博主准备把一些常用的集合源码过一遍,比如:ArrayList.HashMap及 ...

  5. java集合 源码解析 学习手册

    学习路线: http://www.cnblogs.com/skywang12345/ 总结 1 总体框架 2 Collection架构 3 ArrayList详细介绍(源码解析)和使用示例 4 fai ...

  6. Java基础——集合源码解析 List List 接口

    今天我们来学习集合的第一大体系 List. List 是一个接口,定义了一组元素是有序的.可重复的集合. List 继承自 Collection,较之 Collection,List 还添加了以下操作 ...

  7. Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析

    目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...

  8. Java并发包源码学习系列:阻塞队列实现之ArrayBlockingQueue源码解析

    目录 ArrayBlockingQueue概述 类图结构及重要字段 构造器 出队和入队操作 入队enqueue 出队dequeue 阻塞式操作 E take() 阻塞式获取 void put(E e) ...

  9. Java并发包源码学习系列:阻塞队列实现之LinkedBlockingQueue源码解析

    目录 LinkedBlockingQueue概述 类图结构及重要字段 构造器 出队和入队操作 入队enqueue 出队dequeue 阻塞式操作 E take() 阻塞式获取 void put(E e ...

随机推荐

  1. Innodb_large_prefix

    innodb_large_prefix Prefixes, defined by the length attribute, can be up to 767 bytes long for InnoD ...

  2. 模板——伸展树 splay 实现快速分裂合并的序列

    伸展操作:将treap中特定的结点旋转到根 //将序列中从左数第k个元素伸展到根,注意结点键值保存的是原序列id void splay(Node* &o, int k) { ] == NULL ...

  3. AWS Credentials 使用

    AWS的文档系统真是烂到家了!!!!! To connect to any of the supported services with the AWS SDK for Java, you must ...

  4. H3C RIP基本配置举例

  5. WPF 使用 Composition API 做高性能渲染

    在 WPF 中很多小伙伴都会遇到渲染性能的问题,虽然 WPF 的渲染可以甩浏览器渲染几条街,但是还是支持不了游戏级的渲染.在 WPF 使用的 DX 只是优化等级为 9 和 DX 9 差不多的性能,微软 ...

  6. H3C FTP其他常用命令

  7. activiti工作流引擎学习(三)

    5.接收任务活动(receiveTask,即等待活动)不是一个任务节点 接收任务是一个简单任务,他会等待回应消息的到达,当前,官方只实现了这个任务的java语义,当流程达到接受任务,流程状态会保存到数 ...

  8. dotnet 使用 System.CommandLine 写命令行程序

    在写命令行程序的时候,会遇到命令行解析的问题,以及参数的使用和规范化等坑.现在社区开源了命令行项目,可以帮助小伙伴快速开发命令行程序,支持自动的命令行解析和规范的参数 我写过一篇关于命令行解析的博客C ...

  9. onload事件属性,JQ中的load,ready方法

    onload事件属性,JQ中的load,ready方法 前言 页面中的很多操作,需要我们在所需资源下载完成后,才可以进行操作,而资源没有及时下载,我们进行操作的话,是会报错.因此我们需要熟练掌握哪些事 ...

  10. UE4 C++ 代码编译方式

    Unreal 有一个非常酷的特性 —> 不必关闭编辑器就可以编译 C++ 更改! 有两种方法可以达到这个目的: 1.直接点击编辑器主工具栏中的 编译(Compile) 按钮. 2.在编辑器继续运 ...