LinkedList概述

​ LinkedList 是 Java 集合框架中一个重要的实现,我们先简述一下LinkedList的一些特点:

  • LinkedList 底层采用的双向链表结构;
  • LinkedList 支持空值和重复值(List的特点);
  • LinkedList 实现Deque接口,具有双端队列的特性,也可以作为栈来使用;
  • LinkedList 存储元素过程中,无需像 ArrayList 那样进行扩容,但存储元素的节点需要额外的空间存储前驱和后继的引用;
  • LinkedList 在链表头部和尾部插入效率比较高,但在指定位置进行插入时,需要定位到该位置处的节点,此操作的时间复杂度为O(N)
  • LinkedList 是非线程安全的集合类,并发环境下,多个线程同时操作 LinkedList,会引发不可预知的异常错误。

LinkedList继承体系

​ 直接通过idea查看一下LinkedList的继承体系,体系结构比较复杂,一点点看。

  • 继承自 AbstractSequentialList;
  • 实现了 List 和 Deque 接口;
  • 实现序列化接口;
  • 实现了Cloneable接口

​ 这里简单说一下AbstractSequentialList这个类,该类提供一套基本的基于顺序访问的接口,通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问某种序列表(比如链表)的接口。AbstractSequentialList 提供的方法基本上都是通过 ListIterator 实现的,比如下面的get和add方法。但是虽然LinkedList 继承了 AbstractSequentialList,却并没有直接使用父类的方法,而是重新实现了一套的方法,后面我们会讲到这些方法的实现。

  1. public E get(int index) {
  2. try {
  3. return listIterator(index).next();
  4. } catch (NoSuchElementException exc) {
  5. throw new IndexOutOfBoundsException("Index: "+index);
  6. }
  7. }
  8. public void add(int index, E element) {
  9. try {
  10. listIterator(index).add(element);
  11. } catch (NoSuchElementException exc) {
  12. throw new IndexOutOfBoundsException("Index: "+index);
  13. }
  14. }
  15. // 留给子类实现
  16. public abstract ListIterator<E> listIterator(int index);

​ 另外的就是文章开头概述的,LinkedList实现了Deque接口,具有双端队列的特点。

LinkedList的成员属性

  1. //记录链表中的实际元素个数
  2. transient int size = 0;
  3. //维护链表的首结点引用
  4. transient Node<E> first;
  5. //维护链表的尾节点引用
  6. transient Node<E> last;

可以看到first和last都是Node类型的,所以我们简单看一下LinkedList中的这个内部类

  1. private static class Node<E> {
  2. E item; //结点中存放的实际元素
  3. Node<E> next; //维护结点的后继结点
  4. Node<E> prev; //维护结点的前驱结点
  5. //构造方法,创建一个新的结点,参数为:前驱结点,插入元素引用,后继节点
  6. Node(Node<E> prev, E element, Node<E> next) {
  7. this.item = element;
  8. this.next = next;
  9. this.prev = prev;
  10. }
  11. }

​ 可以看到Node这个静态内部类的结构也是比较简单的,每个结点维护的就是自己存储的元素信息+前驱结点引用+后继节点引用。这里就不做过多的阐述,下面简单看看LinkedList的构造方法

LinkedList的构造方法

  1. //构造一个空的集合(链表为空)
  2. public LinkedList() {
  3. }
  4. //先调用自己的无参构造方法构造一个空的集合,然后将Collection集合中的所有元素加入该链表中
  5. //如果传入的Collection为空,会抛出空指针异常
  6. public LinkedList(Collection<? extends E> c) {
  7. this();
  8. addAll(c);
  9. }

LinkedList的主要方法

add方法

LinkedList实现的添加方法主要有下面几种

  • 在链表尾部添加结点(linkLast方法)

  • 在链表首部添加元素(linkFirst方法)

  • 在链表中间添加元素(linkBefore方法)

下面我们看看这三种方法的实现。

(1)linkLast方法

  1. public void addLast(E e) {
  2. linkLast(e);
  3. }

​ 在addLast方法中直接就是调用了linkLast方法实现结点的添加(没有返回值,所以add方法一定是返回true的),所以下面我们看看这个方法:

  1. void linkLast(E e) {
  2. //(1)获得当前链表实例的全局后继节点
  3. final Node<E> l = last;
  4. //(2)创建一个新的结点,从Node的构造方法我们就能知道
  5. //这个新的结点中存放的元素item为当前传入的泛型引用,前驱结点为全局后继结点,后继节点为null
  6. //(即相当于要将这个新节点作为链表的新的后继节点)
  7. final Node<E> newNode = new Node<>(l, e, null);// Node(Node<E> prev, E element, Node<E> next){}
  8. //(3)更新全局后继节点的引用
  9. last = newNode;
  10. //(4)如果原链表的后继结点为null,那么也需要将全局头节点引用指向这个新的结点
  11. if (l == null)
  12. first = newNode;
  13. //(5)不为null,因为是双向链表,创建新节点的时候只是将newNode的prev设置为原last结点。这里就需要将原last
  14. //结点的后继结点设置为newNode
  15. else
  16. l.next = newNode;
  17. //(6)更新当前链表中的size个数
  18. size++;
  19. //(7)这里是fast-fail机制使用的参数
  20. modCount++;
  21. }

​ 我们通过一个示例图来简单模拟这个过程

  • 当链表初始时为空的时候,我么调用add方法添加一个新的结点

  • 链表不为空,此时调用add方法在链表尾部添加结点的时候

(2)linkFirst方法

​ 该方法是一个private方法,通过addFirst方法调用暴露给使用者。

  1. public void addFirst(E e) {
  2. linkFirst(e);
  3. }

​ 我们还是主要看看linkFirst方法的实现逻辑

  1. private void linkFirst(E e) {
  2. //(1)获取全局头节点
  3. final Node<E> f = first;
  4. //(2)创建一个新节点,其前驱结点为null,后继结点为当前的全局首结点
  5. final Node<E> newNode = new Node<>(null, e, f);
  6. //(3)更新全局首结点引用
  7. first = newNode;
  8. //(4)如果首结点为null,last结点指向新建的结点
  9. if (f == null)
  10. last = newNode;
  11. //(5)不为null,原头节点的前驱结点为newNode
  12. else
  13. f.prev = newNode;
  14. size++;
  15. modCount++;
  16. }

​ 上面的逻辑也比较简单,就是将新添加的结点设置为头节点,然后更新链表中结点之间的指向,我们通过下面这个图简单理解一下(链表初始为null就不做演示了,和上面图示的差不多,这里假设已经存在结点)

(3)linkBefore方法

  1. public void add(int index, E element) {
  2. //检查index的合法性:大于等于0小于等于size,不合法会抛出异常
  3. checkPositionIndex(index);
  4. //index等于size,就在尾部插入新节点,linkLast方法上面说到过
  5. if (index == size)
  6. linkLast(element);
  7. //否则就在指定index处插入结点,先找到index处的结点(调用的是node(index方法))
  8. else
  9. linkBefore(element, node(index));
  10. }
  11. private void checkPositionIndex(int index) {
  12. if (!isPositionIndex(index))
  13. throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
  14. }
  15. private boolean isPositionIndex(int index) {
  16. return index >= 0 && index <= size;
  17. }

​ add(index,element)方法中主要的逻辑还是linkBefore,我们下面看看这个方法,在此之前调用的是node(index)方法,找到index处的结点

  1. Node<E> node(int index) {
  2. //index < size/2 (index在链表的前半部分)
  3. if (index < (size >> 1)) {
  4. //使用全局头节点去查找(遍历链表)
  5. Node<E> x = first;
  6. for (int i = 0; i < index; i++)
  7. x = x.next;
  8. return x;
  9. } else {
  10. //index > size / 2 (index在链表的后半部分)
  11. Node<E> x = last;
  12. //使用全局尾节点向前查找
  13. for (int i = size - 1; i > index; i--)
  14. x = x.prev;
  15. return x;
  16. }
  17. }

​ node方法实现利用双向链表以及记录了链表总长度的这两个特点,分为前后两部分去遍历查询jindex位置处的结点。查找这个结点后,就会作为参数调用linkBefore方法,如下所示

  1. void linkBefore(E e, Node<E> succ) {
  2. //succ != null;succ就是指定位置处的结点
  3. //传入的结点element=succ
  4. final Node<E> pred = succ.prev;
  5. //创建新的结点
  6. //前驱结点是传入的结点的前驱结点
  7. //后继结点是传入的结点
  8. final Node<E> newNode = new Node<>(pred, e, succ);
  9. //更新index处结点的前驱结点引用
  10. succ.prev = newNode;
  11. //index处结点的前驱结点为null,那么就相当于在头部插入结点,并且更新first
  12. if (pred == null)
  13. first = newNode;
  14. //不为null,那么它的后继结点就是新的结点
  15. else
  16. pred.next = newNode;
  17. size++;
  18. modCount++;
  19. }

​ 这个方法的逻辑也比较简单,就是在succ和succ.prev两个结点之间插入一个新的结点,我们通过简单的图示理解这个过程

删除

​ 作为双端队列,删除元素也有两种方式队列首删除元素队列尾删除元素;作为List,又要支持中间删除元素,所以删除元素一个有三个方法。

(1)unlinkFirst方法

​ 下面是调用unlinkFirst方法的两个public方法(Deque接口的方法实现),主要区别就是removeFirst方法执行时候,first为null的时候会抛出异常,而pollFirst返回null。

  1. // remove的时候如果没有元素抛出异常
  2. public E removeFirst() {
  3. final Node<E> f = first;
  4. if (f == null)
  5. throw new NoSuchElementException();
  6. return unlinkFirst(f);
  7. }
  8. // poll的时候如果没有元素返回null
  9. public E pollFirst() {
  10. final Node<E> f = first;
  11. return (f == null) ? null : unlinkFirst(f);
  12. }

​ 主要还是看unlinkFirst这个方法的实现

  1. private E unlinkFirst(Node<E> f) {
  2. // assert f == first && f != null;
  3. //获取头结点的元素值
  4. final E element = f.item;
  5. //获取头结点的后继结点
  6. final Node<E> next = f.next;
  7. //删除头节点中存放的元素item和后继结点,GC
  8. f.item = null;
  9. f.next = null; // help GC
  10. //更新头节点引用(原头节点的后继结点)
  11. first = next;
  12. //链表中只有一个结点,那么尾节点也是null了
  13. if (next == null)
  14. last = null;
  15. //将新的头节点的前驱结点设置为null
  16. else
  17. next.prev = null;
  18. //更新size和modCount
  19. size--;
  20. modCount++;
  21. //返回原头节点的值
  22. return element;
  23. }

(2)unlinkLast方法

​ 下面是调用unlinkLast方法的两个public方法(Deque接口的方法实现),主要区别就是removeLast方法执行时候,first为null的时候会抛出异常,而pollLast返回null。

  1. // remove的时候如果没有元素抛出异常
  2. public E removeLast() {
  3. final Node<E> l = last;
  4. if (l == null)
  5. throw new NoSuchElementException();
  6. return unlinkLast(l);
  7. }
  8. // poll的时候如果没有元素返回null
  9. public E pollLast() {
  10. final Node<E> l = last;
  11. return (l == null) ? null : unlinkLast(l);
  12. }

​ 下面是unlinkLast方法的实现

  1. // 删除尾节点
  2. private E unlinkLast(Node<E> l) {
  3. // 尾节点的元素值
  4. final E element = l.item;
  5. // 尾节点的前置指针
  6. final Node<E> prev = l.prev;
  7. // 清空尾节点的内容,协助GC
  8. l.item = null;
  9. l.prev = null; // help GC
  10. // 让前置节点成为新的尾节点
  11. last = prev;
  12. // 如果只有一个元素,删除了把first置为空
  13. // 否则把前置节点的next置为空
  14. if (prev == null)
  15. first = null;
  16. else
  17. prev.next = null;
  18. // 更新size和modCount
  19. size--;
  20. modCount++;
  21. // 返回删除的元素
  22. return element;
  23. }

(4)unlink方法

  1. // 删除中间节点
  2. public E remove(int index) {
  3. // 检查是否越界
  4. checkElementIndex(index);
  5. // 删除指定index位置的节点
  6. return unlink(node(index));
  7. }
  1. // 删除指定节点x
  2. E unlink(Node<E> x) {
  3. // x的元素值
  4. final E element = x.item;
  5. // x的前置节点
  6. final Node<E> next = x.next;
  7. // x的后置节点
  8. final Node<E> prev = x.prev;
  9. // 如果前置节点为空
  10. // 说明是首节点,让first指向x的后置节点
  11. // 否则修改前置节点的next为x的后置节点
  12. if (prev == null) {
  13. first = next;
  14. } else {
  15. prev.next = next;
  16. x.prev = null;
  17. }
  18. // 如果后置节点为空
  19. // 说明是尾节点,让last指向x的前置节点
  20. // 否则修改后置节点的prev为x的前置节点
  21. if (next == null) {
  22. last = prev;
  23. } else {
  24. next.prev = prev;
  25. x.next = null;
  26. }
  27. // 清空x的元素值,协助GC
  28. x.item = null;
  29. // 元素个数减1
  30. size--;
  31. // 修改次数加1
  32. modCount++;
  33. // 返回删除的元素
  34. return element;
  35. }

查找

LinkedList 底层基于链表结构,无法向 ArrayList 那样随机访问指定位置的元素。LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后查找,时间复杂度为 O(N)。相关源码如下:

  1. public E get(int index) {
  2. checkElementIndex(index); //还是先检验index的合法性,这里上面已经说过
  3. //调用node方法遍历查询index处的结点,然后返回结点存放的值item,node方法上面已经说过
  4. return node(index).item;
  5. }

遍历

​ 链表的遍历过程也很简单,和上面查找过程类似,我们从头节点往后遍历就行了。但对于 LinkedList 的遍历还是需要注意一些,不然可能会导致代码效率低下。通常情况下,我们会使用 foreach 遍历 LinkedList,而 foreach 最终转换成迭代器形式。所以分析 LinkedList 的遍历的核心就是它的迭代器实现,相关代码如下:

  1. public ListIterator<E> listIterator(int index) {
  2. checkPositionIndex(index);
  3. return new ListItr(index);
  4. }
  5. private class ListItr implements ListIterator<E> {
  6. private Node<E> lastReturned;
  7. private Node<E> next;
  8. private int nextIndex;
  9. private int expectedModCount = modCount;
  10. /** 构造方法将 next 引用指向指定位置的节点 */
  11. ListItr(int index) {
  12. // assert isPositionIndex(index);
  13. next = (index == size) ? null : node(index);
  14. nextIndex = index;
  15. }
  16. public boolean hasNext() {
  17. return nextIndex < size;
  18. }
  19. public E next() {
  20. checkForComodification();
  21. if (!hasNext())
  22. throw new NoSuchElementException();
  23. lastReturned = next;
  24. next = next.next;
  25. nextIndex++;
  26. return lastReturned.item;
  27. }
  28. //...other method
  29. }

​ 这里主要说下遍历 LinkedList 需要注意的一个点。LinkedList 不擅长随机位置访问,如果大家用随机访问的方式遍历 LinkedList,效率会很差。比如下面的代码:

  1. List<Integet> list = new LinkedList<>();
  2. list.add(1)
  3. list.add(2)
  4. ......
  5. for (int i = 0; i < list.size(); i++) {
  6. Integet item = list.get(i);
  7. // do something
  8. }

​ 当链表中存储的元素很多时,上面的遍历方式对于效率肯定是非常低的。原因在于,通过上面的方式每获取一个元素(调用get(i)方法,上面说到了这个方法的实现),LinkedList 都需要从头节点(或尾节点)进行遍历(node()方法的实现),效率低,上面的遍历方式在大数据量情况下,效率很差。在日常使用中应该尽量避免这种用法。

总结

最后总结一下面试常问的ArrayListLinkedList的区别,关于ArrayList请参考我上一篇ArrayList源码分析

  • ArrayList是基于动态数组实现的,LinkedList是基于双向链表实现的;

  • 对于随机访问来说,ArrayList(数组下标访问)要优于LinkedList(遍历链表访问);

  • 不考虑直接在尾部添加数据的话,ArrayList按照指定的index添加/删除数据是通过复制数组实现。LinkedList通过寻址改变节点指向实现;所以添加元素的话LinkedList(改变结点的next和prev指向即可)要优于ArrayList(移动数组元素)。

  • LinkedList在数据存储上不存在浪费空间的情况。ArrayList动态扩容会导致有一部分空间是浪费的。

LinkedList源码分析(jdk1.8)的更多相关文章

  1. ArrayList源码分析--jdk1.8

    ArrayList概述   1. ArrayList是可以动态扩容和动态删除冗余容量的索引序列,基于数组实现的集合.  2. ArrayList支持随机访问.克隆.序列化,元素有序且可以重复.  3. ...

  2. ReentrantLock源码分析--jdk1.8

    JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分 ...

  3. Java入门系列之集合LinkedList源码分析(九)

    前言 上一节我们手写实现了单链表和双链表,本节我们来看看源码是如何实现的并且对比手动实现有哪些可优化的地方. LinkedList源码分析 通过上一节我们对双链表原理的讲解,同时我们对照如下图也可知道 ...

  4. ArrayList 和 LinkedList 源码分析

    List 表示的就是线性表,是具有相同特性的数据元素的有限序列.它主要有两种存储结构,顺序存储和链式存储,分别对应着 ArrayList 和 LinkedList 的实现,接下来以 jdk7 代码为例 ...

  5. Java集合之LinkedList源码分析

    概述 LinkedLIst和ArrayLIst一样, 都实现了List接口, 但其内部的数据结构不同, LinkedList是基于链表实现的(从名字也能看出来), 随机访问效率要比ArrayList差 ...

  6. java集合系列之LinkedList源码分析

    java集合系列之LinkedList源码分析 LinkedList数据结构简介 LinkedList底层是通过双端双向链表实现的,其基本数据结构如下,每一个节点类为Node对象,每个Node节点包含 ...

  7. Java集合基于JDK1.8的LinkedList源码分析

    上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点.本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于 ...

  8. List中的ArrayList和LinkedList源码分析

    ​ List是在面试中经常会问的一点,在我们面试中知道的仅仅是List是单列集合Collection下的一个实现类, List的实现接口又有几个,一个是ArrayList,还有一个是LinkedLis ...

  9. LinkedList 源码分析(JDK 1.8)

    1.概述 LinkedList 是 Java 集合框架中一个重要的实现,其底层采用的双向链表结构.和 ArrayList 一样,LinkedList 也支持空值和重复值.由于 LinkedList 基 ...

随机推荐

  1. Ember报错

    错误是ember-data的版本不对 解决办法是: npm install --save ember-data@2.14.2 //bing.com中去查资料,应有尽有

  2. 使用事件注册器进行swoole代码封装

    在使用swoole的时候,事件回调很难维护与编写,写起来很乱.特别在封装一些代码的时候,使用这种注册,先注册用户自己定义的,然后注册些默认的事件函数. Server.php class Server ...

  3. css之vw布局

    vw,vh是视口单位,是相对视口单位,与百分百布局不一样的是,百分百是相对于父及元素,而vw布局是相对与窗口. 而rem布局是要与js一起配合 // 以iphone6设计稿 @function px2 ...

  4. Python 爬虫从入门到进阶之路(十一)

    之前的文章我们介绍了一下 Xpath 模块,接下来我们就利用 Xpath 模块爬取<糗事百科>的糗事. 之前我们已经利用 re 模块爬取过一次糗百,我们只需要在其基础上做一些修改就可以了, ...

  5. sql-实现select取行号、分组后在分组内排序、每个分组中的前n条数据

    表结构设计: 实现select取行号 sql局部变量的2种方式 set @name='cm3333f'; select @id:=1; 区别:set 可以用=号赋值,而select 不行,必须使用:= ...

  6. 深入学习Spring框架(一)- 入门

    1.Spring是什么? Spring是一个JavaEE轻量级的一站式开发框架. JavaEE: 就是用于开发B/S的程序.(企业级) 轻量级:使用最少代码启动框架,然后根据你的需求选择,选择你喜欢的 ...

  7. C#中的委托和事件(下篇)

    上次以鸿门宴的例子写了一篇博文,旨在帮助C#初学者迈过委托和事件这道坎,能够用最快的速度掌握如何使用它们.如果觉得意犹未尽,或者仍然不知如何在实际应用中使用它们,那么,这篇窗体篇,将在Winform场 ...

  8. 解决thinkphp在开发环境下文件模块找不到的问题

    win10系统下,phpstudy开发环境下小问题描述: 找不到public公共模块. Not Found The requested URL /public/admin/login.html was ...

  9. Django rest framework(1)----认证

    目录 Django组件库之(一) APIView源码 Django restframework   (1)  ----认证 Django rest framework(2)----权限 Django ...

  10. python实现DFA模拟程序(附java实现代码)

    DFA(确定的有穷自动机) 一个确定的有穷自动机M是一个五元组: M=(K,∑,f,S,Z) K是一个有穷集,它的每个元素称为一个状态. ∑是一个有穷字母表,它的每一个元素称为一个输入符号,所以也陈∑ ...