前言

在上篇文章中我们对ArrayList对了详细的分析,今天我们来说一说LinkedList。他们之间有什么区别呢?最大的区别就是底层数据结构的实现不一样,ArrayList是数组实现的(具体看上一篇文章),LinedList是链表实现的。至于其他的一些区别,可以说大部分都是由于本质不同衍生出来的不同应用。

LinkedList

链表

在分析LinedList之前先对链表做一个简单的介绍,毕竟链表不像数组一样使用的多,所以很多人不熟悉也在所难免。

链表是一种基本的线性数据结构,其和数组同为线性,但是数组是内存的物理存储上呈线性,逻辑上也是线性;而链表只是在逻辑上呈线性。在链表的每一个存储单元中不仅存储有当前的元素,还有下一个存储单元的地址,这样的可以通过地址将所有的存储单元连接在一起。

每次查找的时候,通过第一个存储单元就可以顺藤摸瓜的找到需要的元素。执行删除操作只需要断开相关元素的指向就可以了。示意图如下:

 
2018-01-10_114030
 
2018-01-10_114053
 
2018-01-10_114109

当然了在?LinkedList中使用的并不是最基本的单向链表,而是双向链表。

在LinedList中存在一个基本存储单元,是LinkedList的一个内部类,节点元素存在两个属性,分别保存前一个节点和后一个节点的引用。

  1. //静态内部类
  2. private static class Node<E> {
  3. //存储元素的属性
  4. E item;
  5. //前后节点引用
  6. Node<E> next;
  7. Node<E> prev;
  8. Node(Node<E> prev, E element, Node<E> next) {
  9. this.item = element;
  10. this.next = next;
  11. this.prev = prev;
  12. }
  13. }

定义

  1. public class LinkedList<E>
  2. extends AbstractSequentialList<E>
  3. implements List<E>, Deque<E>, Cloneable, java.io.Serializable

在定义上和ArrayList大差不差,但是需要注意的是,LinkedList实现了Deque(间接实现了Qeque接口),Deque是一个双向对列,为LinedList提供了从对列两端访问元素的方法。

初始化

在分析ArrayList的时候我们知道ArrayList使用无参构造方法时的初始化长度是10,并且所有无参构造出来的集合都会指向同一个对象数组(静态常量,位于方法区),那么LinkedList的初始化是怎样的呢?

打开无参构造方法

  1. public LinkedList() {
  2. }

什么都没有,那么只能够去看属性了。

  1. //初始化长度为0
  2. transient int size = 0;
  3. //有前后节点
  4. transient Node<E> first;
  5. transient Node<E> last;

图示初始化

  1. LinkedList<String> list = new LinkedList<String>();
  2. String s = "sss";
  3. list.add(s);
 
 

方法

add(E e)
  1. public boolean add(E e) {
  2. linkLast(e);
  3. return true;
  4. }

从方法中我们知道在调用添加方法之后,并不是立马添加的,而是调用了linkLast方法,见名知意,新元素的添加位置是集合最后。

  1. void linkLast(E e) {
  2. // 将最后一个元素赋值(引用传递)给节点l final修饰符 修饰的属性赋值之后不能被改变
  3. final Node<E> l = last;
  4. // 调用节点的有参构造方法创建新节点 保存添加的元素
  5. final Node<E> newNode = new Node<>(l, e, null);
  6. //此时新节点是最后一位元素 将新节点赋值给last
  7. last = newNode;
  8. //如果l是null 意味着这是第一次添加元素 那么将first赋值为新节点 这个list只有一个元素 存储元素 开始元素和最后元素均是同一个元素
  9. if (l == null)
  10. first = newNode;
  11. else
  12. //如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next
  13. l.next = newNode;
  14. //长度+1
  15. size++;
  16. //修改次数+1
  17. modCount++;
  18. }

从以上代码中我们可以看到其在添加元素的时候并不依赖下标。

而其中的处理是,通过一个last(Node对象)保存最后一个节点的信息(实际上就是最后一个节点),每次通过不断的变化最后一个元素实现元素的添加。(想要充分理解此处,需要理解java值传递和引用传递的区别和本质)。

add(int index, E element)
  1. 添加到指定的位置
  2.  
  3. public void add(int index, E element) {
  4. //下标越界检查
  5. checkPositionIndex(index);
  6. //如果是向最后添加 直接调用linkLast
  7. if (index == size)
  8. linkLast(element);
  9. //反之 调用linkBefore
  10. else
  11. linkBefore(element, node(index));
  12. }
  13. //在指定元素之前插入元素
  14. void linkBefore(E e, Node<E> succ) {
  15. // assert succ != null; 假设断言 succ不为null
  16. //定义一个节点元素保存succ的prev引用 也就是它的前一节点信息
  17. final Node<E> pred = succ.prev;
  18. //创建新节点 节点元素为要插入的元素e prev引用就是pred 也就是插入之前succ的前一个元素 next是succ
  19. final Node<E> newNode = new Node<>(pred, e, succ);
  20. //此时succ的上一个节点是插入的新节点 因此修改节点指向
  21. succ.prev = newNode;
  22. // 如果pred是null 表明这是第一个元素
  23. if (pred == null)
  24. //成员属性first指向新节点
  25. first = newNode;
  26. //反之
  27. else
  28. //节点前元素的next属性指向新节点
  29. pred.next = newNode;
  30. //长度+1
  31. size++;
  32. modCount++;
  33. }

节点元素插入图示

 
 
 
 

在上面的代码中我们应该注意到了,LinkedList在插入元素的时候也要进行一定的验证,也就是下标越界验证,下面我们看一下具体的实现。

  1. private void checkPositionIndex(int index) {
  2. if (!isPositionIndex(index))
  3. throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
  4. }
  5. //如果输入的index在范围之内返回ture
  6. private boolean isPositionIndex(int index) {
  7. return index >= 0 && index <= size;
  8. }

通过对两个添加方法的分析,我们可以很明显的感受到LinkedList添加元素的效率,不需要扩容,不需要复制数组。

get
  1. public E get(int index) {
  2. //检查下标元素是否存在 实际上就是检查下标是否越界
  3. checkElementIndex(index);
  4. //如果没有越界就返回对应下标节点的item 也就是对应的元素
  5. return node(index).item;
  6. }
  7.  
  8. //下标越界检查 如果越界就抛异常
  9. private void checkElementIndex(int index) {
  10. if (!isElementIndex(index))
  11. throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
  12. }
  13. private boolean isElementIndex(int index) {
  14. return index >= 0 && index < size;
  15. }
  16. //该方法是用来返回指定下标的非空节点
  17. Node<E> node(int index) {
  18. //假设下标未越界 实际上也没有越界 毕竟在此之前执行了下标越界检查
  19. // assert isElementIndex(index);
  20.  
  21. //如果index小于size的二分之一 从前开始查找(向后查找) 反之向前查找
  22. if (index < (size >> 1)) {//左移 效率高 值得学习
  23. Node<E> x = first;
  24. //遍历
  25. for (int i = 0; i < index; i++)
  26. //每一个节点的next都是他的后一个节点引用 遍历的同时x会不断的被赋值为节点的下一个元素 遍历到index是拿到的就是index对应节点的元素
  27. x = x.next;
  28. return x;
  29. } else {
  30. Node<E> x = last;
  31. for (int i = size - 1; i > index; i--)
  32. x = x.prev;
  33. return x;
  34. }
  35. }

在这段代码中充分体现了双向链表的优越性,可以从前也可以从后开始遍历,通过对index范围的判断能够显著的提高效率。但是在遍历的时候也可以很明显的看到LinkedList get方法获取元素的低效率,时间复杂度O(n)。

remove(int index)

所谓删除节点 就是把节点的前后引用置为null,并且保证没有任何其他节点指向被删除节点。

  1. public E remove(int index) {
  2. //下标越界检查
  3. checkElementIndex(index);
  4. //此处的返回值实际上是执行了两个方法
  5. //node获取制定下标非空节点
  6. //unlink 断开指定节点的联系
  7. return unlink(node(index));
  8. }
  9. E unlink(Node<E> x) {
  10. //假设x不是null
  11. // assert x != null;
  12. //定义一个变量element接受x节点中的元素 最后会最后返回值返回
  13. final E element = x.item;
  14. //定义连个节点分别获得x节点的前后节点引用
  15. final Node<E> next = x.next;
  16. final Node<E> prev = x.prev;
  17. //如果节点前引用为null 说明这是第一个节点
  18. if (prev == null) {
  19. //x是第一个节点 即将被删除 那么first需要被重新赋值
  20. first = next;
  21. } else {
  22. //如果不是x不是第一个节点 将prev(x的前一个节点)的next指向x的后一个节点(绕过x)
  23. prev.next = next;
  24. //x的前引用赋值null
  25. x.prev = null;
  26. }
  27. //如果节点后引用为null 说明这是最后一个节点 一系列类似前引用的处理方式 不再赘述
  28. if (next == null) {
  29. last = prev;
  30. } else {
  31. next.prev = prev;
  32. x.next = null;
  33. }
  34. //将x节点中的元素赋值null
  35. x.item = null;
  36. size--;
  37. modCount++;
  38. return element;
  39. }

说明

  1. prev,item,next均置为null 是为了让虚拟机回收
  2. 我们可以看到LinkedList删除元素的效率也不错
LinkedList总结
  1. 查询速度不行,每次查找都需要遍历,这就是在ArrayList分析时提到过的顺序下标遍历
  2. 添加元素,删除都很有速度优势
  3. 实现对列接口

ArrayList和LinkedList的区别

  1. 顺序插入,两者速度都很快,但是ArrayList稍快于LinkedList,数组实现,数组是提前创建好的;LinkedList每次都需要重新new新节点
  2. LinedList需要维护前后节点,会更耗费内存
  3. 遍历,LinedList适合用迭代遍历;ArrayList适合用循环遍历
    1. 不要使用普通for循环遍历LinedList
    2. 也不要使用迭代遍历遍历ArrayList(具体看上篇文章《ArrayList分析》)
  4. 删除和插入就不说了,毕竟ArrayList需要复制数组和扩容。

我不能保证每一个地方都是对的,但是可以保证每一句话,每一行代码都是经过推敲和斟酌的。希望每一篇文章背后都是自己追求纯粹技术人生的态度。

永远相信美好的事情即将发生。

Java集合干货——LinkedList源码分析的更多相关文章

  1. 死磕 java集合之LinkedList源码分析

    问题 (1)LinkedList只是一个List吗? (2)LinkedList还有其它什么特性吗? (3)LinkedList为啥经常拿出来跟ArrayList比较? (4)我为什么把LinkedL ...

  2. Java集合之LinkedList源码分析

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

  3. Java集合干货——ArrayList源码分析

    ArrayList源码分析 前言 在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然.关于ArrayList的具体 ...

  4. Java 集合之LinkedList源码分析

    1.介绍 链表是数据结构中一种很重要的数据结构,一个链表含有一个或者多个节点,每个节点处理保存自己的信息之外还需要保存上一个节点以及下一个节点的指针信息.通过链表的表头就可以访问整个链表的信息.Jav ...

  5. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  6. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  7. 死磕 java集合之PriorityQueue源码分析

    问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...

  8. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

  9. 死磕 java集合之LinkedHashSet源码分析

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

随机推荐

  1. CentOS 6.5 安装MySQL过程

    使用软件的版本 CentOS 6.5 mysql-5.5.22.tar.gz cmake-2.8.6.tar.gz 准备工作 解压安装mysql之前把关于mysql软件包卸载,以免程序冲突,端口冲突. ...

  2. iBATIS使用$和#的一些理解

    我们在使用iBATIS时会经常用到#这个符号. 比如: sql 代码 select * from member where id =#id# 然后,我们会在程序中给id这个变量传递一个值,iBATIS ...

  3. eclipse中JPA插件的安装与使用

    说明 上周实验室学习了数据库设计相关的内容,其中涉及到将数据库实体化的问题,JPA是一个很好的实现工具,便开始着手于JPA的学习.因为JPA涉及到的知识还是挺多的,需要学习许多新的知识,所以对于JPA ...

  4. oracle如何连接别人的数据库,需要在本地添加一些配置

    2.oracle如何连接别人的数据库,需要在本地添加一些配置 1.找到 listener.ora 文件,打开(一般在 C 文件夹) ORCL = (DESCRIPTION = (ADDRESS = ( ...

  5. gcc编译相关tips

    http://blog.csdn.net/benpaobagzb/article/details/51364005 静态库链接时搜索路径顺序: ld会去找GCC命令中的参数-L 再找gcc的环境变量L ...

  6. ansible服务及剧本编写

    第1章 ansible软件概念说明 python语言是运维人员必会的语言,而ansible是一个基于Python开发的自动化运维工具 (saltstack).其功能实现基于SSH远程连接服务:ansi ...

  7. Spring(概念)

    在本文中只讲述一些概念性的东西,因为我在开始学习JAVA的时候对这些概念性的东西总是不太理解,总结总结再感悟一下,也方便后人. 理解的不深,用通俗的语言讲一下: 百度百科这样介绍: spring框架主 ...

  8. ABP 用swagger UI测试API报401无权限访问问题

    问题描述: 当我们用swagger UI对Web API 进行测试时报401错误 我们点开GET /api/services/app/Role/GetAll,输入参数 点击Try it out!按钮, ...

  9. ReactNative实现图集功能

    需求描述: 图片缩放.拖动.长按保存等基础图片查看的功能: 展示每张图片文本描述: 实现效果,如图: 实现步骤 使用第三方插件:react-native-image-zoom-viewer 插件Git ...

  10. 用js筛选数据排序

    题目 参考以下示例代码,页面加载后,将提供的空气质量数据数组,按照某种逻辑(比如空气质量大于60)进行过滤筛选,最后将符合条件的数据按照一定的格式要求显示 <!DOCTYPE html> ...