ArrayDeque 源码解析(JDK1.8)

目录

一. 前言

二. 源码解析

2.1. 概览

2.2. 属性

2.3. 构造方法

2.4. 入队

2.4.1. addFirst(E, e)

2.4.2. add(E e) & addLast(E e)

2.4.3. offer(E e)

2.5. 扩容

2.6. 出队

2.6.1. poll() & pollFirst()

2.6.2. pollLast()

2.7. 删除元素

2.8. 获取元素

2.9. 栈操作


一. 前言

    ArrayDeque 和
LinkedList 是Deque的两个通用实现,由于官方更推荐使用ArrayDeque用作栈和队列,由于作者已经讲解过 LinkedList,本文将着重讲解ArrayDeque的具体实现。

    双端队列是一种特殊的队列,它的两端都可以进出元素,故而得名双端队列。ArrayDeque 是一种以数组方式实现的双端队列,它是非线程安全的。由其名字可以看出,其是一个由数组实现的双端队列,对比 LinkedList 是由链表实现的双端队列。

    从名字可以看出ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要程序员手动同步;另外,该容器不允许放入null元素。

上图中我们看到,head指向首端第一个有效元素,tail指向尾端第一个可以插入元素的空位。因为是循环数组,所以head不一定总等于0,tail也不一定总是比head大。

二. 源码解析

2.1. 概览

通过继承体系可以看到,ArrayDeque 实现了 Deque 接口,Deque 接口继承自 Queue 接口,它是对 Queue 的一种增强。

public interface Deque<E> extends Queue<E> {// 添加元素到队列头void addFirst(E e);// 添加元素到队列尾void addLast(E e);// 添加元素到队列头boolean offerFirst(E e);// 添加元素到队列尾boolean offerLast(E e);// 从队列头移除元素E removeFirst();// 从队列尾移除元素E removeLast();// 从队列头移除元素E pollFirst();// 从队列尾移除元素E pollLast();// 查看队列头元素E getFirst();// 查看队列尾元素E getLast();// 查看队列头元素E peekFirst();// 查看队列尾元素E peekLast();// 从队列头向后遍历移除指定元素boolean removeFirstOccurrence(Object o);// 从队列尾向前遍历移除指定元素boolean removeLastOccurrence(Object o);/** 队列中的方法*/  // 添加元素,等于addLast(e)boolean add(E e);// 添加元素,等于offerLast(e)boolean offer(E e);// 移除元素,等于removeFirst()E remove();// 移除元素,等于pollFirst()E poll();// 查看元素,等于getFirst()E element();// 查看元素,等于peekFirst()E peek();/** 栈方法*/  // 入栈,等于addFirst(e)void push(E e);// 出栈,等于removeFirst()E pop();/** Collection中的方法*/  // 删除指定元素,等于removeFirstOccurrence(o)boolean remove(Object o);// 检查是否包含某个元素boolean contains(Object o);// 元素个数public int size();// 迭代器Iterator<E> iterator();// 反向迭代器Iterator<E> descendingIterator();
}

Deque 中新增了以下几类方法:
    *First,表示从队列头操作元素;
    *Last,表示从队列尾操作元素;
    push(e),pop(),以栈的方式操作元素的方法。

2.2. 属性

// 存储元素的数组
transient Object[] elements;// 头指针
transient int head;// 尾指针
transient int tail;// 默认最小容量(注意:elements的长度一定是2的次方幂)
private static final int MIN_INITIAL_CAPACITY = 8;

从属性我们可以看到,ArrayDeque 使用数组存储元素,并使用头尾指针标识队列的头和尾,其最小容量是 8。

ArrayDeque底层是使用数组实现的,而且数组的长度必须是2的整数次幂,这么操作的原因是为了后面位运算好操作。在ArrayDeque当中有两个整形变量head和tail,分别指向右侧的第一个进入队列的数据和左侧第一个进入队列的数据,整个内存布局如下图所示:

其中 tail 指的位置没有数据,head 指的位置存在数据。

2.3. 构造方法

1. 调用无参构造器时,默认创建一个长度为16的数组。
2. 调用传入初始容量 n 的构造器,当 n 小于 8 时,会初始化一个长度为 8 的一个数组。
3. 当 n 大于等于 8 时,会初始化一个长度为 大于n的最小的2的幂 的数组(比如传入 3 算出来是 8,传入 9 算出来是 16,传入 16 算出来是 32)。

通过构造方法,我们知道默认初始容量是 16,最小容量是 8。

/** 空参构造器,底层初始化一个长度为16的数组*/
public ArrayDeque() {elements = new Object[16];
}/** 指定元素个数初始化* 传入初始容量,注意最终的容量是大于(没有等于)numElements的最大的2的幂* 然后会创建出来。*/
public ArrayDeque(int numElements) {allocateElements(numElements);
}/* * 传入一个集合,将集合c中的元素初始化到数组中* 创建一个长度为<小于等于c.size的最大的2的幂>的数组* 然后将c中的元素添加到elements中。*/
public ArrayDeque(Collection<? extends E> c) {allocateElements(c.size());addAll(c);
}
// 构造一个长度为 严格大于numElements的最小的2的幂 的一个数组
private void allocateElements(int numElements) {elements = new Object[calculateSize(numElements)];
}// 返回严格大于numElements的最小的2的幂 (当numElements小于8时,返回8)
private static int calculateSize(int numElements) {// MIN_INITIAL_CAPACITY = 8int initialCapacity = MIN_INITIAL_CAPACITY;// 当numElements大于等于8时,计算出大于numElements的最小的2的幂if (numElements >= initialCapacity) {initialCapacity = numElements;initialCapacity |= (initialCapacity >>>  1);initialCapacity |= (initialCapacity >>>  2);initialCapacity |= (initialCapacity >>>  4);initialCapacity |= (initialCapacity >>>  8);initialCapacity |= (initialCapacity >>> 16);initialCapacity++;// 条件成立:说明爆int了,需要缩小数据,将initialCapacity无符号右移一位,相当于/2if (initialCapacity < 0)   // Too many elements, must back offinitialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements}// 这里如果numElements小于8时,直接返回8return initialCapacity;
}

2.4. 入队

2.4.1. addFirst(E, e)

// 从队头入队
public void addFirst(E e) {// 不允许null元素if (e == null)throw new NullPointerException();/** 将head指针减1并与数组长度减1取模* 因为element.length一定是2的幂,2的幂-1的二进制从低位起是一串1,高位都是0* 初始时head = 0,0 - 1 = -1 ,-1 & 15 = 15,此时head = 15*      下一次 15 - 1 = 14,14 & 15 = 14,此时head = 14*      再下一次 14 - 1 = 13,13 & 15 = 13,此时head = 13*      ...* 这是为了防止数组到头了边界溢出。* 最终如果到头了,且数组未满时,就从尾再向前,相当于循环利用数组。* 即head指向的是当前队头元素。*/elements[head = (head - 1) & (elements.length - 1)] = e;// tail指向的是头元素的下一个位置。判断head == tail即判断数组是否满了,需要扩容。if (head == tail)// 从方法名可以看出,扩容为原数组长度2倍。doubleCapacity();
}

2.4.2. add(E e) & addLast(E e)

public boolean add(E e) {addLast(e);return true;
}// 从队尾入队
public void addLast(E e) {// 不允许null元素if (e == null)throw new NullPointerException();// 初始时tail为0,直接入队,此时tail指向的是从队尾入队队列的头元素的下一个位置。elements[tail] = e;/** head指向的是队头元素的位置* tail + 1指向队头的下一个元素,判断是否 == head,即判断数组是否满了。* 即是否走扩容的逻辑。*/if ( (tail = (tail + 1) & (elements.length - 1)) == head)doubleCapacity();
}

2.4.3. offer(E e)

public boolean offer(E e) {return offerLast(e);
}public boolean offerFirst(E e) {addFirst(e);return true;
}public boolean offerLast(E e) {addLast(e);return true;
}

小结:
1. 入队有两种方式,从队列头或者从队列尾;
2. 如果容量不够了,直接扩大为两倍;
3. 通过取模的方式让头尾指针在数组范围内循环;
4. x & (len - 1) = x % len,使用 & 位运算的方式更快;

2.5. 扩容

private void doubleCapacity() {// assert:断言,判断head是否等于tail// 值为true时,程序从断言语句处继续执行// 值为false时,程序从断言语句处停止执行assert head == tail;// 头指针的位置int p = head;// 数组长度int n = elements.length;// 头指针离数组尾的距离int r = n - p; // number of elements to the right of p// 新长度为旧长度的两倍int newCapacity = n << 1;// 判断是否溢出if (newCapacity < 0)throw new IllegalStateException("Sorry, deque too big");// 新建新数组Object[] a = new Object[newCapacity];// 将旧数组head之后的元素拷贝到新数组中System.arraycopy(elements, p, a, 0, r);// 将旧数组下标0到head之间的元素拷贝到新数组中System.arraycopy(elements, 0, a, r, p);// 赋值为新数组elements = a;// head指向0,tail指向旧数组长度表示的位置head = 0;tail = n;
}

扩容这里迁移元素可能有点绕,请看下面这张图来理解:

2.6. 出队

2.6.1. poll() & pollFirst()

public E poll() {return pollFirst();
}// 从队列头出队
public E pollFirst() {int h = head;@SuppressWarnings("unchecked")// 取队列头元素 (head指向的就是头元素)E result = (E) elements[h];// 如果队列为空,就返回nullif (result == null)return null;// 将队列头置为空elements[h] = null;// 队列头指针右移一位head = (h + 1) & (elements.length - 1);// 返回出队的元素return result;
}

2.6.2. pollLast()

// 从队列尾出队
public E pollLast() {// 尾指针左移一位 因为通过addLast()我们可以知道,tail指向的是头元素的下一个位置int t = (tail - 1) & (elements.length - 1);@SuppressWarnings("unchecked")// 取当前尾指针处元素E result = (E) elements[t];// 如果队列为空返回nullif (result == null)return null;// 将当前尾指针处置为空elements[t] = null;// tail指向新的尾指针处tail = t;// 返回出队的元素return result;
}

2.7. 删除元素

此处的 remove 和 poll,前者会抛出异常,后者不会。

public E remove() { // 当作队列时默认头部删除return removeFirst();
}public E poll() { // 默认头部删除return pollFirst();
}public E removeFirst() {E x = pollFirst(); if (x == null) // 此时不存在会抛出异常throw new NoSuchElementException(); // 没有此元素异常return x;
}public E removeLast() {E x = pollLast();if (x == null)throw new NoSuchElementException();return x;
}public E pollFirst() {int h = head; // 保存头部@SuppressWarnings("unchecked") // 未选中 告诉编译器忽略 unchecked 警告信息,意思可以是null元素,编译期会通过,因为deque接口不允许有空值E result = (E) elements[h]; // 保存这个元素// Element is null if deque empty 如果deque为空,则元素为nullif (result == null)return null;elements[h] = null;     // Must null out slot 垃圾回收head = (h + 1) & (elements.length - 1); // 头指向指向下一个return result; // 返回这个值
}public E pollLast() { // 尾部删除int t = (tail - 1) & (elements.length - 1); // 因为tail指向的是最后一个元素的下一个空位置,所以得先找到最后一个元素@SuppressWarnings("unchecked") // 可以是空E result = (E) elements[t];if (result == null)return null;elements[t] = null;tail = t;return result;
}
public E pop() { // 当作栈时默认头部出栈return removeFirst();
}

从队列头部开始和尾部开始删除指定元素

// 从队列头向后遍历移除指定元素
public boolean removeFirstOccurrence(Object o) {if (o == null)return false;int mask = elements.length - 1; // 保存数组长度 mask即掩码int i = head; // 保存头部Object x; // x用于保存待删的元素while ( (x = elements[i]) != null) { // 从前往后遍历数组if (o.equals(x)) { // 如果相等delete(i); // 删除ireturn true;}i = (i + 1) & mask;}return false;
}// 从队列尾向前遍历移除指定元素
public boolean removeLastOccurrence(Object o) {if (o == null)return false;int mask = elements.length - 1;int i = (tail - 1) & mask;Object x;while ( (x = elements[i]) != null) { // 从后往前遍历数组if (o.equals(x)) {delete(i);return true;}i = (i - 1) & mask;}return false;
}private void checkInvariants() { // 有效性检查,assert elements[tail] == null; // 判断是否尾部为空,tail位置没有元素// 如果head == tail说明数组为空,令head位置为空,否则头部有元素,tail-1有元素assert head == tail ? elements[head] == null :(elements[head] != null &&elements[(tail - 1) & (elements.length - 1)] != null);// head-1位置没有元素assert elements[(head - 1) & (elements.length - 1)] == null;
}private boolean delete(int i) { // 删除 i位置元素checkInvariants(); // 校验不变量  有效性检查final Object[] elements = this.elements; // 定义一个新数组保存旧数组final int mask = elements.length - 1;final int h = head;//保存头部final int t = tail;//保存尾部final int front = (i - h) & mask; // i位置前的元素个数final int back  = (t - i) & mask; // i位置后的元素个数// Invariant: head <= i < tail mod circularity// 不变的是: 头小于i小于tail 保证循环性// 再次检验,如果i到头部的距离大于等于尾部到头部的距离,表示当前队列已经被修改了,通过最开始检测,i是不应该满足该条件。if (front >= ((t - h) & mask)) // 如果i不在head和tail之间throw new ConcurrentModificationException();// 判断i靠近头还是尾,尽量移动较少元素// Optimize for least element motionif (front < back) { // 如果i靠近headif (h <= i) { // 在进行检测 h小于等于i 在i前面// 直接覆盖 比如 0 1 2 3 3是待删元素,覆盖后 0 0 1 2System.arraycopy(elements, h, elements, h + 1, front);} else { // Wrap around h大于i 在i后面System.arraycopy(elements, 0, elements, 1, i);elements[0] = elements[mask];System.arraycopy(elements, h, elements, h + 1, mask - h);}elements[h] = null;head = (h + 1) & mask;return false; // 返回false则是 从左往右移} else { // i靠近tailif (i < t) { // Copy the null tail as well i在tail前System.arraycopy(elements, i + 1, elements, i, back);tail = t - 1;} else { // Wrap around  i在tail后面System.arraycopy(elements, i + 1, elements, i, mask - i);elements[mask] = elements[0];System.arraycopy(elements, 1, elements, 0, t);tail = (t - 1) & mask;}return true; // 返回true则是从右往左}
}

2.8. 获取元素

peekFirst() 的作用是返回但不删除Deque首端元素,也即是head位置处的元素,直接返回elements[head]即可。
peekLast() 的作用是返回但不删除Deque尾端元素,也即是tail位置前面的那个元素。

public E element() { // 获取元素 默认获取队头return getFirst();
}public E getFirst() {@SuppressWarnings("unchecked")E result = (E) elements[head];if (result == null)throw new NoSuchElementException();return result;
}public E getLast() {@SuppressWarnings("unchecked")E result = (E) elements[(tail - 1) & (elements.length - 1)];if (result == null)throw new NoSuchElementException();return result;
}public E peek() {return peekFirst();
}@SuppressWarnings("unchecked")
public E peekFirst() {// elements[head] is null if deque emptyreturn (E) elements[head];
}@SuppressWarnings("unchecked")
public E peekLast() {return (E) elements[(tail - 1) & (elements.length - 1)];
}

2.9. 栈操作

前面我们介绍 Deque 的时候说过,Deque 可以直接作为栈来使用,那么 ArrayDeque 是怎么实现的呢?非常简单,看如下代码:

// 入栈
public void push(E e) {addFirst(e);
}// 出栈
public E pop() {// 底层调用的还是pollFirst()return removeFirst();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/112028.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【学习笔记】RabbitMQ01:基础概念认识以及快速部署

参考资料 RabbitMQ官方网站RabbitMQ官方文档噼咔噼咔-动力节点教程 文章目录 一、认识RabbitMQ1.1 消息中间件&#xff08;MQ Message Queue 消息队列1.2 主流的消息中间件1.3 MQ的应用场景1.3.1 异步处理1.3.2 系统解耦1.3.3 流量削峰1.3.4 日志处理 二、RabbitMQ运行环境搭建…

驱动开发day2

任务&#xff1a;使用模块化编译安装驱动实现三盏LED灯的亮灭 驱动程序 #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h>#define PHY_RCC 0X50000A28 #define PH…

神经网络中的反向传播:综合指南

塔曼纳 一、说明 反向传播是人工神经网络 &#xff08;ANN&#xff09; 中用于训练深度学习模型的流行算法。它是一种监督学习技术&#xff0c;用于调整网络中神经元的权重&#xff0c;以最小化预测输出和实际输出之间的误差。 在神经网络中&#xff0c;反向传播是计算损失函数…

基于 Qt UDP通信局域网通信

前言 该例程经过实际验证可以正常使用,只简单的使用UDP中的单播模式(一对一), 所用测试系统在同一局域网,其中: QT版本:5.12 PC端UDP模式:单播 UDP通信目标:基于STM32F4+LWIP协议的以太网接口 调试助手: 虚拟串口+串口助手+UDP和TCP调试助手[编程人员必备]一、UDP通…

MySQL数据库下载与安装使用

文章目录 MySQL数据库下面是各个版本完整的生命周期。下载MySQL安装包安装和使用MySQL一些基础MySQL使用命令 MySQL数据库 这里我选择的是免安装绿色解压版本 现在各位开发者使用的MySQL&#xff0c;大部分版本都是 5.7&#xff0c;根据官方说明&#xff0c;MySQL 5.7 将于 202…

18.项目开发之前端项目搭建测试

项目开发之前端项目搭建测试 解压文件&#xff0c;将前端项目目录&#xff0c;拖拽到HBuilder中 前端项目QuantTrade_vue地址&#xff1a;传送门 后端项目QuantTrade地址&#xff1a; https://pan.baidu.com/s/1GF45B0QepApH8JbRIOLY7w?pwd1016 开启idea的项目&#xff0c;先…

智慧油气田方案:视频+AI识别,助力油气田生产与管理智能化转型

一、背景与挑战 根据《“十四五”能源领域科技创新规划》指出&#xff0c;要推动核心技术创新突破&#xff0c;推动煤炭、油田、电厂、电网等传统行业与数字化、智能化技术深度融合。我国油田产业已经摆脱了早期粗放式增长的阶段&#xff0c;需要更加精细化、智慧化、科学化的…

PyQt学习笔记-获取Hash值的小工具

目录 一、概述1.1 版本信息&#xff1a;1.2 基本信息&#xff1a;1.2.1 软件支持的内容&#xff1a;1.2.2 支持的编码格式 1.3 软件界面图 二、代码实现2.1 View2.2 Controller2.3 Model 三、测试示例 一、概述 本工具居于hashlibPyQtQFileDialog写的小工具&#xff0c;主要是…

django建站过程(2)创建第一个应用程序页面

创建第一个应用程序页面 设置第一个页面【settings.py,urls.py,views.py】settings.pyurls.pyviews.py django是由一系列应用程序组成&#xff0c;协同工作&#xff0c;让项目成为一个整体。前面已创建了一个应用程序baseapp,使用的命令 python manage.py startapp baseapps这…

机器学习,神经网络中,自注意力跟卷积神经网络之间有什么样的差异或者关联?

如图 6.38a 所示&#xff0c;如果用自注意力来处理一张图像&#xff0c;假设红色框内的“1”是要考虑的像素&#xff0c;它会产生查询&#xff0c;其他像素产生 图 6.37 使用自注意力处理图像 键。在做内积的时候&#xff0c;考虑的不是一个小的范围&#xff0c;而是整张图像的…

发展新能源汽车加快充换电基础设施建设实施方案-安科瑞黄安南

摘要&#xff1a;为深入贯彻落实《国务院办公厅关于印发新能源汽车产业发展规划&#xff08;2021—2035年&#xff09;的通知》&#xff08;国办发 ﹝2020﹞39号&#xff09;、《国家发展改革委等部门关于进一步提升电动汽车充电基础设施服务保障能力的实施意见》&#xff08;发…

【电子通识】USB接口三大类型图册

基本概念 不同时期的USB接口有不同的类型&#xff0c;USB接口分为插头和插座&#xff1a; 插头&#xff0c;plug&#xff0c;对应的也叫公口&#xff0c;即插别人的。 插座&#xff0c;receptacle&#xff0c;对应也叫做母口&#xff0c;即被插的。 USB的接口类型&#xff0…

VMware下linux中ping报错unknown host的解决办法

一、错误截图 二、解决办法 2.1 按照步骤查看本机虚拟IP 依次点击&#xff1a;【编辑】》【虚拟网络编辑器】&#xff0c;选中NET模式所属的行&#xff0c;就能看到子网地址。 比喻&#xff0c;我的子网地址是&#xff1a;192.168.18.0 那么&#xff0c;接下来要配置的linux…

根据pid查看jar包(windows)

打开jdk/bin/jvisualvm.exe,根据pid找到jar包的主启动类,jdk14以后不再默认使用,官网下载,也可以使用老版本的查看

如果Domino上的邮件无法直接发送到@outlook.com

大家好&#xff0c;才是真的好。 目前将Domino仅仅作为邮件服务器的企业用户还不少。如果Domino服务器版本比较新&#xff08;例如版本为11.0.x、12.0.x等&#xff09;&#xff0c;外发邮件时&#xff0c;没有通过邮件网关中转邮件&#xff0c;而是直接发送到Internet互联网上…

未来数字化转型发展的前景如何,企业又该怎么实现?

商业世界有一个认识&#xff0c;互联网只用看中国和美国&#xff0c;其他国家已经被远远甩在了后边&#xff0c;移动互联网的出现更是将互联网的跨地域、跨国、互联等属性发挥到了极致&#xff0c;让众多互联网巨头开启了争夺世界各国市场的脚步。 移动互联网的飞速发展以及物…

VMware——Window11安装VMware17(图解版)

目录 一、VMware17百度云下载二、安装三、注册 一、VMware17百度云下载 下载链接&#xff1a;https://pan.baidu.com/s/1dv_Y7ig2LUFxeHvrG2rOTA 提取码&#xff1a;elih 二、安装 下载 VMware-workstation-full-17.0.2-21581411.exe 安装包后&#xff0c;右键以管理员身份运…

软考-访问控制技术原理与应用

本文为作者学习文章&#xff0c;按作者习惯写成&#xff0c;如有错误或需要追加内容请留言&#xff08;不喜勿喷&#xff09; 本文为追加文章&#xff0c;后期慢慢追加 by 2023年10月 访问控制概念 访问控制是计算机安全的一个重要组成部分&#xff0c;用于控制用户或程序如…

PHP的学习入门建议

学习入门PHP的步骤如下&#xff1a; 确定学习PHP的目的和需求&#xff0c;例如是为了开发网站还是为了与数据库交互等。学习PHP的基础语法和程序结构&#xff0c;包括变量、数据类型、循环、条件等。学习PHP的面向对象编程&#xff08;OOP&#xff09;概念和技术。学习与MySQL…

测试用例编写详解

一、前言 测试用例的编写需要按照一定的思路进行&#xff0c;而不是想到哪写到哪&#xff0c;一般测试机制成熟的公司都会有公司自己自定义的测试用例模板&#xff0c;以及一整套的测试流程关注点&#xff0c;当然我们自己在测试生涯中也应当积累一套自己的测试框架&#xff0…