欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java容器类源码详解 Deque与ArrayDeque

程序员文章站 2022-06-23 18:19:42
前言 queue 也是 java 集合框架中定义的一种接口,直接继承自 collection 接口。除了基本的 collection 接口规定测操作外,queue 接口还...

前言

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)之后的。参考下面的图片:

Java容器类源码详解 Deque与ArrayDeque

那么为什么(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的幂次方的保证。

Java容器类源码详解 Deque与ArrayDeque

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 的元素。

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