问题

(1)什么是双端队列?

(2)ArrayDeque是怎么实现双端队列的?

(3)ArrayDeque是线程安全的吗?

(4)ArrayDeque是有界的吗?

简介

双端队列是一种特殊的队列,它的两端都可以进出元素,故而得名双端队列。

ArrayDeque是一种以数组方式实现的双端队列,它是非线程安全的。

继承体系

通过继承体系可以看,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中新增了以下几类方法:

(1)*First,表示从队列头操作元素;

(2)*Last,表示从队列尾操作元素;

(3)push(e),pop(),以栈的方式操作元素的方法;

源码分析

主要属性

// 存储元素的数组
transient Object[] elements; // non-private to simplify nested class access
// 队列头位置
transient int head;
// 队列尾位置
transient int tail;
// 最小初始容量
private static final int MIN_INITIAL_CAPACITY = 8;

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

主要构造方法

// 默认构造方法,初始容量为16
public ArrayDeque() {
elements = new Object[16];
}
// 指定元素个数初始化
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
// 将集合c中的元素初始化到数组中
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
// 初始化数组
private void allocateElements(int numElements) {
elements = new Object[calculateSize(numElements)];
}
// 计算容量,这段代码的逻辑是算出大于numElements的最接近的2的n次方且不小于8
// 比如,3算出来是8,9算出来是16,33算出来是64
private static int calculateSize(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
if (numElements >= initialCapacity) {
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++; if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
return initialCapacity;
}

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

入队

入队有很多方法,我们这里主要分析两个,addFirst(e)和addLast(e)。

// 从队列头入队
public void addFirst(E e) {
// 不允许null元素
if (e == null)
throw new NullPointerException();
// 将head指针减1并与数组长度减1取模
// 这是为了防止数组到头了边界溢出
// 如果到头了就从尾再向前
// 相当于循环利用数组
elements[head = (head - 1) & (elements.length - 1)] = e;
// 如果头尾挨在一起了,就扩容
// 扩容规则也很简单,直接两倍
if (head == tail)
doubleCapacity();
}
// 从队列尾入队
public void addLast(E e) {
// 不允许null元素
if (e == null)
throw new NullPointerException();
// 在尾指针的位置放入元素
// 可以看到tail指针指向的是队列最后一个元素的下一个位置
elements[tail] = e;
// tail指针加1,如果到数组尾了就从头开始
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}

(1)入队有两种方式,从队列头或者从队列尾;

(2)如果容量不够了,直接扩大为两倍;

(3)通过取模的方式让头尾指针在数组范围内循环;

(4)x & (len - 1) = x % len,使用&的方式更快;

扩容

private void doubleCapacity() {
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;
}

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

出队

出队同样有很多方法,我们主要看两个,pollFirst()和pollLast()。

// 从队列头出队
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
// 取队列头元素
E result = (E) elements[h];
// 如果队列为空,就返回null
if (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);
@SuppressWarnings("unchecked")
// 取当前尾指针处元素
E result = (E) elements[t];
// 如果队列为空返回null
if (result == null)
return null;
// 将当前尾指针处置为空
elements[t] = null;
// tail指向新的尾指针处
tail = t;
// 返回取得的元素
return result;
}

(1)出队有两种方式,从队列头或者从队列尾;

(2)通过取模的方式让头尾指针在数组范围内循环;

(3)出队之后没有缩容哈哈^^

前面我们介绍Deque的时候说过,Deque可以直接作为栈来使用,那么ArrayDeque是怎么实现的呢?

public void push(E e) {
addFirst(e);
} public E pop() {
return removeFirst();
}

是不是很简单,入栈出栈只要都操作队列头就可以了。

总结

(1)ArrayDeque是采用数组方式实现的双端队列;

(2)ArrayDeque的出队入队是通过头尾指针循环利用数组实现的;

(3)ArrayDeque容量不足时是会扩容的,每次扩容容量增加一倍;

(4)ArrayDeque可以直接作为栈使用;

彩蛋

双端队列与双重队列?

双端队列(Deque)是指队列的两端都可以进出元素的队列,里面存储的是实实在在的元素。

双重队列(Dual Queue)是指一种队列有两种用途,里面的节点分为数据节点和非数据节点,它是LinkedTransferQueue使用的数据结构。

还记得LinkedTransferQueue吗?点击链接直达【死磕 java集合之LinkedTransferQueue源码分析】。


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java集合之ArrayDeque源码分析的更多相关文章

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

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

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

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

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

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

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

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

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

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

  6. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

  7. 【死磕 Java 集合】— ConcurrentSkipListMap源码分析

    转自:http://cmsblogs.com/?p=4773 [隐藏目录] 前情提要 简介 存储结构 源码分析 主要内部类 构造方法 添加元素 添加元素举例 删除元素 删除元素举例 查找元素 查找元素 ...

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

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

  9. 死磕 java集合之ConcurrentSkipListSet源码分析——Set大汇总

    问题 (1)ConcurrentSkipListSet的底层是ConcurrentSkipListMap吗? (2)ConcurrentSkipListSet是线程安全的吗? (3)Concurren ...

随机推荐

  1. ES6的let和const的不同点

    详细的异同点请点击:https://blog.csdn.net/zhouziyu2011/article/details/71366078

  2. 46-web页面登入前和登入后控制

    可以将user存入session中,然后在前端根据能否取到user,来判断是否登入 <c:if test="${user == null }"> <li clas ...

  3. node.js中path路径模块的使用

    path模块是node.js中处理路径的核心模块.可以很方便的处理关于文件路径的问题. join() 将多个参数值合并成一个路径 const path = require('path'); conso ...

  4. Spring Boot 整合mybatis 使用多数据源

    本人想要实现一个项目里面多个数据库源连接,所以就尝试写一个demo,不多说,先贴结构,再贴代码,可以根据以下的顺序,直接copy解决问题. 首先,dao和resource下的mappers可以用myb ...

  5. HOSTNAME问题 和yum配置163源的操作 安装lsb_release,KSH,CSH

    HOSTNAME 在 /etc/hosts 里添加一行 127.0.0.1 yourhostname yum配置 来自http://www.cnblogs.com/wutengbiao/p/41889 ...

  6. [杂谈]杂谈章1 问几个JAVA问题

    1.面向对象.面向过程 区别 2.Java 如何实现的平台无关 和C/C++不同的是,Java语言提供的编译器不针对特定的操作系统和CPU芯片进行编程,而是针对Java虚拟机把Java源程序编译成称为 ...

  7. 【Git】 GitLab简单使用

    本例介绍简单实用GitLab,安装请参照[Git] GitLab服务器社区版安装与配置 1.用户和组的管理 a.创建组,在首页点击Create a group b.创建用户,在首页点击Add peop ...

  8. squid常用操作

    如何查看squid的缓存命中率 使用命令: squidclient -h host -p port mgr:info比如: /usr/local/squid/bin/squidclient -h 12 ...

  9. xml配置文件中常见的命名空间解释

    1.1schema文档即xml schema document,schema文件的格式是.xsd(xml schema document的缩写xsd). 简单来说:schema就是对xml的进一步约束 ...

  10. Makefile基础学习

    Makefile基础学习 理论知识 makefile关系到了整个工程的编译规则.一个工程中的源文件不计其数,并且按类型.功能.模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文 ...