资讯专栏INFORMATION COLUMN

Java集合干货——LinkedList源码分析

jsdt / 3434人阅读

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

前言

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

LinkedList 链表

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

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

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

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

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

</>复制代码

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

</>复制代码

  1. public class LinkedList
  2. extends AbstractSequentialList
  3. implements List, Deque, 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 first;
  5. transient Node last;

图示初始化

</>复制代码

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

节点元素插入图示

在上面的代码中我们应该注意到了,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. private void checkElementIndex(int index) {
  9. if (!isElementIndex(index))
  10. throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
  11. }
  12. private boolean isElementIndex(int index) {
  13. return index >= 0 && index < size;
  14. }

</>复制代码

  1. //该方法是用来返回指定下标的非空节点
  2. Node node(int index) {
  3. //假设下标未越界 实际上也没有越界 毕竟在此之前执行了下标越界检查
  4. // assert isElementIndex(index);
  5. //如果index小于size的二分之一 从前开始查找(向后查找) 反之向前查找
  6. if (index < (size >> 1)) {//左移 效率高 值得学习
  7. Node x = first;
  8. //遍历
  9. for (int i = 0; i < index; i++)
  10. //每一个节点的next都是他的后一个节点引用 遍历的同时x会不断的被赋值为节点的下一个元素 遍历到index是拿到的就是index对应节点的元素
  11. x = x.next;
  12. return x;
  13. } else {
  14. Node x = last;
  15. for (int i = size - 1; i > index; i--)
  16. x = x.prev;
  17. return x;
  18. }
  19. }

在这段代码中充分体现了双向链表的优越性,可以从前也可以从后开始遍历,通过对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. }

</>复制代码

  1. E unlink(Node x) {
  2. //假设x不是null
  3. // assert x != null;
  4. //定义一个变量element接受x节点中的元素 最后会最后返回值返回
  5. final E element = x.item;
  6. //定义连个节点分别获得x节点的前后节点引用
  7. final Node next = x.next;
  8. final Node prev = x.prev;
  9. //如果节点前引用为null 说明这是第一个节点
  10. if (prev == null) {
  11. //x是第一个节点 即将被删除 那么first需要被重新赋值
  12. first = next;
  13. } else {
  14. //如果不是x不是第一个节点 将prev(x的前一个节点)的next指向x的后一个节点(绕过x)
  15. prev.next = next;
  16. //x的前引用赋值null
  17. x.prev = null;
  18. }
  19. //如果节点后引用为null 说明这是最后一个节点 一系列类似前引用的处理方式 不再赘述
  20. if (next == null) {
  21. last = prev;
  22. } else {
  23. next.prev = prev;
  24. x.next = null;
  25. }
  26. //将x节点中的元素赋值null
  27. x.item = null;
  28. size--;
  29. modCount++;
  30. return element;
  31. }

说明

prev,item,next均置为null 是为了让虚拟机回收

我们可以看到LinkedList删除元素的效率也不错

LinkedList总结

查询速度不行,每次查找都需要遍历,这就是在ArrayList分析时提到过的顺序下标遍历

添加元素,删除都很有速度优势

实现对列接口

ArrayList和LinkedList的区别

顺序插入,两者速度都很快,但是ArrayList稍快于LinkedList,数组实现,数组是提前创建好的;LinkedList每次都需要重新new新节点

LinedList需要维护前后节点,会更耗费内存

遍历,LinedList适合用迭代遍历;ArrayList适合用循环遍历

不要使用普通for循环遍历LinedList

也不要使用迭代遍历遍历ArrayList(具体看上篇文章《ArrayList分析》)

删除和插入就不说了,毕竟ArrayList需要复制数组和扩容。

</>复制代码

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68264.html

相关文章

  • Java集合干货——ArrayList源码分析

    摘要:关于的具体实现,一些基本的都也知道,譬如数组实现,线程不安全等等,但是更加具体的就很少去了解了,例如初始化的长度,扩容等。 前言 在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然。关于ArrayList的具体实现,一些基本的都也知道,譬如数组实现,线程不安全等等,但是更加具体的就很少去了解了,例如:...

    Render 评论0 收藏0
  • Java集合干货——ArrayList源码分析

    摘要:关于的具体实现,一些基本的都也知道,譬如数组实现,线程不安全等等,但是更加具体的就很少去了解了,例如初始化的长度,扩容等。 前言 在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然。关于ArrayList的具体实现,一些基本的都也知道,譬如数组实现,线程不安全等等,但是更加具体的就很少去了解了,例如:...

    wupengyu 评论0 收藏0
  • Java 常用List集合使用场景分析

    摘要:常用集合使用场景分析过年前的最后一篇,本章通过介绍,,,底层实现原理和四个集合的区别。和都是线程安全的,不同的是前者使用类,后者使用关键字。面试官会认为你是一个基础扎实,内功深厚的人才到这里常用集合使用场景分析就结束了。 Java 常用List集合使用场景分析 过年前的最后一篇,本章通过介绍ArrayList,LinkedList,Vector,CopyOnWriteArrayList...

    godruoyi 评论0 收藏0
  • [学习笔记-Java集合-2] List - LinkedList源码分析

    摘要:删除元素作为双端队列,删除元素也有两种方式,一种是队列首删除元素,一种是队列尾删除元素。作为,又要支持中间删除元素,所以删除元素一个有三个方法,分别如下。在中间删除元素比较低效,首先要找到删除位置的节点,再修改前后指针,时间复杂度为。 介绍 LinkedList是一个以双向链表实现的List,它除了作为List使用,还可以作为队列或者栈来使用,它是怎么实现的呢?让我们一起来学习吧。 继...

    novo 评论0 收藏0
  • 类的加载机制 - 收藏集 - 掘金

    摘要:是现在广泛流行的代从开始学习系列之向提交代码掘金读完本文大概需要分钟。为了进行高效的垃圾回收,虚拟机把堆内存划分成新生代老年代和永久代中无永久代,使用实现三块区域。 React Native 开源项目 - 仿美团客户端 (Android、iOS 双适配) - Android - 掘金推荐 React Native 学习好项目,仿照美团客户端... 极简 GitHub 上手教程 - 工具...

    Gilbertat 评论0 收藏0

发表评论

0条评论

jsdt

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<