Java容器类源码详解 Deque与ArrayDeque

前言

Queue 也是 Java 集合框架中定义的一种接口,直接继承自 Collection 接口。除了基本的 Collection 接口规定测操作外,Queue 接口还定义一组针对队列的特殊操作。通常来说,Queue 是按照先进先出(FIFO)的方式来管理其中的元素的,但是优先队列是一个例外。

Deque 接口继承自 Queue接口,但 Deque 支持同时从两端添加或移除元素,因此又被成为双端队列。鉴于此,Deque 接口的实现可以被当作 FIFO队列使用,也可以当作LIFO队列(栈)来使用。官方也是推荐使用 Deque 的实现来替代 Stack。

ArrayDeque 是 Deque 接口的一种具体实现,是依赖于可变数组来实现的。ArrayDeque 没有容量限制,可根据需求自动进行扩容。ArrayDeque不支持值为 null 的元素。

下面基于JDK 8中的实现对 ArrayDeque 加以分析。

方法概览

public interface Queue<E> extends Collection<E> {
//向队列中插入一个元素,并返回true
//如果队列已满,抛出IllegalStateException异常
boolean add(E e);
//向队列中插入一个元素,并返回true
//如果队列已满,返回false
boolean offer(E e);
//取出队列头部的元素,并从队列中移除
//队列为空,抛出NoSuchElementException异常
E remove();
//取出队列头部的元素,并从队列中移除
//队列为空,返回null
E poll();
//取出队列头部的元素,但并不移除
//如果队列为空,抛出NoSuchElementException异常
E element();
//取出队列头部的元素,但并不移除
//队列为空,返回null
E peek();
}

Deque 提供了双端的插入与移除操作,如下表:

    First Element (Head)   Last Element (Tail)
  Throws exception Special value Throws exception Special value
Insert addFirst(e) offerFirst(e) addLast(e) offerLast(e)
Remove removeFirst() pollFirst() removeLast() pollLast()
Examine getFirst() peekFirst() getLast() peekLast()

Deque 和 Queue 方法的的对应关系如下:

Queue Method Equivalent Deque Method
add(e) addLast(e)
offer(e) offerLast(e)
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()

Deque 和 Stack 方法的对应关系如下:

Stack Method Equivalent Deque Method
push(e) addFirst(e)
pop() removeFirst()
peek() peekFirst()

ArrayList 实现了 Deque 接口中的所有方法。因为 ArrayList 会根据需求自动扩充容量,因而在插入元素的时候不会抛出IllegalStateException异常。

底层结构

//用数组存储元素
transient Object[] elements; // non-private to simplify nested class access
//头部元素的索引
transient int head;
//尾部下一个将要被加入的元素的索引
transient int tail;
//最小容量,必须为2的幂次方
private static final int MIN_INITIAL_CAPACITY = 8;

在 ArrayDeque 底部是使用数组存储元素,同时还使用了两个索引来表征当前数组的状态,分别是 head 和 tail。head 是头部元素的索引,但注意 tail 不是尾部元素的索引,而是尾部元素的下一位,即下一个将要被加入的元素的索引。

初始化

ArrayDeque 提供了三个构造方法,分别是默认容量,指定容量及依据给定的集合中的元素进行创建。默认容量为16。

public ArrayDeque() {
elements = new Object[16];
}
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}

ArrayDeque 对数组的大小(即队列的容量)有特殊的要求,必须是 2^n。通过 allocateElements方法计算初始容量:

private void allocateElements(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
}
elements = new Object[initialCapacity];
}

>>>是无符号右移操作,|是位或操作,经过五次右移和位或操作可以保证得到大小为2^k-1的数。看一下这个例子:

0 0 0 0 1 ? ? ? ? ? //n
0 0 0 0 1 1 ? ? ? ? //n |= n >>> 1;
0 0 0 0 1 1 1 1 ? ? //n |= n >>> 2;
0 0 0 0 1 1 1 1 1 1 //n |= n >>> 4;

在进行5次位移操作和位或操作后就可以得到2^k-1,最后加1即可。这个实现还是很巧妙的。

添加元素

向末尾添加元素:

public void addLast(E e) {
if (e == null)
throw new NullPointerException();
//tail 中保存的是即将加入末尾的元素的索引
elements[tail] = e;
//tail 向后移动一位
//把数组当作环形的,越界后到0索引
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
//tail 和 head相遇,空间用尽,需要扩容
doubleCapacity();
}

这段代码中,(tail = (tail + 1) & (elements.length - 1)) == head这句有点难以理解。其实,在 ArrayDeque 中数组是当作环形来使用的,索引0看作是紧挨着索引(length-1)之后的。参考下面的图片:

那么为什么(tail + 1) & (elements.length - 1)就能保证按照环形取得正确的下一个索引值呢?这就和前面说到的 ArrayDeque 对容量的特殊要求有关了。下面对其正确性加以验证:

  • length = 2^n,二进制表示为: 第 n 位为1,低位 (n-1位) 全为0
  • length - 1 = 2^n-1,二进制表示为:低位(n-1位)全为1
  • 如果 tail + 1 <= length - 1,则位与后低 (n-1) 位保持不变,高位全为0
  • 如果 tail + 1 = length,则位与后低 n 全为0,高位也全为0,结果为 0

可见,在容量保证为 2^n 的情况下,仅仅通过位与操作就可以完成环形索引的计算,而不需要进行边界的判断,在实现上更为高效。

向头部添加元素的代码如下:

public void addFirst(E e) {
if (e == null) //不支持值为null的元素
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}

其它的诸如add,offer,offerFirst,offerLast等方法都是基于上面这两个方法实现的,不再赘述。

扩容

在每次添加元素后,如果头索引和尾部索引相遇,则说明数组空间已满,需要进行扩容操作。 ArrayDeque 每次扩容都会在原有的容量上翻倍,这也是对容量必须是2的幂次方的保证。

private void doubleCapacity() {
assert head == tail; //扩容时头部索引和尾部索引肯定相等
int p = head;
int n = elements.length;
//头部索引到数组末端(length-1处)共有多少元素
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];
//复制头部索引到数组末端的元素到新数组的头部
System.arraycopy(elements, p, a, 0, r);
//复制其余元素
System.arraycopy(elements, 0, a, r, p);
elements = a;
//重置头尾索引
head = 0;
tail = n;
}

移除元素

ArrayDeque支持从头尾两端移除元素,remove方法是通过poll来实现的。因为是基于数组的,在了解了环的原理后这段代码就比较容易理解了。

public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// Element is null if deque empty
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];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}

获取队头和队尾的元素

@SuppressWarnings("unchecked")
public E peekFirst() {
// elements[head] is null if deque empty
return (E) elements[head];
}
@SuppressWarnings("unchecked")
public E peekLast() {
return (E) elements[(tail - 1) & (elements.length - 1)];
}

迭代器

ArrayDeque 在迭代是检查并发修改并没有使用类似于 ArrayList 等容器中使用的 modCount,而是通过尾部索引的来确定的。具体参考 next 方法中的注释。但是这样不一定能保证检测到所有的并发修改情况,加入先移除了尾部元素,又添加了一个尾部元素,这种情况下迭代器是没法检测出来的。

private class DeqIterator implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
private int cursor = head;
/**
* Tail recorded at construction (also in remove), to stop
* iterator and also to check for comodification.
*/
private int fence = tail;
/**
* Index of element returned by most recent call to next.
* Reset to -1 if element is deleted by a call to remove.
*/
private int lastRet = -1;
public boolean hasNext() {
return cursor != fence;
}
public E next() {
if (cursor == fence)
throw new NoSuchElementException();
@SuppressWarnings("unchecked")
E result = (E) elements[cursor];
// This check doesn't catch all possible comodifications,
// but does catch the ones that corrupt traversal
// 如果移除了尾部元素,会导致tail != fence
// 如果移除了头部元素,会导致 result == null
if (tail != fence || result == null)
throw new ConcurrentModificationException();
lastRet = cursor;
cursor = (cursor + 1) & (elements.length - 1);
return result;
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
if (delete(lastRet)) { // if left-shifted, undo increment in next()
cursor = (cursor - 1) & (elements.length - 1);
fence = tail;
}
lastRet = -1;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
Object[] a = elements;
int m = a.length - 1, f = fence, i = cursor;
cursor = f;
while (i != f) {
@SuppressWarnings("unchecked") E e = (E)a[i];
i = (i + 1) & m;
if (e == null)
throw new ConcurrentModificationException();
action.accept(e);
}
}
}

除了 DeqIterator,还有一个反向的迭代器 DescendingIterator,顺序和 DeqIterator 相反。

小结

ArrayDeque 是 Deque 接口的一种具体实现,是依赖于可变数组来实现的。ArrayDeque 没有容量限制,可根据需求自动进行扩容。ArrayDeque 可以作为栈来使用,效率要高于 Stack;ArrayDeque 也可以作为队列来使用,效率相较于基于双向链表的 LinkedList 也要更好一些。注意,ArrayDeque 不支持为 null 的元素。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java ArrayDeque实现Stack的功能

    在J2SE6引入了ArrayDeque类,它继承了Deque(双向队列)接口,使用此类可以自己实现java.util.Stack类的功能,去掉了java.util.Stack的多线程同步的功能. 例如创建一个存放Integer类型的Stack,只要在类中创建一个ArrayDeque类的变量作为属性,之后定义的出栈.入栈,观察栈顶元素的操作就直接操作ArrayDeque的实例变量即可. import java.util.ArrayDeque; import java.util.Deque; pub

  • java集合 ArrayDeque源码详细分析

    问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的两端都可以进出元素,故而得名双端队列. ArrayDeque是一种以数组方式实现的双端队列,它是非线程安全的. 继承体系 通过继承体系可以看,ArrayDeque实现了Deque接口,Deque接口继承自Queue接口,它是对Queue的一种增强. public interface Deque<E>

  • Java ArrayDeque使用方法详解

    题目要求为: 卡拉兹(Callatz)猜想: 对任何一个自然数n,如果它是偶数,那么把它砍掉一半:如果它是奇数,那么把(3n+1)砍掉一半.这样一直反复砍下去,最后一定在某一步得到n=1.当我们验证卡拉兹猜想的时候,为了避免重复计算,可以记录下递推过程中遇到的每一个数.例如对n=3进行验证的时候,我们需要计算3.5.8.4.2.1,则当我们对n=5.8.4.2进行验证的时候,就可以直接判定卡拉兹猜想的真伪,而不需要重复计算,因为这4个数已经在验证3的时候遇到过了,我们称5.8.4.2是被3"覆盖

  • Java集合ArrayDeque类实例分析

    Java集合ArrayDeque类实例分析 前言 ArrayDeque类是双端队列的实现类,类的继承结构如下面,继承自AbastractCollection(该类实习了部分集合通用的方法,其实现了Collection接口),其实现的接口Deque接口中定义了双端队列的主要的方法,比如从头删除,从尾部删除,获取头数据,获取尾部数据等等. public class ArrayDeque<E> extends AbstractCollection<E> implements Deque&

  • java.util.ArrayDeque类使用方法详解

    本文为大家介绍了java.util.ArrayDeque类使用方法,供大家参考,具体内容如下 1. ArrayDeque有两个类属性,head和tail,两个指针. 2. ArrayDeque通过一个数组作为载体,其中的数组元素在add等方法执行时不移动,发生变化的只是head和tail指针,而且指针是循环变化,数组容量不限制. 3. offer方法和add方法都是通过其中的addLast方法实现,每添加一个元素,就把元素加到数组的尾部,此时,head指针没有变化,而tail指针加一,因为指针是

  • Java容器类源码详解 Deque与ArrayDeque

    前言 Queue 也是 Java 集合框架中定义的一种接口,直接继承自 Collection 接口.除了基本的 Collection 接口规定测操作外,Queue 接口还定义一组针对队列的特殊操作.通常来说,Queue 是按照先进先出(FIFO)的方式来管理其中的元素的,但是优先队列是一个例外. Deque 接口继承自 Queue接口,但 Deque 支持同时从两端添加或移除元素,因此又被成为双端队列.鉴于此,Deque 接口的实现可以被当作 FIFO队列使用,也可以当作LIFO队列(栈)来使用

  • Java 容器类源码详解 Set

    前言 Set 表示由无重复对象组成的集合,也是集合框架中重要的一种集合类型,直接扩展自 Collection 接口.在一个 Set 中,不能有两个引用指向同一个对象,或两个指向 null 的引用.如果对象 a 和 b 的引用满足条件 a.equals(b),那么这两个对象也不能同时出现在集合中. 通常 Set 是不要求元素有序的,但也有一些有序的实现,如 SortedMap 接口.LinkedHashSet 接口等. 概述 Set 的具体实现通常都是基于 Map 的.因为 Map 中键是唯一的,

  • java LinkedList源码详解及实例

    一.LinkedList概述: LinkedList与ArrayList一样,是实现了List接口.由于LinkedList是基于链表实现的,所以它执行插入和删除操作时比ArrayList更高效,而随机访问的性能要比ArrayList低. 二.LinkedList的实现: 1.构造方法 //构造一个空的LinkedList public LinkedList() { } //接收一个Collection参数c,默认构造方法构造一个空的链表,并通过addAll将c中的元素全部添加到链表中. pub

  • java  LinkedList源码详解及实例

    一.LinkedList概述: LinkedList与ArrayList一样,是实现了List接口.由于LinkedList是基于链表实现的,所以它执行插入和删除操作时比ArrayList更高效,而随机访问的性能要比ArrayList低. 二.LinkedList的实现: 1.构造方法 //构造一个空的LinkedList public LinkedList() { } //接收一个Collection参数c,默认构造方法构造一个空的链表,并通过addAll将c中的元素全部添加到链表中. pub

  • Java并发编程之ConcurrentLinkedQueue源码详解

    一.ConcurrentLinkedQueue介绍 并编程中,一般需要用到安全的队列,如果要自己实现安全队列,可以使用2种方式: 方式1:加锁,这种实现方式就是我们常说的阻塞队列. 方式2:使用循环CAS算法实现,这种方式实现队列称之为非阻塞队列. 从点到面, 下面我们来看下非阻塞队列经典实现类:ConcurrentLinkedQueue (JDK1.8版) ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全的队列.当我们添加一个元素的时候,它会添加到队列的尾部,当我们

  • Android实现屏幕锁定源码详解

    最近有朋友问屏幕锁定的问题,自己也在学习,网上找了下也没太详细的例子,看的资料书上也没有有关屏幕锁定程序的介绍,下个小决心,自己照着官方文档学习下,现在做好了,废话不多说,先发下截图,看下效果,需要注意的地方会加注释,有问题的朋友可以直接留言,我们共同学习交流,共同提高进步!直接看效果图: 一:未设置密码时进入系统设置的效果图如下: 二:设置密码方式预览: 三:密码解密效果图 四:九宫格解密时的效果图 下面来简单的看下源码吧,此处讲下,这个小DEMO也是临时学习下的,有讲的不明白的地方请朋友直接

  • Android线程间通信Handler源码详解

    目录 前言 01. 用法 02.源码 03.结语 前言 在[Android]线程间通信 - Handler之使用篇主要讲了 Handler 的创建,发送消息,处理消息 三个步骤.那么接下来,我们也按照这三个步骤,从源码中去探析一下它们具体是如何实现的.本篇是关于创建源码的分析. 01. 用法 先回顾一下,在主线程和非主线程是如何创建 Handler 的. //主线程 private val mHandler: Handler = object : Handler(Looper.getMainLo

  • Android开发数据结构算法ArrayList源码详解

    目录 简介 ArrayList源码讲解 初始化 扩容 增加元素 一个元素 一堆元素 删除元素 一个元素 一堆元素 修改元素 查询元素 总结 ArrayList优点 ArrayList的缺点 简介 ArrayList是List接口的一个实现类,它是一个集合容器,我们通常会通过指定泛型来存储同一类数据,ArrayList默认容器大小为10,自身可以自动扩容,当容量不足时,扩大为原来的1.5倍,和上篇文章的Vector的最大区别应该就是线程安全了,ArrayList不能保证线程安全,但我们也可以通过其

  • Spring AOP底层源码详解

    ProxyFactory的工作原理 ProxyFactory是一个代理对象生产工厂,在生成代理对象之前需要对代理工厂进行配置.ProxyFactory在生成代理对象之前需要决定到底是使用JDK动态代理还是CGLIB技术. // config就是ProxyFactory对象 // optimize为true,或proxyTargetClass为true,或用户没有给ProxyFactory对象添加interface if (config.isOptimize() || config.isProxy

  • Java8中AbstractExecutorService与FutureTask源码详解

    目录 前言 一.AbstractExecutorService 1.定义 2.submit 3.invokeAll 4.invokeAny 二.FutureTask 1.定义 2.构造方法 3.get 4.run/ runAndReset 5. cancel 三.ExecutorCompletionService 1.定义 2.submit 3.take/ poll 总结 前言 本篇博客重点讲解ThreadPoolExecutor的三个基础设施类AbstractExecutorService.F

随机推荐