LinkedList & ArrayDeque源码阅读

这篇具有很好参考价值的文章主要介绍了LinkedList & ArrayDeque源码阅读。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习

本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则你会不知道我在说什么

LinkedList简介

从功能上看,LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字)。关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。

从内部实现来看,LinkedList其实只是对链表的封装,没什么特别之处,下面代码分析也会非常简短。与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低,这些显然也是链表和数组的区别。

LinkedList例子

例子就不看了,都是些一眼懂的API

LinkedList继承结构

LinkedList & ArrayDeque源码阅读,java

LinkedList,作为一个顺序列表继承于AbstractSequentialList,AbstractSequentialList与AbstractList的区别是,前者一般是不可随机访问的列表,或者说随机访问效率很低,而后者可以高效地基于下标随机访问,LinkedList作为链表的封装,众所周知链表无法随机访问元素,因此继承于AbstractSequentialList。

LinkedList作为List(Sequence),每个元素都对应着一个下标,链表头指针(first)指向的节点下标为0,尾指针(last)指向的节点下标为size-1。

其中JCF相关的接口和抽象类不了解的话,可以在参考「博客园」JCF相关基础类接口/抽象类源码阅读。

LinkedList代码分析

成员变量

LinkedList的成员变量也很少,了解链表的话一眼懂:

// 元素个数
transient int size = 0;
// 链表头指针
transient Node<E> first;
// 链表尾指针
transient Node<E> last;

// 链表节点类
private static class Node<E> {
  E item;
  LinkedList.Node<E> next;
  LinkedList.Node<E> prev;

  Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
  }
}

头和尾指针指向的都是真实元素节点,没有dummy node。注意到Node有next和prev指针,说明是LinkedList维护的是一个双向链表。

方法

首先是构造函数,没啥好说,一个无参构造对应空列表,一个根据Collection接口规范的用其他集合来初始化本集合的构造函数:

public LinkedList() {}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

下面看一下各个方法,一些链表操作或比较简单的方法就不分析了,只用注释说明功能,先看一下内部方法:

// 头插新节点
private void linkFirst(E e);
// 尾插新节点
void linkLast(E e);
// 在succ节点前插入新节点
void linkBefore(E e, Node<E> succ);
// 删除头节点
private E unlinkFirst(Node<E> f);
// 删除尾节点
private E unlinkLast(Node<E> l);
// 删除节点
E unlink(Node<E> x);
// 获取下标为index的节点
LinkedList.Node<E> node(int index) {
  if (index < (size >> 1)) { // 如果节点在链表前半部分,则从first开始往后遍历
    LinkedList.Node<E> x = first;
    for (int i = 0; i < index; i++)
      x = x.next;
    return x;
  } else { // 否则从last开始往前遍历
    LinkedList.Node<E> x = last;
    for (int i = size - 1; i > index; i--)
      x = x.prev;
    return x;
  }
}

再看一下公有方法(太简单的公有方法略过)

// 按c的迭代器的顺序插入链表,最先遍历到的元素下标最小
public boolean addAll(int index, Collection<? extends E> c);
// 清空链表,不像ArrayList的置null,这个方法会删除所有节点
public void clear();

至此,实现Deque的方法已经介绍完。在JCF介绍一文中提到过继承AbstractSequentialList的类要么重写增删改查,要么重新实现列表迭代器,LinkedList两者都做了,增删改查方法很简单,略过,看一下迭代器:

private class ListItr implements ListIterator<E> {
  // 相当于AbstractList.Itr的lastRet下标对应的节点
  private Node<E> lastReturned;
  // 相当于AbstractList.Itr的cursor下标对应的节点
  private Node<E> next;
  // 相当于Abstrac
  private int nextIndex;
  // 期望的修改次数,用于检测并发修改
  private int expectedModCount = modCount;
}

可以发现LinkedList实现的这个ListIterator没什么特别的,也还是直接对链表进行操作,与AbstractList.ListIterator其实很类似,就不讲了。

java 1.6开始还提供了一个反向迭代器,一眼懂:

public Iterator<E> descendingIterator() {
  return new DescendingIterator();
}

private class DescendingIterator implements Iterator<E> {
  private final ListItr itr = new ListItr(size());
  public boolean hasNext() {
    return itr.hasPrevious();
  }
  public E next() {
    return itr.previous();
  }
  public void remove() {
    itr.remove();
  }
}

ArrayDeque简介

ArrayDeque是双端队列接口Deque的实现。出于性能上的考虑,如果你要用FIFO队列,可以优先考虑ArrayDeque而不是LinkedList;如果要使用栈,可以优先考虑ArrayDeque而不是LinkedList。ArrayDeque除了个别方法外,基本摊还时间复杂度都是O(1)。这个类的迭代器也是fail-fast的(这个概念在ArrayList那篇中讲过)。

ArrayDeque继承结构

LinkedList & ArrayDeque源码阅读,java

根据继承结构可以确定,ArrayDeque就是一个用数组实现的双端队列,并且由于是“双端”,我们可以进一步推测出应该是用循环数组来实现的。

ArrayDeque代码分析

按照惯例,先搞懂成员变量的含义:

public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, Serializable {
  // 实际存储元素的数组,没有存元素的话为null
  transient Object[] elements;
  // 循环数组的头指针,指向头元素
  transient int head;
  // 循环数组的尾指针,指向下一个要入队的尾元素
  transient int tail;
}

注意没有存元素的位置是null,因此ArrayDeque不能存值为null的元素,至于为什么,看完下面分析就知道了。

先复习一下循环数组,当调用addLast的时候,往尾部增加元素;当调用addFirst的时候往头部增加元素,画一张图帮助理解,其中数字代表元素下标,蓝色的代表该位置有元素,其他的不用多解释了:

LinkedList & ArrayDeque源码阅读,java

要注意的是,tail永远要指向null,意味着数组大小为n的话,最多只能存n-1个元素。否则如果存满数组的话,最终tail与head相等,此时无法判断是队空还是队满。

ArrayDeque没有设size变量保存元素个数,因为可以用head和tail计算得到。但是循环数组中tail可能大于head也可能小于head,因此设计一个概念:循环距离。首先tail入队时是递增的,head入队下标是递减的,因此head和tail的循环距离就是(结合图来理解):

  • 当tail>=head,为tail-head
  • 当tail<head,为tail-head+数组长度(相当于数组长度减去空白格子的个数,得到蓝色格子个数)

因此size就等于循环距离。ok,关于循环数组的原理以及元素出入队的细节都搞清楚了,那么由于循环数组是个数组(废话),总会有容量不够的时候,下一步就来分析数组的扩容。扩容的核心方法是grow:

// 最少要增加needed(ArrayList中的grow是最少增加到minCapacity)
private void grow(int needed) {
  final int oldCapacity = elements.length;
  int newCapacity;
  // 这一段是计算扩容后的长度,总体策略是:
  // 如果数组比较小(小于64)那么扩容到2倍,否则扩容到1.5倍
  int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);
  if (jump < needed
      || (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)
    newCapacity = newCapacity(needed, jump);
  // 这一段是扩容
  final Object[] es = elements = Arrays.copyOf(elements, newCapacity);
  if (tail < head || (tail == head && es[head] != null)) {
    int newSpace = newCapacity - oldCapacity;
    System.arraycopy(es, head,
                     es, head + newSpace,
                     oldCapacity - head);
    for (int i = head, to = (head += newSpace); i < to; i++)
      es[i] = null;
  }
}

grow的最后一段代码是为了处理tail<=head的情况,具体还是看图,假设从之前那张图中数组的最后一个状态开始扩容,大小+3(实际上不一定需要扩容,扩容的话也不一定是+3,这里只是为了方便说明这段代码的最后一个部分):

LinkedList & ArrayDeque源码阅读,java

由于tail < head的情况下,元素是物理不连续的(循环数组嘛),因此最后一段 if 将后面那段移动到新数组的尾部,保持循环数组中元素的连续性。

Deque和Collection的一些方法就不说了,都很简单。挑一点看着相对需要分析的函数来看一下:

// 删除从头到尾的第i个元素
boolean delete(int i) {
  final Object[] es = elements;
  final int capacity = es.length;
  final int h, t;
  // 计算第i个元素与头的循环距离
  final int front = sub(i, h = head, capacity);
  // 计算第i个元素与尾的循环距离
  final int back = sub(t = tail, i, capacity) - 1;
  // 哪个循环距离小,就移动哪部分。这里意思是,因为是循环数组,删除一个元素之后,可以把前半部分的元素整体往后移动,也可以把后半部分的元素整体往前移动,因此选择需要移动元素少的那部分。
  if (front < back) {
    if (h <= i) {
      System.arraycopy(es, h, es, h + 1, front);
    } else { // Wrap around
      System.arraycopy(es, 0, es, 1, i);
      es[0] = es[capacity - 1];
      System.arraycopy(es, h, es, h + 1, front - (i + 1));
    }
    es[h] = null;
    head = inc(h, capacity);
    return false;
  } else {
    tail = dec(t, capacity);
    if (i <= tail) {
      System.arraycopy(es, i + 1, es, i, back);
    } else {
      System.arraycopy(es, i + 1, es, i, capacity - (i + 1));
      es[capacity - 1] = es[0];
      System.arraycopy(es, 1, es, 0, t - 1);
    }
    es[tail] = null;
    return true;
  }
}

还有的函数比如bulkRemoveModified,其实跟ArrayList的removeIf很像了,也是先标记所有将要被删除的元素,然后再进行逐个删除,这种做法是为了filter进行逐元素进行判断时,保持遍历过程中列表是可重入读的,在实际进行删除时使用双指针法保持元素连续性。具体可以参考「博客园」ArrayList源码阅读。

最后看一眼迭代器吧,因为JCF前几篇都已经详细介绍过迭代器,ArrayDeque的迭代器实现也没什么特别的,就不展开讲了:

private class DeqIterator implements Iterator<E> {
  int cursor;
  int remaining = size();
  int lastRet = -1;
}

总结

类似ArrayList是对数组的封装,LinkedList是对数组的封装,代码看起来也比较简单,适合编程新手学习一下“封装”的概念。(又水一篇)。

java 1.6新出了一个ArrayDeque,出于性能上的考虑,如果你要用FIFO队列,可以优先考虑ArrayDeque而不是LinkedList;如果要使用栈,可以优先考虑ArrayDeque而不是LinkedList。

参考链接

「博客园」JCF相关基础类接口/抽象类源码阅读

「博客园」ArrayList源码阅读

「Java全栈知识体系」Collection - LinkedList源码解析文章来源地址https://www.toymoban.com/news/detail-793864.html

到了这里,关于LinkedList & ArrayDeque源码阅读的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java进阶(7)——手动实现LinkedList & 内部node类的实现 & 增删改查的实现 & toString方法 & 源码的初步理解

    1.linkedList的节点,当前,上一个,下一个的思想; 2.根据index找node的方法,根据index确定从头部还是尾部; 3.linkedlist的增删改查的实现,本质是改变节点的信息; 4.递归方法实现自定义链表的toString方法; Java进阶(3)——手动实现ArrayList 源码的初步理解分析 数组插入数据和

    2024年02月11日
    浏览(36)
  • 源码分析——LinkedList源码分析

    LinkedList 是一个实现了 List接口 和 Deque接口 的 双端链表 。 LinkedList底层的链表结构使它 支持高效的插入和删除操作 ,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList 不是线程安全的 ,如果想使LinkedList变成线程安全的,可以调用静态类 Collections类 中的 s

    2024年02月14日
    浏览(24)
  • LinkedList部分底层源码分析

    JDK版本为1.8.0_271,以插入和删除元素为例,LinkedList部分源码如下: 插入删除结点的过程如图所示: 只有1个元素的LinkedList 包含4个元素的LinkedList add(E e)方法 add(int index,E e)方法 remove(Object obj)方法 remove(int index)方法

    2024年04月13日
    浏览(25)
  • Java数据结构学习和源码阅读(线性数据结构)

    链表的数据结构 一组由节点组成的数据结构,每个元素指向下一个元素,是线性序列。 最简单的链表结构: 数据 指针(存放执行下一个节点的指针) 不适合的场景: 需要循环遍历将导致时间复杂度的提升 链表分类—单向链表 链表结构: 数据 指针 Next(指向下一个节点)

    2024年02月12日
    浏览(36)
  • 【数据结构与算法】4、双向链表(学习 jdk 的 LinkedList 部分源码)

    🎁 单链表的节点中只有一个 next 指针引用着下一个节点的地址 🎁 当要获取单链表中的最后一个元素的时候,需要从头节点开始遍历到最后 🎁 单链表一开始的时候有 first 头指针引用着头节点的地址 💰 双向链表可以提升链表的综合性能 💰 双向链表的节点中有 prev 指针引

    2024年02月12日
    浏览(34)
  • dubbo源码阅读之-java spi, dubbo spi 和 Spring spi 到底有啥区别

    SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

    2024年02月08日
    浏览(33)
  • 《手把手教你》系列技巧篇(六)-java+ selenium自动化测试-阅读selenium源码(详细教程)

    1.简介 前面几篇基础系列文章,足够你迈进了Selenium门槛,再不济你也至少知道如何写你第一个基于Java的Selenium自动化测试脚本。接下来宏哥介绍Selenium技巧篇,主要是介绍一些常用的Selenium方法或者接口(API),通过这些接口(API)或者方法的具体操作,达到能够熟练使用

    2024年02月03日
    浏览(42)
  • 【Java】LinkedList 集合

    LinkedList集合特点 LinkedList 底层基于双向链表实现增删 效率非常高,查询效率非常低。 LinkedList源码解读分析 LinkedList 是双向链表实现的 List LinkedList 是非线程安全的(线程是不安全的) LinkedList 元素允许为null,允许重复元素 LinkedList 是基于链表是实现的,因此插入删除效率高

    2024年02月07日
    浏览(32)
  • Java集合之LinkedList

    目录 基本介绍  常用方法 源码解析 1. LinkedList的底层结构,Node双向链表 2. LinkedList的几个内部变量 3. getFirst() 4. removeFirst() 5. addFirst(E e) 6. contains(Object o) 7. add(E e) 8. remove(Object o) 9. addAll(int index, Collection c) 10. get(int index) 11. spliterator() 总结 LinkedList是实现了List和Deque接口的双

    2024年02月15日
    浏览(29)
  • 详解Java LinkedList

    LinkedList是List接口的实现类,基于双向链表实现,继承自AbstractSequentialList类,同时也实现了Cloneable、Serializable接口。此外还实现了Queue和Deque接口,可以作为队列或双端队列使用。 LinkedList的插入删除时间复杂度: 在头部或尾部插入删除元素,只需要修改头节点或尾节点的指针

    2024年02月06日
    浏览(27)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包