LinkedList(附源码分析)

416 阅读9分钟

​先欣赏个美景~~

这个也是同上一篇ArrayList一起肝出来的,别问我为什么,我不需要睡觉,上一篇分析了ArrayList的源码、特性和面试热点问题,这一篇一起来看LinkedList吧!

先给大家看下Collection接口,下面有List和Set接口继承于集合Collection接口(Map接口和Collection接口没关系哦)

由于湿兄想给大家贴些源码来分析,所以一篇文章肯定肝不完,这一篇主要肝linkedList,其余的Map和Set下面继续肝~

无论是工作中还是面试中,集合可以说是最最常见的了,我就不信你在校招时没被问过集合,我就不信你在工作中用不到集合,就是不信。

了解

Collection和Map是两个高度抽象的接口:

  • Collection抽象的是集合,包含了集合的基本操作和属性,Collection主要包含List和Set两大分支。

  • List是有序的链表,允许存储重复的元素,List的主要实现类有LinkedList, ArrayList, Vector, Stack。

  • Set是不允许存在重复元素的集合,Set的主要实现类有HastSet和TreeSet(依赖哈希实现,后面介绍)。

集合是Java中用来存储多个对象的一个容器,我们知道容器数组,数组长度不可变,且只能存储同样类型的元素,可以存储基本类型或者引用类型。而集合长度可变,可以存储不同类型元素(但是我们一般不这么干),集合只能存储引用类型(基本类型会变成包装类)。

集合的fail-fast机制和fail-safe机制:

  • fail-fast快速失败机制,一个线程A在用迭代器遍历集合时,另个线程B这时对集合修改会导致A快速失败,抛出ConcurrentModificationException 异常。在java.util中的集合类都是快速失败的。

  • fail-safe安全失败机制,遍历时不在原集合上,而是先复制一个集合,在拷贝的集合上进行遍历。在java.util.concurrent包下的容器类是安全失败的,建议在并发环境下使用这个包下的集合类。

fail-fast****快速失败是通过在遍历过程中使用一个modCount变量,每次遍历之前会检查modCount变量是否和预期的值一样,是的话遍历,不一样抛出异常,终止遍历。

LinkedList

源码部分先声明下,有我的CSDN网址水印,个人原创,无抄袭,大家随意观看~~

LinkedList定义:

  public class LinkedList<E> extends AbstractSequentialList<E> 
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable {}

LinkedList简介:

  • LinkedList是List接口的双向链表的实现,并允许包括null在内的所有元素。同时也实现了Deque接口,Deque是一个双向队列,那么也意味着LinkedList也是双端队列的一种实现,我们可以操作LinkedList像操作队列和栈一样。

  • 双向链表是以Node节点为基础的实例,Node中包含成员变量:prev,next,item。其中,prev是该节点的上一个节点,next是该节点的下一个节点,item是该节点所包含的值,所以插入速度快(时间复杂度为O(1),但是涉及到先确定位置再插入的时间复杂度也会变为O(n)),只需要移动前后节点指针即可。

  • LinkedList是有序的并且可以包含重复元素,非线程安全的。

  • LinkedList的链式存储结构比数组的连续存储内存利用率更高,查询速度相对较慢,因为每次查询都需要从头节点或者尾节点遍历next指针。

LinkedList源码:

先来看下属性~

构造函数(了解即可)

      /**       * Constructs an empty list.       */   
   public LinkedList() {      }   
   public LinkedList(Collection<? extends E> c) {  
      this();     
   addAll(c); 
     }

内部类Node就是实际的结点,用于存放实际元素的地方

add函数:将指定元素添加到列表末尾。

接着我们看**linkLast()**函数:

下面看一个示例说明和其示意图:

  List<Integer> lists = new LinkedList<Integer>();  lists.add(5);  lists.add(6);

说明:如上图所示,当执行完了添加元素5和元素后之后的状态是,5的pre指针是null(同时代表first),然后next指针指向6(同时代表last),然后尾节点的next指针也为null;执行添加元素的过程时,先将尾节点保存为final类型,再创建新节点,pre指针指向尾节点,再将尾节点last指针指向该新节点,重新赋值尾节点。

addAll()函数:添加一个集合,如下图所示,addAll函数有两个重载函数,不过底层都会转化为addAll(int, Collection<? extends E>)这个,所以我们来分析这个函数;

补充:其中第2步涉及到一个node()函数,根据索引定位找到元素并返回。其中会根据index < (size >> 1)先判断index属于LinkedList的前半段还是后半段,因为LinkedList是双向链表,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样便于更快的索引找到元素。

get()函数:利用上面的node函数定位,不再细说;

总结:LinkedList底层的数据结构是基于双向循环链表的,且头结点中不存放数据。在LinkedList中涉及到的增删改操作都是通过操作节点Node的指针,所以比较方便插入删除等操作,效率更高;在ArrayList的操作中需要将很多数组元素进行位置的移动,所以相较而言效率更低些;

重点关注问题:

在addAll()中,传入一个集合为何要先转变成一个数组再遍历数组,添加数组的元素,而不是直接遍历集合?

如上截图是官方解释,toArray的目的是保证传进来的这个集合不会被任何地方引用,也保证这个集合不会有任何机会被修改(即在addAll过程中Collection的内容发生变化),保证了数据的安全性

分析一下LinkedList的遍历方式?

1、迭代器和链表迭代器

Iterator iterator=list.iterator();  //ListIterator<Student> iterator=list.listIterator();  while(iterator.hasNext()){     System.out.println(iterator.next());   }

2、快速随机(会存在严重效率问题,看后面分析,强烈不建议使用!!)

for(int i=0;i<list.size();i++){      System.out.println(list.get(i));     }

3、使用增强for遍历

for(String str:list){        System.out.println(str);      }

4、list的函数pollFirst,pollLast,removeFirst,removeLast(但用它们遍历时,会删除原始数据)

while(list.pollFirst() != null)//while(list.pollLast() != null)//while(list.removeFirst() != null)//while(list.removeLast() != null)      {  }

分析遍历:list的函数removeFist或removeLast遍历方式效率很高,但是遍历之后会删除原始数据。而list的快速随机(用size进行遍历循环)的遍历方式存在很多的问题,为什么呢?一起看:

如果测试数据较少的时候可能会发现没什么问题,但是随着数据量的加大我们会发现数据有明显的卡顿现象,我们在前面分析了get()方法以及其内部的node()方法(用来定位该位置的元素),会判断当前元素是在链表前半段或者后半段然后决定从哪边遍历,然后取数据的时候无论目标元素在哪里都会从头部节点或者尾部节点遍历到目标节点再去除数据。

如果链表中存在10个元素需要遍历,那么每次元素的查询次数为 (1,2,3,4,5,5,4,3,2,1)总次数为30,随着n趋向于无穷大时时间复杂度趋向于O(n^2)。当目标数量n越大时,时间复杂度的增长也就越快,从而导致了程序假死。

所以,强烈建议不要使用size循环遍历数据,使用遍历器或者foreach的效率都还不错。

LinkedList如何提供通过位置获取数据的功能的,它的查询效率真的非常低吗?

LinkedList实际上是通过双向链表去实现的。既然是双向链表,那么它的顺序访问会非常高效,而随机访问效率比较低。LinkedList最大的好处在于头尾和已知节点的插入和删除时间复杂度都是o(1)。

但是涉及到先确定位置再操作的情况,则时间复杂度会变为o(n),因为对于LinkedList来说确定位置是需要从头节点或者尾节点循环。当然,每个节点都需要保留prev和next指针也是经常被吐槽是浪费了空间。

它是如何用作栈、队列或双端队列的?

如上图所示,实现了Deque接口,LinkedList可用作队列或双端队列就是因为实现了它。(具体实现方法不说了,根据队列和栈的结构来实现即可,相信聪明的大家肯定会~)

LinkedList不是线程安全的,怎么办?

为何线程不安全?线程安全问题是由多个线程同时写或同时读写同一个资源造成的,不多解释,看下图,参考ArrayList;

解决办法~

1、Collections.synchronizedList(new LinkedList());见上面在ArrayList中的分析,也会出现:由iterator()和listIterator()返回的迭代器是fail-fast的,需要手动同步

2、换成ConcurrentLinkedQueue或者LinkedBlockingQueue这种支持添加元素为原子操作的队列;LinkedBlockingQueue使用锁机制,ConcurrentLinkedQueue使用的是CAS算法(乐观锁),不过LBQ的底层获取锁也是使用的CAS算法。

LinkedBlockingQueue的put等方法,是使用ReentrantLock来实现的添加元素原子操作。我们看一下add和offer()方法,方法中使用了CAS来实现的无锁的原子操作:

关于插入元素的性能,ConcurrentLinkedQueue肯定是最快的,在实际的使用过程中,尤其在多cpu的服务器上,有锁和无锁的差距便体现出来了,ConcurrentLinkedQueue会比LinkedBlockingQueue快很多。

分析ArrayList和LinkedList两者各适用于哪些场合

1、如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;

2、如果应用程序有更多的插入或者删除操作,较少的数据读取,LinkedList对象要优于ArrayList对象;

3、ArrayList的插入,删除操作也不一定比LinkedList慢,如果在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。

**总结:**其实使用场景还是要具体分析,根据ArrayList和LinkedList的特点来进行分析需要应用哪个。

Vector

Vector定义:

public class Vector<E> extends AbstractList<E>   implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

Vector简介:

  • Vector 继承了AbstractList,实现了List,所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。

  • Vector是线程安全的,也可以看作是线程安全的ArrayList,因为其内部很多函数是加了Synchronized关键字的ArrayList函数,不过也因为这个导致Vector效率稍低些;

  • 在Vector中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问

Vector核心函数:

Vector内部很多源码和ArrayList的类似,不再赘述,感兴趣的可以自己去研究~

重点关注问题:

底层是数组,现在已经很少使用,被ArrayList替代,了解即可。

  • Vector所有方法都是同步(synchronized),而这些从来都不是必须的,有性能损失。

  • Vector初始length是10 超过length时以**100%**比率增长,相比于ArrayList更多消耗内存。

Stack

Stack定义:

 class Stack<E> extends Vector<E> {}

Stack简介:

  • Stack是栈,继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。

  • 由于Vector是通过数组实现的,这就意味着,Stack也是通过数组实现的,而非链表。

  • 类Stack是栈,但是实际这个类用的并不多,但是它实现的栈结构却是经常使用的,栈结构我们会在数据结构篇详细讲解。

絮叨叨

你知道的越多,你不知道的也越多。

建议:这个是LinkedList,底层双向链表,常用集合类之一。Java基础集合是面试中的宠儿,也是我们工作中最常用的工具类了。很多同学可能会被各种集合以及底层原理搞懵逼,其实大家多用几遍,多看几遍源码,发现,不过如此~

觉得不错的可以给船长来个关注,也欢迎可爱帅气的你推荐给你的朋友,转发和点赞是可以给湿兄多打打气~~