一、概述
1、在Java中Queue是一个接口,而对于Queue的具体实现由的是采用数组的形式,有的是链表的形式;
2、Queue实现了先进先出(FIFO)的队列的约定;
3、Deque支持在两端插入和删除元素,他是一个双端队列;
二、Queue中的接口定义
Queue接口继承了Collection接口,实现的是先进先出的队列;
接口方法 | 接口定义 | 区别 |
---|---|---|
boolean add(E e) | 向队列尾部插入元素 | 如果队列满不允许插入时,抛出异常 |
boolean offer(E e) | 向队列尾部插入元素 | 如果队列满不允许插入时,返回fase |
E remove() | 移除一个元素 | 如果队列为空,抛出异常 |
E poll() | 移除一个元素 | 如果队列为空,返回null |
E element() | 获取队列头部元素 | 如果队列为空,抛出异常 |
E peek() | 获取队列头部元素 | 如果队列为空,返回null |
三、Deque中的接口定义
Deque接口继承了Queue接口,他是一个双端队列;
接口方法 | 接口定义 | 区别 |
---|---|---|
void addFirst(E e) | 向队头中插入元素 | 当队满不允许插入时抛出异常 |
void addLast(E e) | 向队尾中插入元素 | 当队满不允许插入时抛出异常 |
boolean offerFirst(E e) | 向队头中插入元素 | 当队满不允许插入时返回false |
boolean offerLast(E e) | 向队尾中插入元素 | 当队满不允许插入时返回false |
E removeFirst() | 移除队头的元素 | 队列为空抛出异常 |
E removeLast() | 移除队尾的元素 | 队列为空抛出异常 |
E pollFirst() | 移除队头的元素 | 队列为空返回null |
E pollLast() | 移除队尾的元素 | 队列为空返回null |
E getFirst() | 获取队头元素 | 队列为空抛出异常 |
E getLast() | 获取队尾元素 | 队列为空抛出异常 |
E peekFirst() | 获取队头元素 | 队列为空返回null |
E peekLast() | 获取队尾元素 | 队列为空返回null |
boolean removeFirstOccurrence(Object o) | 从此双端队列移除第一次出现的指定元素 | |
boolean removeLastOccurrence(Object o) | 从此双端队列移除最后一次出现的指定元素 | |
boolean add(E e) | 插入元素到队列尾部 | 重写queue中的方法 |
boolean offer(E e) | 插入元素到队列尾部 | 重写queue中的方法 |
E remove() | 删除队列第一个元素 | 重写queue中的方法 |
E poll() | 删除队列第一个元素 | 重写queue中的方法 |
E element() | 获取队列头部第一个元素 | 重写queue中的方法 |
E peek() | 获取队列头部第一个元素 | 重写queue中的方法 |
void push(E e) | 向队列头部插入一个元素 | 队满不允许插入抛出异常,模仿栈的操作 |
E pop() | 从队列头部取出一个元素 | 队列为空抛出异常,模仿栈的操作 |
boolean remove(Object o) | s双端队列移除元素 | 重定义Collection接口中的方法 |
boolean contaions(Object o) | 判断双端队列是否包含元素 | 重定义Collection接口中的方法 |
int size(); | 返回双端队列元素个数 | 重定义Collection接口中的方法 |
Iterator<E> iterator() Iterator<E> descendingIterator() | 返回双端队列迭代器 返回一个迭代器在此deque队列的顺序相反的元素 | 重定义Collection接口中的方法 |
三、ArrayDeque的分析
在Queue中LinkedList实现了queue的方法,所以可以将LinkedList当做queue来使用,并且在官方推荐中建议使用ArrayDeque,在前面也已经讲过LinkedList,所以在后面将对ArrayDeque进行分析。
从名字上看可以看出ArrayDeque是通过数组来实现的,但是由于双端队列的特性,该数组必须是循环的,并且队列的头部和尾部是动态的,数组的任何一点都可以被当做是队列的起点或者终点,也就是数组的下标0不一定是代表队列的头部。
下面是三种情况的队列,可以看到队头位置head不一定永远在队尾tail的前面,他们的索引值的大小是没有比较意义的。
1、ArrayDeque的初始化
在下面可以看出ArrayDeque是用数组存储元素的,其中有两个变量head和tail分别标记队列的头部位置和尾部位置,队列的最小长度是8,初始化默认的的队列长度是16,并且队列的长度必须是2的幂数,即使指定了队列的长度,也必须进行调整,使其为2的次幂,这里为什么必须是2的次幂,在后面在进行说明。
//底层数据用数组存储
transient Object[] elements;
//队列的头部位置,当删除或出栈时会操作该位置的元素
transient int head;
//队列的尾部位置,当插入元素时会使用到该值
transient int tail;
//初始化最小队列长度,必须是2的幂数
private static final int MIN_INITIAL_CAPACITY = 8;
/**
* 初始化一个ArrayDeque,默认容量是16
*/
public ArrayDeque() {
elements = new Object[16];
}
/**
* 初始化指定容量的ArrayDeque
* @param numElements
*/
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
/**
* 初始化包含指定集合的ArrayDeque,
* 首先根据集合中元素的个数确定初始化数组的长度,然后将集合中的元素添加到ArrayDeque中
* @param c
*/
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
/**
* 根据指定的容量进行调整,使ArrayDeque的容量必须是2的幂数
*/
private void allocateElements(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
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、ArrayDeque的扩容
ArrayDeque的扩容其实是申请一个更大容量的数组,将原数组中的元素复制到新的数组中,这段代码的实现逻辑是申请一个新的数组,其容量是原数组的2倍,然后做两步复制操作,首先将原数组head右边的元素复制到新的数组中,从0开始位置,然后将原数组head右边的元素复制到新的数组中,最后重新给head和tail赋值,此时head=0,tail为原数组的长度值n。具体的过程和原因可以看下边的图。
private void doubleCapacity() {
assert head == tail; //表示队列满了,需要进行扩容
int p = head; //p指定为head的位置
int n = elements.length; //n为队列的长度
int r = n - p; //r为p到数组尾间建个的位置个数
int newCapacity = n << 1; //新的队列的长度=老队列的长度*2
if (newCapacity < 0) //表示超过了队列的最大限制长度
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r); //将旧的数组中p位置右边的元素复制到新的数组中,从0开始的位置
System.arraycopy(elements, 0, a, r, p); //将就的数组中0开始的位置右边的元素复制到新的数组中,从r开始的位置
elements = a;
head = 0;
tail = n;
}
3、ArrayDeque头部插入元素
向ArrayDeque的头部插入元素有两种方法,其实现过程是一样的,只是对于满队列情况下返回值的处理不一样。
向队列的头部插入元素,实际上就是在head的前面插入元素,通常情况下在前面插入元素只需要是element[--head]=e就可以,但是由于ArrayDeque是双端队列,循环数组,会存在数组下标越界的情况,所以在插入时还需要进行下标的检查,因此在插入一个元素值时,存在下面几步校验:
(1)插入的元素是否为空,在ArrayDeque中不允许存在null元素;
(2)数组的下标是否越界,由于是循环数组,根据位比较可以很好的找到head的前一个元素的位置;
(3)空间是否够用,在插入元素之后,队列是否是满队列,如果是满队列,需要进行扩容。
注意:可以看到在检查空间是否够用总是在插入元素之后进行检查的,也就是说tail指向的位置其实是空的,也就是最后一个元素的后一个位置,因此队列中至少都是存在一个空位置的,所以在插入之前不需要考虑空间的问题。
/**
* 向队列头部插入一个元素
* 元素为空抛出异常
* @param e
*/
public void addFirst(E e) {
//元素为空,抛出异常
if (e == null)
throw new NullPointerException();
//检查下标是否越界
elements[head = (head - 1) & (elements.length - 1)] = e;
//空间是否够用,如果头部索引位置等于尾部索引位置,说明队列满了,需要扩容
if (head == tail)
doubleCapacity();
}
/**
*队列头部插入元素,实际上是调用addFirst方法
* @param e
* @return
*/
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
4、ArrayDeque尾部插入元素
向队列尾部插入元素时,也就是在tail的位置插入元素,由于在前面已经说过tail位置指向的是下一个可以插入的空位,所以在尾部插入时,可以直接在tail位置进行插入,插入完成之后在进行空间校验。
/**
* 向队列尾部插入一个元素
* @param e
*/
public void addLast(E e) {
//元素为空,抛出异常
if (e == null)
throw new NullPointerException();
//在tail位置插入元素
elements[tail] = e;
//获取tail的位置,并判断是否越界,如果队列满了进行扩容
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
/**
* 队列尾部插入元素
* @param e
* @return
*/
public boolean offerLast(E e) {
addLast(e);
return true;
}
5、ArrayDeque中的remove和peek等操作
下面几个方法是删除队头队尾元素或者是取出队头队尾的元素。
对于队头的操作,removeFirst和pollFirst是取出队头的元素,并删除该位置的元素,因此在取出元素后,需要将head向后移动一位,这时候需要对下标进行判断是否越界;getFirst和peekFirst是获取队头的元素,但不对队列进行操作,所以直接取出head的元素即可。
对于队尾的操作,removeLast和pollLast是取出队尾的元素,并删除该位置的元素,由于tail总是指向下一个待插入元素的位置,所以需要先获取到tail前一个位置的下标,取出元素,并将该位置元素置空;getLast和peekLast是获取队尾的元素,不对队列进行操作。
//删除队列头部第一个元素
public E removeFirst() {
E x = pollFirst();
if (x == null)
throw new NoSuchElementException();
return x;
}
//移除队列头部元素
public E pollFirst() {
int h = head;
//获取头部元素
@SuppressWarnings("unchecked")
E result = (E) elements[h];
//如果队列为空,返回null
if (result == null)
return null;
//将head位置的元素置为空
elements[h] = null;
//head向后移动一位
head = (h + 1) & (elements.length - 1);
return result;
}
//删除队列尾部元素
public E removeLast() {
E x = pollLast();
if (x == null)
throw new NoSuchElementException();
return x;
}
//移除队列尾部元素
public E pollLast() {
//获取tail前面一个位置
int t = (tail - 1) & (elements.length - 1);
@SuppressWarnings("unchecked")
E result = (E) elements[t];
if (result == null)
return null;
//将该位置元素置为空,表示删除了
elements[t] = null;
//tail向前移动一位
tail = t;
return result;
}
//获取队列头部元素,为空抛出异常
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;
}
// 获取队列头部元素,为空返回null
@SuppressWarnings("unchecked")
public E peekFirst() {
return (E) elements[head];
}
//获取队列尾部元素,为空返回null
@SuppressWarnings("unchecked")
public E peekLast() {
return (E) elements[(tail - 1) & (elements.length - 1)];
}
6、ArrayDeque的delete操作
delete操作基本上是以少移动数据为原则,也就是删除的位置i如果离head近,那么就移动head到i之间的数据,如果i离tail比较近,就移动i到tail之间的数据,具体的移动步骤和方法可以结合下面的源码和图片进行理解。
private boolean delete(int i) {
checkInvariants();
final Object[] elements = this.elements;
//数组的长度
final int mask = elements.length - 1;
final int h = head;
final int t = tail;
//i与队列头部的距离
final int front = (i - h) & mask;
//i与队列尾部的距离
final int back = (t - i) & mask;
//判断i是否在head和tail之间
if (front >= ((t - h) & mask))
throw new ConcurrentModificationException();
//i的位置靠近队列头部,那么移动头部的元素,返回false
if (front < back) {
if (h <= i) { //如果head是在i的前面,那么直接将h到i之间的元素全部向后移动一位,h位置的元素置空,head向后移动一位
System.arraycopy(elements, h, elements, h + 1, front);
} else { //如果h的位置在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;
} else { //i的位置靠近队列的尾部
if (i < t) { //如果i在tail的前面
System.arraycopy(elements, i + 1, elements, i, back);
tail = t - 1;
} else { //如果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;
}
}
7、ArrayDeque的队列长度为什么必须是2的次幂?
在前面的方法中也可以看到,在进行队列的操作时,获取下标的位置都要进行位的与运算,并且是将下标与(队列的长度-1)进行与运算,当队列的长度是2的次幂时,那么减1之后,其二进制为所有的位置都是1,进行与运算时,也就能保证得到一个正确的期望的位置,而位操作是循环数组中一种常见的操作,效率也很高。
四、队列的总结
1、Queue队列是一种特殊的线性表,它允许在队列的前端进行删除操作,在队列的后端进行插入操作 ,队列中最先插入的元素将是最先被删除的元素,因此队列又被成为先进先出的线性表;
2、Deque是一种双端队列,它允许在队列的两端进行操作;
3、ArrayDeque是一种以数组形式实现的双端队列,他的默认容量是16,且其容量必须是2的次幂;
4、ArrayDeque并不是固定大小的队列,每次队列满了就将队列容量扩大一倍,因此总是能成功的插入一个元素;
5、ArrayDeque中不可以存储null元素,因为系统是根据某个位置是否为null来判断元素的存在;
6、ArrayDeque作为栈使用时,性能比Stack好,作为队列使用时,性能比LinkedList好;
7、ArrayDeque不是线程安全的;