【源码解析】你真的了解ArrayDeque嘛?

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 上篇文章说LinkedList也可以实现队列和栈的功能,但是我们一般要用队列功能的话推荐使用ArrayDeque,因为他底层是数组,而队列和栈都是只要操作头部或尾部,所以这样的话数组的性能就比链表快一点。LinkedList和ArrayDeque都是通过实现了Deque这个接口来获得队列和栈的功能。而Deque这个接口通过继承Queue这个接口来取得队列功能,然后在这个基础进行扩展,实现了双端队列,由此可以获得栈的功能。为了空间能得到充分利用,ArrayDeque使用了循环队列;还有LinkedList可以插入null值,而ArrayDeque是不能插入null的。

积千里跬步,汇万里江河;每天进步一点点,终有一天将成大佬

前言


上篇文章说LinkedList也可以实现队列的功能,但是我们一般要用队列功能的话推荐使用ArrayDeque,因为他底层是数组,而队列和栈都是只要操作头部或尾部,所以这样的话数组的性能就比链表快一点。

LinkedListArrayDeque都是通过实现了Deque这个接口来获得队列的功能。而Deque这个接口通过继承Queue这个接口来取得队列功能,然后在这个基础进行扩展,实现了双端队列,由此可以获得的功能。为了空间能得到充分利用,ArrayDeque使用了循环队列;还有LinkedList可以插入null值,而ArrayDeque是不能插入null的。

什么是双端队列?

简单来说,就是两端都可以操作的队列(🌚说了和没说一样…)。哈哈,还是看图吧

一般队列是这样的:

双端队列是这样的

总的来说,普通队列只可在头部删除元素和尾部添加元素,而双端队列头部和尾部都可以添加和删除元素

什么是循环队列?

不如说你定了个5容量大小的数组,你第一次插入的位置是下标为2,当你添加第4个元素的时候,他不会进行扩容,而是通过头尾指针进行对比,然后把数据插入到下标为0的位置上。当头尾指针相等时,表示这个队列数组已经满了,这时才会扩容。

这里的数组从上向下的顺序,有人会问为什么头尾指针都指向第三个方格呢?因为这边演示的是第一个元素插入到下标为2的位置嘛。。当然,ArrayDeque是从0开始的,所以初始化时头尾指针都是指向下标为0的位置上。

Deque有什么?

话不多说,看图:

ArrayDeque具体实现的方法主要在蓝色的方框里,其他两个颜色的方框都是通过调用蓝色方框里的这些方法来实现相关功能的,可以再看一张我画的脑图:

这边队列的每种功能都有两个方法,其中add()remove()element()如果操作失败会报异常offer()poll()peek()操作失败会返回null或者false

其实真正用到的就深红色方框里写的这些方法,所以本文我就说这四个方法,addLast()pollFirstgetFirst()addFirst()peekFirst

内部变量

ArrayDeque内部就只有4个变量,对象数组element[]头指针head尾指针tailMIN_INITIAL_CAPACITY表示最小初始化容量为8

构造方法

构造方法和其他集合一样,有有参构造无参构造

无参构造

很简单,直接初始化一个容量为16的对象数组

public ArrayDeque() {
    elements = new Object[16];
}

有参构造

传入参数为int数

public ArrayDeque(int numElements) {
    allocateElements(numElements);
}
  • allocateElements(int numElements)分配空数组以容纳给定数量的元素。
private void allocateElements(int numElements) {
    elements = new Object[calculateSize(numElements)];
}
  • calculateSize(int numElements)调整传入的值大小

上面的算法中用到了位运算,如果不了解位运算的话,可以看位运算这篇文章。这里把数值设置成2的n次方(是整数次),是为了满足下面要说的循环队列这个算法

传入的参数为集合对象

public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

第一步调用了和上面一样的方法,这里多了个addAll()方法

  • addAll(Collection c)

这边复制时并没有用和ArrayList一样的System.arraycopy()方法,而是采用for循环来调用add()方法进行一个一个添加的;为什么这么做呢?因为ArrayDeque和其他集合不一样,它里面是不能有null值的,而其他集合里面有的是可以传null的,所以这边采用add()一个一个的加,add()方法如果传入的值为空的话,就会报异常;(add()实际调用的是addLast(),下面再讲)

addLast()

源码解析

这个方法的意思是添加数据到尾部,下面图片方框中的位与算法是实现循环队列这个功能的核心算法

还记得上面初始化时候,不管传入的是什么数值,最后出来的都是2n(整数次)方。这个算法就是&右边为2n1时,&左边的数为正整数时,不管有多大,最后的结果永远<=2n;当&左边的数为0时,结果为0;当&左边的数为负数时,-1=2n1

举一些例子:当2n=82n1=7

  • 4&7=4 9&7=1 22&7=6
  • 0&7=0
  • -1&7=7 -2&7=6 -8&7=0

  • doubleCapacity()扩容为原来的2倍

流程图

方便理解,我画下上扩容的流程图,比如head在中间:

pollFirst()

移除头部数据

源码解析

删除的时候并没有像ArrayList一样移动数据,而只是移动了head指向的位置

流程图

getFirst()和peekFirst()

这两个方法都是一样的,都是直接返回head指向的数据,区别就是一个会抛异常,一个不会

源码分析

  • getFirst()
public E getFirst() {
    @SuppressWarnings("unchecked")
    E result = (E) elements[head];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}
  • peekFirst()
public E peekFirst() {
    // elements[head] is null if deque empty
    return (E) elements[head];
}

addFirst()

源码解析

这里还是用了上面讲了位与算法,算出head的值,然后插入数据

流程图

clear()

源码解析

清空这个操作是从head指向的元素开始删除,直到head=tail,清空完成;

size()

这个获取队列的大小也是用了上面讲的位与算法,用尾部减去了头部,然后位与数组的长度-1。为什么要这么弄呢?直接向ArrayListLinkedList一样定义个size不好嘛?你不觉得这样更方便吗?少了一个变量,就少维护了一个变量,就少了一个安全隐患啊

public int size() {
    return (tail - head) & (elements.length - 1);
}

总结


上面的方法基本上有位与这个算法的身影,可见这个是核心了;如果不了解位运算的话,可以看位运算这篇文章;

核心算法:

&右边为2n1时,&左边的数为正整数时,不管有多大,最后的结果永远<=2n;当&左边的数为0时,结果为0;当&左边的数为负数时,-1=2n1

ArrayDeque无参构造方法是直接初始化一个容量为16的空数组,而上篇ArrayList文章里,它无参构造方法是初始化了一个空数组,在第一次添加数据的时候才进行扩容到10;

ArrayDeque每次扩容为原来数组长度的2倍

ArrayDeque不能插入null

目录
相关文章
|
10天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
35 2
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0
|
1月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
62 0
|
10天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
23天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
40 3
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
56 5
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
113 5
|
1月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
1月前
|
算法 Java 程序员
Map - TreeSet & TreeMap 源码解析
Map - TreeSet & TreeMap 源码解析
34 0

推荐镜像

更多
下一篇
无影云桌面