LinkedList源码阅分析

LinkedList里面涉及到的一些操作,非常细致,以避免出现的空指针,理解后对于其优点与确定会有一个更加整体的认识吧。

继承关系图(对比ArrayList)

元素的存储结构
在LinkedList中,每一个元素都是Node存储,Node拥有一个存储值的item与一个前驱prev和一个后继next,如下:

// 典型的链表结构

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



构造函数与成员变量
变量主要有3个:

transient int size = 0;//当前列表的元素个数
/*** Pointer to first node.* Invariant: (first == null && last == null) ||*            (first.prev == null && first.item != null)*/
transient Node<E> first;// 第一个元素
/*** Pointer to last node.* Invariant: (first == null && last == null) ||*            (last.next == null && last.item != null)*/
transient Node<E> last;// 最后一个元素



在LinkedList中的构造函数有两个,一个是无参的,另一个是带Collection参数的。

public LinkedList() {}//无参构造函数
public LinkedList(Collection<? extends E> c) {this();addAll(c);//将c中的元素都添加到此列表中
}



其添加的过程中,此时size = 0,如下:

public boolean addAll(Collection<? extends E> c) {return addAll(size, c);//此时 size == 0
}



如果index==size,则添加c中的元素到列表的尾部;否则,添加的第index个元素的前面;

public boolean addAll(int index, Collection<? extends E> c) {// 检查位置是否合法 位置是[0,size],注意是闭区间 否则报异常checkPositionIndex(index);Object[] a = c.toArray();// 得到一个元素数组int numNew = a.length;// c中元素的数量if (numNew == 0)return false;// 没有元素,添加失败// 主要功能是找到第size个元素的前驱和后继。得到此元素需要分情况讨论。// 这段代码是各种情况的总和,可能有一点点容易懵逼。Node<E> pred, succ;// 前驱与后继if (index == size) {// 如果位置与当前的size相同succ = null;// 无后继pred = last;// 前驱为last,即第size个元素(最后一个元素)} else {// 若与size不同,即index位于[0, size)之间succ = node(index);// 后继为第index个元素pred = succ.prev;// 前驱为后继的前驱}// 后文有详细的图片说明// 开始逐个插入for (Object o : a) {@SuppressWarnings("unchecked") E e = (E) o;// 新建一个以pred为前驱、null为后继、值为e的节点Node<E> newNode = new Node<>(pred, e, null);if (pred == null)// 前驱为空,则此节点被当做列表的第一个节点first = newNode;else// 规避掉了NullPointerException,感觉又达到了目的,又实现了逻辑pred.next = newNode;// 不为空,则将前驱的后继改成当前节点pred = newNode;// 将前驱改成当前节点,以便后续添加c中其它的元素}// 至此,c中元素已添加到链表上,但链表中从size开始的那些元素还没有链接到列表上// 此时就需要利用到之前找出来的succ值,它是作为这个c的整体后继if (succ == null) {// 如果后继为空,说明无整体后继last = pred;// c的最后一个元素应当作为列表的尾元素} else {// 有整体后继pred.next = succ;// pred即c中的最后一个元素,其后继指向succ,即整体后继succ.prev = pred;// succ的前驱指向c中的最后一个元素}// 添加完毕,修改参数size += numNew;modCount++;return true;
}



返回序号为index的元素节点。看这段代码中的if语句,真的是佩服,这样写代码,都可以这样减少查找次数。

Node<E> node(int index) {// assert isElementIndex(index);// 这个地方很有意思。视其与中值得差距,觉得从前遍历还是从后遍历。if (index < (size >> 1)) {Node<E> x = first;// 循环index次 迭代到所需要的元素for (int i = 0; i < index; i++)x = x.next;return x;} else {Node<E> x = last;// 循环size-1-index次for (int i = size - 1; i > index; i--)x = x.prev;return x;}
}



测试代码以及验证输出如下:

public class Main {public static void main(String[] args) {List<String> list = new LinkedList<>(Arrays.asList("1", "2", "3"));System.out.println(list.toString());list.addAll(2, Arrays.asList("4", "5"));System.out.println(list.toString());list.addAll(0, Arrays.asList("6", "7"));System.out.println(list.toString());}
}
---
[1, 2, 3]
[1, 2, 4, 5, 3]
[6, 7, 1, 2, 4, 5, 3]



增加元素
对于向列表中添加元素,先看一组基本的添加操作,具体如下:

将e链接成列表的第一个元素
源代码以及相应的分析如下:

private void linkFirst(E e) {final Node<E> f = first;// 前驱为空,值为e,后继为ffinal Node<E> newNode = new Node<>(null, e, f);first = newNode;// first指向newNode// 此时的f有可能为nullif (f == null)// 若f为空,则表明列表中还没有元素last = newNode;// last也应该指向newNodeelsef.prev = newNode;// 否则,前first的前驱指向newNodesize++;modCount++;
}



其过程大致如下两图所示:
初始状态:

后续状态:
添加元素作为第一个元素时,所需要做的工作,有下列所述:
首先,获取第一个节点,然后将该节点的前驱指向新添加的元素所在的节点;
接着,将新添加的节点的后继指向前第一个节点;
最后,将first指向新添加的元素的节点。添加完毕。


将e链接为最后一个元素
源代码以及相应的解释如下:

void linkLast(E e) {final Node<E> l = last;// 找到最后一个节点// 前驱为前last,值为e,后继为nullfinal Node<E> newNode = new Node<>(l, e, null);last = newNode;// last一定会指向此节点if (l == null)// 最后一个节点为空,说明列表中无元素first = newNode;// first同样指向此节点elsel.next = newNode;// 否则,前last的后继指向当前节点size++;modCount++;
}



其操作过程与前述linkFirst()的过程类似,因此其替换后的示意图如下:


将e链接到节点succ前
源代码以及相应的解析如下:

void linkBefore(E e, Node<E> succ) {// assert succ != null;final Node<E> pred = succ.prev; // 找到succ的前驱// 前驱为pred,值为e,后继为succfinal Node<E> newNode = new Node<>(pred, e, succ);// 将succ的前驱指向当前节点succ.prev = newNode;if (pred == null)// pred为空,说明此时succ为首节点first = newNode;// 指向当前节点elsepred.next = newNode;// 否则,将succ之前的前驱的后继指向当前节点size++;modCount++;
}



这个操作有点类似将上述的两个操作整合到一起。其操作简图如下:


有了上述的分析,我们再来看一些添加的操作,这些操作基本上是做了一些逻辑判断,然后再调用上述三个方法去实现添加功能,这里略过就好。

 public boolean add(E e) {linkLast(e);return true;}// 只有这个是有一点逻辑的public void add(int index, E element) {checkPositionIndex(index);if (index == size)// 为最后一个节点,当然是添加到最后一个~linkLast(element);elselinkBefore(element, node(index));}public void addFirst(E e) {linkFirst(e);}public void addLast(E e) {linkLast(e);}


删除元素
删除就是添加过程的逆过程。同样,在分析我们使用的接口前,先分析几个我们看不到的方法,如下:

删除首节点
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 GCfirst = next;// first指向前first的后继,也就是列表中的2号位if (next == null)// 如果此时2号位为空,那么列表中此时已无节点last = null;// last指向nullelsenext.prev = null;// 首节点无前驱size--;modCount++;return element;// 返回首节点保存的元素值
}



删除尾节点
此处的操作与删除首节点的操作类似。

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;// last指向前last的前驱,也就是列表中的倒数2号位
if (prev == null)// 如果此时倒数2号位为空,那么列表中已无节点first = null;// first指向null
elseprev.next = null;// 尾节点无后继
size--;
modCount++;
return element;// 返回尾节点保存的元素值
}



删除某个非空节点
这个也类似添加元素时的第三个基本操作,与结合了上述两个操作有点类似。

// x即为要删除的节点
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;// 保存x的元素值
final Node<E> next = x.next;// 保存x的后继
final Node<E> prev = x.prev;// 保存x的前驱if (prev == null) {// 前驱为null,说明x为首节点first = next;// first指向x的后继
} else {prev.next = next;// x的前驱的后继指向x的后继,即略过了xx.prev = null;// x.prev已无用处,置空引用
}if (next == null) {// 后继为null,说明x为尾节点last = prev;// last指向x的前驱
} else {next.prev = prev;// x的后继的前驱指向x的前驱,即略过了xx.next = null;// x.next已无用处,置空引用
}x.item = null;// 引用置空
size--;
modCount++;
return element;// 返回所删除的节点的元素值
}


有了上面的几个函数作为支撑,我们再来看下面的几个我们能用来删除节点的方法,他们也基本上是在一些逻辑判断的基础之上,再调用上述的基本操作:

public E removeFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return unlinkFirst(f);
}
public E removeLast() {final Node<E> l = last;if (l == null)throw new NoSuchElementException();return unlinkLast(l);
}
// 遍历列表中所有的节点,找到相同的元素,然后删除它
public boolean remove(Object o) {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) {if (o.equals(x.item)) {unlink(x);return true;}}}return false;
}
public E remove(int index) {checkElementIndex(index);return unlink(node(index));
}



修改元素
通过遍历,循环index次,获取到相应的节点后,再通过节点来修改元素值。

public E set(int index, E element) {checkElementIndex(index);Node<E> x = node(index);// 获取到需要修改元素的节点E oldVal = x.item;// 保存之前的值x.item = element;// 修改return oldVal;// 返回修改前的值}


 

查询元素
通过位置,循环index次,获取到节点,然后返回该节点中元素的值public E get(int index) {checkElementIndex(index);return node(index).item;// 获取节点,并返回节点中的元素值
}


 

还有两个获取首尾节点的元素的方法:public E getFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return f.item;
}
public E getLast() {final Node<E> l = last;if (l == null)throw new NoSuchElementException();return l.item;
}


 

获取元素位置
从0开始往后遍历public int indexOf(Object o) {int index = 0;if (o == null) {// 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;// 未找到,返回-1
}


 

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



额外的话
在上面的诸多函数中,有许多是需要进行位置判断的。在源码中,位置判断有两个函数,一个是下标,一个是位置。看到这两个函数,确实是有一些感触,这确实是需要比较强的总结能力以及仔细的观察能力。

// 下标,保证数组访问不越界。
private boolean isElementIndex(int index) {return index >= 0 && index < size;
}
// 位置
private boolean isPositionIndex(int index) {return index >= 0 && index <= size;
}



后记
LinkedList还实现了Queue这个接口,在实现这些接口时,仍然是做一些逻辑处理,然后调用上面所描述的基本操作,如link()、unlink()之类的,因此不再分析。还有其中的关于序列化、Iterator这两块,与ArrayList的实现也是不尽相同的,故在此可参考ArrayList中的解析。
 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/576436.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

取消选中目标CALL

事先在游戏里选中一个怪物bp send,回到游戏里,按ESC&#xff0c;OD断下来,复制 返回到 elementc.072AFDD8 005869B2 返回到 elementc.005869B2 来自 elementc.0058E8A0072AFDEC 00588B1F 返回到 elementc.00588B1F 来自 elementc.00586980072AFE28 005A7346 返回到 el…

《深入理解java虚拟机》第1章 走近Java

1.6实战:自己编译JDK 想要一探JDK内部的实现机制&#xff0c;最便捷的路径之一就是自己编译- -套JDK,通过阅读和跟踪调试JDK源码去了解Java技术体系的原理&#xff0c;虽然门槛会高一点&#xff0c;但肯定会比阅读各种书籍、文章更加贴近本质。另外&#xff0c;JDK中的很多底层…

《深入理解java虚拟机》第2章 Java内存区域与内存溢出异常

Java与C之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”&#xff0c;墙外面的人想进去&#xff0c;墙里面的人却想出来。 2.1 概述 https://blog.csdn.net/q5706503/article/details/84640762 对于从事C、C程序开发的开发人员来说&#xff0c;在内存管理领域&#…

线性表的定义和基本运算之线性结构

一、线性表的逻辑定义和性质 线性表是最简单和最常用的一种数据结构&#xff0c;他是由n个数据元素&#xff08;结点&#xff09;a1,a2,a3,a4........an组成的有限序列。其中&#xff0c;数据元素个数那位表的长度。当n为0时称为空表&#xff0c;非空的线性表通常记为 &#x…

数据结构之指针复习

废话不多说&#xff0c;拿起键盘就是干&#xff0c;直接上代码&#xff1a; #include <stdio.h>int main() {double *p;double x 66.6;p &x; //x占8个字节&#xff0c;一个字节占8位&#xff0c;一个字节一个地址double arr[3] { 1.1,2.2,3.3 };double *q;q &a…

数据结构之结构体复习

为什么出现结构体&#xff1f; 为了表示一些复杂的数据&#xff0c;一些基本数据类型无法满足要求&#xff0c; 当要用一个变量描述一个对象的多个属性时&#xff0c;普通的内置数据类型是表示不了的&#xff0c;这个时候就可以用结构体回。结构体和类很相似&#xff0c;唯一不…

高效管理读书笔记

高效管理读书笔记一、优秀的权威宣言二、主要的内容要点2.1 有权威的领导都会关心自己的员工2.2 问责而不指责2.3 多点尤达&#xff0c;少点超人三、原书一、优秀的权威宣言 优秀的权威就是&#xff1a; 指出大部分人视而不见的问题的气质今天畅所欲言而不是空等明天的好心【…

蒙特卡罗方法介绍(一)

蒙特卡罗方法介绍(一) 一、蒙特卡罗方法的基本思想和解题步骤 1.1 蒙特卡罗方法的基本思想 蒙特卡罗方法也称随机模拟法、随机抽样技术或统计实验发&#xff0c;其基本思想是&#xff1a;为了求解数学、物理、工程技术或生产管理等方面的问题。首先&#xff0c;建立一个与求…

神策数据张涛:如何让用户标签价值落地?

本文根据神策数据副总裁张涛在《用户个性化运营—标签体系搭建新机遇》主题沙龙中演讲整理所得。 标签系统&#xff0c;在企业中已不是什么“高大上”的说辞。然而让用户标签价值真正落地企业不多&#xff0c;就像“青少年谈性”&#xff0c; 有一段话形容得再贴切不过&#xf…

蒙特卡罗方法介绍( 二)

蒙特卡罗方法介绍( 二) 一、蒙特卡罗求解定积分 蒙特卡洛方法求解定积分有两种方法&#xff0c;一种是上一节中讲的投点法&#xff0c;另外一种是期望法&#xff08;也称平均值法&#xff09;。 1.1 投点法 给出如下曲线f(x)f(x)f(x),求f(x)f(x)f(x)在a,ba,ba,b上的积分&am…

大数据技术之kafka (第 3 章 Kafka 架构深入) 分区策略在分析

如果不懂分区策略请看我之前的文章&#xff1a;https://blog.csdn.net/ywl470812087/article/details/105328015 默认的方式我们采用的是Range策略方式&#xff08;按主题给消费者消费&#xff0c;主题被谁订阅了就谁消费&#xff09; 先看下下面这个图&#xff0c;画的很丑&a…

如何达成目标笔记

如何达成目标 一、本书主要内容 推荐序一 升级你的行动工具箱 推荐序二 人们可以改变 引言 成功者和自制力的悖论 //004 自制力到底是怎样的 //007 你能做什么 //009 本书的主题 //011 1.1 准备就绪 第1章 你明白自己去往哪里吗 别说“做到最好” //017 大局与细节 //…

大数据技术之 Kafka (第 4 章 Kafka API ) Producer API

4.1.1 消息发送流程 Kafka 的 Producer 发送消息采用的是异步发送的方式。在消息发送的过程中&#xff0c;涉及到了两个线程——main 线程和 Sender 线程&#xff0c;以及一个线程共享变量——RecordAccumulator。main 线程将消息发送给 RecordAccumulator&#xff0c;Sender…

《关键对话——何谓关键对话》读书笔记(一)

《关键对话——何谓关键对话》读书笔记&#xff08;一&#xff09; 利用假期的时间&#xff0c;将关键对话阅读了一遍&#xff0c;书中提到的观点&#xff0c;方法&#xff0c;场景等很适合我目前处的状态&#xff0c;有的时候读起来仿佛就是自己身临其境&#xff0c;有种感同身…

从java读取Excel继续说大道至简 .

在上一篇博客《从复杂到简单&#xff0c;大道至简》中说道我们要把复杂的问题简单化&#xff0c;也就是要把问题细分&#xff0c;让大问题变成小问题&#xff0c;这样解决起来会相对容易&#xff0c;当我们把容易的小问题解决掉了&#xff0c;大问题自动就会迎刃而解。 所以今天…

推荐算法工程师的成长之道

推荐算法工程师的成长之道 原创&#xff1a; gongyouliu 大数据与人工智能 3月20日 源链接&#xff1a;原文地址 本文&#xff0c;作者会基于自己的实践经验讲述推荐算法工程师的成长之道&#xff0c;这里的“道”有发展路径和道(道理、方法论、经验、智慧)两层意思。 所以本文…

java电子商务源码解读 b2b2c o2o

大型企业分布式互联网电子商务平台&#xff0c;推出PC微信APP云服务的云商平台系统&#xff0c;其中包括B2B、B2C、C2C、O2O、新零售、直播电商等子平台。 分布式、微服务、云架构电子商务平台 java b2b2c o2o 技术解决方案 开发语言&#xff1a; java、j2ee 数据库&#x…

信息流推荐多样性

信息流推荐多样性 一、问题现状 信息流产品中一个常见的问题是多样性越来越差&#xff0c;造成这种问题的原因在于机器学习算法本身。下面通过一副系统循环图来介绍多样性差的问题。 资讯库随机推荐文章&#xff0c;由于是按照全库比例采样&#xff0c;娱乐占比较大&#xf…

Robocode教程2——你的第一个robo,取个好名字哦

摘自&#xff1a;http://site.douban.com/widget/notes/7736245/note/210029011/ 你需要准备的东西&#xff1a;1.c语言的知识和一点点的java知识&#xff0c;robocode意在学习java&#xff0c;不要要太深的java水平&#xff0c;你只要理解java和c的区别就可以了。2.robocode A…

UI设计师的面试过程

Palantir Technologies是一家提供分析、整合、可视化各种数据的IT型技术公司。在该公司&#xff0c;前端工程师和后端工程师有同样的面试过程&#xff0c;前端工程师也需要的一定的编程基础。该公司技术博客Palantir TeckBlog日前发表了一篇博文《The UI Design Interview》&am…