1.什么是Deqeue
Deque的含义是“double ended queue”,双端队列,传统队列操作受限FIFO,而这个队列可以双端进出,比较灵活;
它继承自Queue的(注意这儿是接口继承接口);
public interface Deque<E> extends Queue<E> {
......
}
队列是一种特殊的线性容器,它是一种先进先出(FIFO)的数据结构。
它只允许在容器的头部进行删除操作,而在表的后端进行插入操作。进行插入操作的端成为队尾,进行删除操作的端称为队头。
当队列中没有元素时,被称为空队列。
同理当插入元素已经充满队列,被称为满队列.
再看下双端队列的Api:
可以看到扩展了不少功能,注意我划黄线的地方,可以看到它提供了一堆这种XXXfirst()
,XXXlast()
的方法。意思就是它不再是和队列一样,只能固定的FIFO的操作容器,而是既可以操作首,也可以操作尾,也可以LIFO;
而且它还有pop,push这样的方法,pop是哪儿的方法?看栈的Api:
意思就是说,依仗可以首尾操作的特性,它不仅仅可以作为一个队列来用,它还可以作为一个栈来用,并且官方也是建议用Dequeue替换Stack:
/**
* ......
* 更完整和一致的一组LIFO堆栈操作是由{@ Link Deque}接口及其实现提供应该优先使用这个类。
*
* A more complete and consistent set of LIFO stack operations is
* provided by the {@link Deque} interface and its implementations, which
* should be used in preference to this class. For example:
* <pre>
* {@code
* Deque<Integer> stack = new ArrayDeque<Integer>();}</pre>
*
* @author Jonathan Payne
* @since JDK1.0
*/
public
class Stack<E> extends Vector<E> {
......
}
小结下Deque的Api:它都是根据是否抛异常提供俩套api
然后对应总结下它和栈和队列的api对比:
在了解了大概情况之后,我尝试去深入了解它,那么按照传统的姿势,去看他有哪些实现类,然后挑一个用途广泛的作为一个典型来入门。
Dequeue在java包下的默认子类都有:
我选择用ArrayQueue来作为研究对象。
2.ArrayDeqeue
1.大小自增长的队列
2.内部使用数组存储数据
3.线程不安全
4.内部数组长度为8、16、32…… 2的n次方
5.头指针head从内部数组的末尾开始,尾指针tail从0开始,在头部插入数据时,head减一,在尾部插入数据时,tail加一。当head==tail时说明数组的容量满足不了当前的情况,此时需要扩大容量为原来的二倍。
2.1.它的底层是循环数组:
啥是循环数组?
循环数组是实现循环队列的一种手段,与之对应的还有循环链表,这里的循环指的的是,当容器空间有富余的时候避免无谓的扩容,就比如这个ArrayDeque,一扩就是16位数组,那么究竟什么是循环数组呢?
假设我new了一个5位长度的数组,那么此刻如果我在继续往里放元素,就应该扩容了;
但此时我对head**“出队”**2个元素。此时head指针++2,于是就成了下图这样;
ok,黄色部分是空出来的富余内存部分,那么此时我再**“入队"一个元素,注意关键点来了,若是传统数组肯定就是扩容了,但若是循环数组,此时它的tail指针则是指向头部的富余内存的指针位置,避免了扩容,这就是所谓的"首尾相连"和"循环”**;
具体到ArrayDeque的源码就是下面这部分:
/**
* Inserts the specified element at the end of this deque.
* <p>
* <p>This method is equivalent to {@link #add}.
*
* @param e the element to add
* @throws NullPointerException if the specified element is null
*/
public void addLast(E e) {
System.out.println("arrayDeque addLast == " + "tail == " + tail);
// System.out.println("arrayDeque == "+"head == "+ head);
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ((tail = (tail + 1) & (elements.length - 1)) == head)//循环数组首尾相连
doubleCapacity();
}
内部维护一个长度为16的Object[]
/**
* Constructs an empty array deque with an initial capacity
* sufficient to hold 16 elements.
*/
public ArrayDeque() {
elements = new Object[16];
}
2.2.ArrayDeque代码测试:
下面我就结合上面的理论通过代码的方式来验证验证。
首先我复制一个ArrayDeque,目标是为了打log和调试数据,废话不多说直接看我新增的方法和Log:
/**
* 查看队列内部维护的数组的真实长度
*/
public int sizeOfArray() {
if (null != elements)
return elements.length;
return -1;
}
/**
* 查看头尾指针位置
*/
public void showHeadAndTail() {
System.out.println("tail == " + tail);
System.out.println("head == " + head);
}
如何验证?
我们知道ArrayDeque默认16长度数组,那么我先入队16个元素,然后出队2个元素,然后观察它的头尾指针以及是否扩容。
public static void main(String[] args) {
MyArrayDeque arrayDeque = new MyArrayDeque();
//先入满队元素
for (int i = 0; i < 15; i++) {
arrayDeque.addLast("1");
}
//观察
ob(arrayDeque);
//出队俩个元素
arrayDeque.poll();
arrayDeque.poll();
//观察
ob(arrayDeque);
//在入队2个元素
arrayDeque.add("1");
arrayDeque.add("1");
//观察
ob(arrayDeque);
//再入队1个元素
arrayDeque.add("1");
//观察
ob(arrayDeque);
}
观察结果:
sizeOfArray == 16 //开始入满队元素时,内部数组长度为16,未扩容
tail == 15 //尾巴下标为15
head == 0 //头下标为默认,因为没出队
//此时出队了2个元素
sizeOfArray == 16 //内部数组容器依旧没扩容,也没变
tail == 15 //由于是出队所以尾部下标没变
head == 2 //由于是出队,所以理所应当的头部下标++2
//此时由入队1个元素
sizeOfArray == 16 //由于之前出队了2个元素,所以内存空间有富余,根据循环数组的特性,tail应该往头部head富余的地方走
tail == 1 //如上述
head == 2 //之前出队时候就是2
//此时在入队元素
sizeOfArray == 32 //由于队列已满,再入队时就是扩容了
tail == 16 //16就已经是扩容后的下标了
head == 0
3.本文Demo
https://github.com/zj614android/designPattern/tree/master/app/src/main/java/com/zj/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/java%E5%AE%B9%E5%99%A8
4.Thanks
https://www.jianshu.com/p/5763d9c1c321