今天突发奇想看了一下LinkedList的源码,还挺有趣的,话不多说,show me the code。

我使用的是IDEA,快捷键仅供参考。

按住Ctrl再点击类名可以进入类的源码,随便写一个含有LinedList的程序,点进LinedList类。这个类实现了一些接口,继承了AbstractSequentialList这个类,可以想象等下有一些常见的方法的实现方式可能要点进AbstractSequentialList这个类中才看得到了。

先来了解一下节点的实现,按住Ctrl点击Node,发现Node是LinkedList的一个内部类,和我们自己定义的Node很相似,有值,前一个节点和后一个节点,带参构造方法可以直接将节点添加到链表中:


贴代码,去掉原注释添加新注释看得更清楚

public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{transient int size = 0;transient Node<E> first; //指向头节点transient Node<E> last; //指向尾节点public LinkedList() { //空链表构造}public LinkedList(Collection<? extends E> c) { //将c集合中的全部元素添加进链表this();addAll(c); }

上图中最后一个方法可以按住Ctrl点addAll看一下,添加了注释内容如下:

继续点addAll,就找到了方法的具体实现。类中每个方法上面都有详细的注释,读读注释就知道这个类的前因后果以及可能踩到的坑。所以我简述了一下注释的内容并放在方法上面。另外方法中的大部分内容也以注释的方式进行解释。

/**将集合c中的元素按原顺序插入index位置以前。将index位置元素及后面全部元素向后移动。返回值是Boolean类型,参数分别是插入位置和要插入的集合
*/
public boolean addAll(int index, Collection<? extends E> c) {checkPositionIndex(index);// toArray方法可以点进去看,进入一个接口,方法上的详细注释表明,这个方法会将集合中的全部元素转化成数组,且保留原始顺序。Object[] a = c.toArray(); int numNew = a.length;//如果要插入的是空集合,返回falseif (numNew == 0)return false;//插入位置的前一个结点,后一个节点(index节点)。有了这两个节点才能使用Node的构造函数进行节点插入。Node<E> pred, succ;//插入位置是最后一个节点的后面,以追加的方式插入if (index == size) {succ = null;pred = last;//插入位置是其它节点} else {succ = node(index);pred = succ.prev;}//边插入边更新first节点for (Object o : a) {@SuppressWarnings("unchecked") E e = (E) o;Node<E> newNode = new Node<>(pred, e, null);if (pred == null)first = newNode;elsepred.next = newNode;pred = newNode;}//更新last节点if (succ == null) {last = pred;} else {pred.next = succ;succ.prev = pred;}//更新节点总数size += numNew;//每对链表修改一次,modCount就加一modCount++;return true;}

这里有个modCount属性非常有意思,它其实是LinkedList父类中Abstract类的的一个变量。LinekedList类是线程不安全的,如果一个线程遍历List,另一个线程删除了元素,会产生混乱的结果,抛出ConcurrentModificationException。Iterator(迭代器)类中还有一个变量称为expectedModCount。迭代器初始化时,会设置expectedModCount=modCount。任何通过迭代器修改LinkedList结构的行为都会同时更新expectedModCount和modCount,使这两个值相等。通过LinkedList对象修改其结构的方法只更新modCount。所以假设有两个线程A和B。A通过迭代器遍历并修改LinkedList,而B,与此同时,通过对象修改其结构,会导致迭代器的expectedModCount与链表对象的modCount不相等,而抛出异常。

那么为什么链表支持迭代器前进并更新呢(先调用next(),再调用remove()删除迭代器只想的元素)?因为只有iterator的remove方法会在调用自身的remove之后让 expectedModCount与modCount再相等,所以是安全的。但是因为remove()方法不仅会删除元素,还会维护一个标志,用来记录目前是不是可删除状态,因此不能连续两次调用它的remove()方法,调用之前至少有一次next()方法的调用。

接下来几个方法分别是插入头节点,插入尾节点,在某一节点之前插入节点,这些操作的实现应该已经被我们熟知。不作详细解释。


然后是删除尾部节点的操作,请注意l.prev = null的部分:

    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;elseprev.next = null;size--;modCount++; //同样的,增加修改次数return element;}

后面的一堆方法,也是节点的增删和返回,不作过多解释。

下面这个方法可以详细看一下,是remove(Object o)方法,按照值删除。同样把要点写在注释里

    /*** 删除第一次出现的与目标相等的节点,如果链表中没有该节点,则链表不会被改变。*/public boolean remove(Object o) {//注意目标可以为空,并不是值得链表节点为空,而是链表节点的值为空if (o == null) {for (Node<E> x = first; x != null; x = x.next) {if (x.item == null) {unlink(x); //unlink方法是将目标节点移除。return true;}}} else { //只有当目标值不为null的时候才判断相等for (Node<E> x = first; x != null; x = x.next) {if (o.equals(x.item)) {unlink(x);return true;}}}return false; //如果没有找到节点就返回false}

接下来是clear(),清空链表操作。这是一个O(n)的操作!它将链表的所有节点的值,前一个节点,后一个节点全都置空了。方法中的注释翻译过来,是说虽然逐个删除不必要,但是这么做可以保证垃圾回收机制将链表正确回收,即便有iterator引用链表中节点,也能被正常回收。

    public void clear() {// Clearing all of the links between nodes is "unnecessary", but:// - helps a generational GC if the discarded nodes inhabit//   more than one generation// - is sure to free memory even if there is a reachable Iteratorfor (Node<E> x = first; x != null; ) {Node<E> next = x.next;x.item = null;x.next = null;x.prev = null;x = next;}first = last = null;size = 0;modCount++;}

接下来的一些方法是在固定位置的更改,添加,删除,判断等。需要注意的就是后面一些私有方法,是判断位置是否合法,不是Boolean方法,而是判断失败直接抛出异常。前面的公有方法会先引用后面的私用方法,index不合法则抛异常,合法再进行下一步操作。

接下来的index方法和前面的remove()有点类似,返回第一个符合元素的位置。如果找不到则返回-1。

接下来是peek()方法。咦?返回第一个元素,这个与getFirst()方法有什么区别?对比一下源码可以看出来,peek()可以返回null,而getFirst()当头元素是空的时候会抛出异常。


后面一些方法基本都是前面增删改方法的别名,不再复制了,让我们一直向下找到removeLastOccurrence(Object o) 方法。重新提醒一下,按照对象的值查找,在链表里是O(n)的,下面这个方法是从后往前查找,其它与查找目标值第一次出现的实现类似。

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

后面是一些迭代器有关的内部类和方法,我会放到下一篇博客再说。

先跳过所有跟iterator相关的部分,我们会来到一个很重要的地方:clone()方法。先看注释,说clone()是浅拷贝,浅拷贝是深拷贝都能得到一个看似一模一样的新对象。但是在实际使用过程中会发现:对于浅拷贝,当对象中含有可变的引用类型属性时,在复制得到的新对象对该引用类型属性内容进行修改,原始对象响应的属性内容也会发生变化。而深拷贝会将引用类型的属性内容也拷贝一份新的,实现深拷贝有两种方式:第一种是给需要拷贝的引用类型也实现Cloneable接口并覆写clone方法;第二种则是利用序列化。可以参考https://www.cnblogs.com/nickhan/p/8569329.html。
那么我们知道链表中的clone()方法是浅拷贝,接下来简单讲一下为什么clone()方法调用了superClone()方法。superClone()调用了super.clone(),即Object类的clone()方法。即Object类的clone()方法是一个本地方法,它有两个作用,一是为对象拷贝申请内存空间,二是检查调用了clone()的类是否实现了clonable接口,如果没有实现该接口,会抛出CloneNotSupportedException异常。

接下来的复制过程也可以看出来,使用clone.add(x.item)仅仅是用Node的值创建新的Node(如果不了解的话,可以点进add方法看一下实现),并追加到链表末尾,如果该Node的值也是一个引用,那么新Node和原始Node的值会指向同一个地址。

 //这个方法没有任何注释,其实就是@SuppressWarnings("unchecked")private LinkedList<E> superClone() {try {return (LinkedList<E>) super.clone();} catch (CloneNotSupportedException e) {throw new InternalError(e);}}/*** 返回本对象的浅拷贝*/public Object clone() {LinkedList<E> clone = superClone();// 初始化一个空链表clone.first = clone.last = null;clone.size = 0;clone.modCount = 0;// Initialize clone with our elementsfor (Node<E> x = first; x != null; x = x.next)clone.add(x.item);return clone;}

接下来有两个toArray()方法,无参方法返回Object[]数组,这个方法比较简单,数组中的值是所有Node节点中的值。数组的顺序与链表的顺序一致。

    public Object[] toArray() {Object[] result = new Object[size];int i = 0;for (Node<E> x = first; x != null; x = x.next)result[i++] = x.item;return result;}

有参方法如下,这个方法上的注释很长,首先说明这个方法相比上一个方法,更容易控制返回类型,但是如果类型不匹配,即数组a的元素类型不是链表中元素值的类型或值类型的父类,会抛出ArrayStoreException。另外如果a元素的长度小于链表长度,会通过反射重新建立一个与链表长度相等的数组并将a的引用指向它。然后与上一个方法类似,将链表中所有节点的值按照顺序放入a数组。如果a数组的长度大于链表长度,放置完成后会将下一个位置置为null,作为一个结束标记。

    public <T> T[] toArray(T[] a) {if (a.length < size)a = (T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);int i = 0;Object[] result = a;for (Node<E> x = first; x != null; x = x.next)result[i++] = x.item;if (a.length > size)a[size] = null;return a;}

后面的方法是流相关的方法。暂时不说,END。欢迎批评指正。

JAVA8 LinkedList 链表源码详细解析相关推荐

  1. spark word2vec 源码详细解析

    spark word2vec 源码详细解析 简单介绍spark word2vec skip-gram 层次softmax版本的源码解析 word2vec 的原理 只需要看层次哈弗曼树skip-gram ...

  2. 20行Python代码爬取2W多条音频文件素材【内附源码+详细解析】新媒体创作必备

    大家好,我是辣条. 今天的内容稍显简单,不过对于新媒体创作的朋友们还是很有帮助的,你能用上的话记得给辣条三连! 爬取目标 网站:站长素材 工具使用 开发工具:pycharm 开发环境:python3. ...

  3. ArrayList源码详细解析(一)

    Java ArrayList源码解析(基于JDK 12,对比JDK 8) 自从接触到ArrayList以来,一直觉得很方便,但是从来没有系统.全面的学习了解过ArraryList的实现原理.最近学习了 ...

  4. Hadoop HDFS创建文件/写数据流程、源码详细解析

    HDFS创建文件/写数据源码解析 HDFS HDFS写流程 创建文件源码 客户端 DistributedFileSystem DFSClient DFSOutputStream 客户端/Namenod ...

  5. golang mutex源码详细解析

    目前golang的版本是1.12,其中的mutex是增加了普通模式和饥饿模式切换的优化版本,为了便于理解,这里先从上一个版本1.7版本的mutex开始分析,以后再对优化版本进行说明. Mutex结构说 ...

  6. Faster_R_CNN源码详细解析

    Faster R-CNN整体架构 首先使用共享卷积层为全图提取特征feature maps 将得到的feature maps送入RPN,RPN会产生接近两千个候选框proposals RoI Pool ...

  7. 火车轨道铁路轨道检测识别(附带Python源码+详细解析)

    现在的网络上,铁轨检测的源码几乎没有,所以自己参照着一篇汽车车道线检测的方法,然后调节参数,实现了铁轨的轨道检测,但现在只能检测直线,弯曲的铁轨检测下一步会实现,实现之后会更新的,敬请期待. 弯轨检测 ...

  8. HandlerThread原理、使用实例、源码详细解析

    0.目录 一.HandlerThread简介 二.HandlerThread原理 三.HandlerThread使用实例 四.HandlerThread源码分析 五.总结 一.HandlerThrea ...

  9. MJRefresh 源码详细解析

    MJRefresh是李明杰老师的作品,到现在已经有9800多颗star了,是一个简单实用,功能强大的iOS下拉刷新(也支持上拉加载更多)控件.它的可定制性很高,几乎可以满足大部分下拉刷新的设计需求,值 ...

最新文章

  1. 网络操作系统与通常的操作系统的区别
  2. MybatisPlus中insert方法与insertAllColumn方法的区别
  3. java printstacktrace_为什么异常. printStackTrace() 被认为是不好的实践?_java_酷徒编程知识库...
  4. Python中的特殊属性与方法
  5. php原生态三级联动_ajax php实现三级联动的方法
  6. c语言文件读写_学生信息管理系统(C语言\单向链表\文件读写)
  7. 查找某节点的所有祖先☆
  8. jquery 监听返回事件
  9. 云服务器的IT价值与部署分析
  10. unity3d 材质概述 ---- shader
  11. C# 线程间互相通信
  12. JMeter设置集合点
  13. android 遥控器方向,最简单DIY基于Android系统的万能蓝牙设备智能遥控器
  14. 超简单的用PS(PhotoShop)转换png为ico,简单的制作favicon.ico,使用画图工具转换PNG为ICO图标,不用下什么插件软件什么玩意儿的
  15. vue引入阿里巴巴矢量图库图标
  16. 一文带你了解什么是CDN?
  17. wav文件隐写:Deepsound+TIFF图片PS处理( AntCTF x D^3CTF 2022 misc BadW3ter)
  18. 【用HTML+CSS实现简单的轮播图片效果】
  19. 刚体质量分布与牛顿-欧拉方程
  20. (三)腾讯云开发工程师TCA题库(题目含详细解析)

热门文章

  1. Linux cgroup详解(理论与实例)
  2. JS判断变量是不是数组的5种方法
  3. Spark-Excel算子
  4. 网上邻居无法访问的一点启示
  5. 反射内存卡读写测试(RFM2gRead和RFM2gWrite)-- C++
  6. FFmpeg开发之旅(二)---音频解码
  7. CSS水平垂直居中常见方法总结
  8. android--圆角框--dialog(圆角框)
  9. 如何成功抵御DOS攻击?给你介绍四种方法
  10. “PPT中如何插入和提取swf文件”的解决方案