Queue简介
Queue,即队列,是一个先进先出(First In First Out, FIFO)
的数据结构,在JCF中也有很好的支持。和之间的分析步骤一样,我们先看一下接口的设计,再深入到具体实现类。
1. Queue接口
相对于List
接口来说,Queue
接口中支持的方法相对就比较少了,接口代码如下:
1 | public interface Queue<E> extends Collection<E> { |
Queue接口不支持存放null
元素。
这里有个问题需要关注,原来自己用的时候也没仔细想过,为什么好像有两套功能一样的方法?即add()
和offer()
,remove()
和poll()
,element()
和peek()
的区别是什么?
原来,二者区别的就是在操作失败时的处理上,就好比有两套约定,在Collection
接口中规定,如果add()
操作失败,则抛出相应的异常; 而offer()
方法则规定,如果操作失败,返回false。remove()
和poll()
,element()
和peek()
的区别类似。
方法 | 返回值处理 | 方法 | 返回值处理 |
---|---|---|---|
add() | 失败抛出异常,有IllegalStateException ,ClassCastException 和NullPointerException 三种异常。 |
offer() | 失败返回false |
remove() | 失败抛出NoSuchElementException 异常。 |
poll() | 失败返回null |
element() | 失败抛出NoSuchElementException 异常。 |
peek() | 失败返回null |
2. Deque接口
说完Queue
接口,很自然地就会涉及到Deque
接口。Deque,即Double End Queue,双端队列,顾名思义,就是可以在队头和队尾进行插入或删除操作,我们看下接口的设计,这里我们也只列举一部分:
1 | public interface Deque<E> extends Queue<E> { |
Okay,和我们的猜想一模一样,至于为什么也有两套一样的方法嘛,和上面在Queue
中的解释完完全全一样。
3. ArrayDeque实现类
ArrayDeque
类是实现了Deque
接口的,常见的使用方法如下:
1 | Deque<String> dq=new ArrayDeque<>(); |
我们一步步分析一下这个类的具体实现源代码,先看下这个类实现的接口和类属性。
1 | public class ArrayDeque<E> extends AbstractCollection<E> |
3.1 构造函数
ArrayDeque
的构造函数有三个,方法签名分别是ArrayDeque()
、ArrayDeque(int numElements)
和ArrayDeque(Collection<? extends E> c)
,我们一个个来分析一下。
ArrayDeque()
此构造函数的实现十分的简单粗暴,直接缺省地把数组的容量声明为16。
1
2
3public ArrayDeque() {
elements = new Object[16];
}ArrayDeque(int numElements)
这个构造函数的功能是创建一个容量为用户指定初始大小的双端队列,私有方法如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public ArrayDeque(int numElements) {
allocateElements(numElements);
}
// 开辟空间函数
private void allocateElements(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY; // 最小的初始化容量为8
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
if (numElements >= initialCapacity) { // 如果用户指定的初始化容量大于8
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1); // >>>是不带符号右移 ,>>是带符号右移
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++; // 找到这样一个k,使得2^k > numElements
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
elements = new Object[initialCapacity];
}可以看到,关键就在于
allocateElements()
函数。该方法首先会判断用户指定的初始容量numElements
是否比MIN_INITIAL_CAPACITY
(也就是8)小,如果比8小,就直接开辟8个空间;如果比8大的话,代码会找到这样一个$k$,使得$2^k > numElements$,也就是上面不断右移的作用,这里举个例子,因为在Java中一个int是用4个字节来表示的,这里举例只考虑了4个bit的情况,对应则只需要右移2次。假设用户声明的大小为5,即
initialCapacity = 0101
。initialCapacity |= (initialCapacity >>> 1)
后,initialCapacity = 0111
;再次右移initialCapacity |= (initialCapacity >>> 2)
后,initialCapacity = 0111
;最后initialCapacity++
,此时initialCapacity = 1000
,对应的十进制数为-8,于是经过if判断再次右移一位,initialCapacity
又等于 0100,即十进制4。也就是说,实际队列能分配的最大初始容量其实也就是$2^{n-2}$,其中$n$为二进制位数,即在
Deque
中,最大也就允许用户初始化容量为$2^{30}$。ArrayDeque(Collection<? extends E> c)
这个构造函数就比较简单了,和之前在
List
中的分析差不多,代码如下。1
2
3
4public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
3.2 add相关操作
我们从最简单的add(E e)
开始往下分析。
1 | public boolean add(E e) { |
从源码我们就可以知道,add(E e)
方法默认是在队尾插入的(这也是必须的,因为这就是朴素队列的性质!),然后会调用addLast(E e)
方法,在这里,是不允许插入null元素的,而在List
中是允许存在null对象的,这个需要注意。插入一个元素后,如果队列满了,会进行扩容操作,这里用的是双倍扩容,并会相应地把原队头右边的r个元素搬到新数组从0开头的位置,并把原队尾左边的p个元素搬到新数组从r开头的位置。
有了上面的基础,我们再来看下addFirst(E e)
方法,注意,这个方法只有在双端队列中才有,朴素的队列是不支持在队头插入元素的,这相当于我们平时生活中的”插队“操作,而且是直接插到最前面去。
1 | public void addFirst(E e) { |
总结:add操作的时间复杂度为$O(1)$。 如果碰到要扩容的情况,则空间复杂度为$O(N)$,其中$N$为队列中当前容量。
3.3 offer相关操作
offer()
就是对add()
函数的一个包装,即在队尾添加一个元素。
1 | // 操作成功返回true,如果入参e是null,还是会抛出NullPointerException异常 |
很自然,offerFrst(E e)
则是对addFrst(E e)
的一个包装了。
1 | public boolean offerFirst(E e) { |
3.4 remove相关操作
删除相关的操作也比较多,有朴素队列中的remove()
,默认是从队头删除一个。在实现了Deque
接口的类中,你也可以在队尾删除一个元素,就好比一个人排队,排着排着等得不耐烦就溜了。我们也简单看下源码实现方式。
先看下removeLast()
函数:
1 | // 从队尾删除一个元素 |
这里有个地方我们需要注意,在队列中,tail下标指向的那个元素其实是下一次添加时存放的位置,所以tail的上一个位置才是最后一个元素,于是在pollLast()
函数中的操作就是先找到队尾那个元素,并保留。然后把队尾向前移动一个单位,最后返回之前保留的原队尾元素。
当然,朴素队列中的删除操作默认是从队首删除,代码也很类似。
1 | public E remove() { |
poll()
系列方法则是对remove()
系列方法的包装。
总结:remove操作时间复杂度为$O(1)$,删除时是不会涉及到释放容量这一说法的。
3.5 peek相关操作
peek()
就是返回队列的队首元素了,当返回值为null的时候,说明队列是空的。
1 | public E peek() { |
peekFirst()
、peekLast()
的分析和前面类似,就不再赘述。element()
系列方法则是对`peek()
系列方法的包装。
4. LinkedList实现类
LinkedList
即可以作为朴素队列Queue
接口的实现类来使用,也可以作为双端队列Deque
接口的实现类来使用。LinkedList
的源码分析可以参阅之前的文章>>Java集合-List篇<<,LinkedList
作为朴素队列来使用的代码如下:
1 | Queue<String> q=new LinkedList<>(); |
这里我们只再次看下相关方法是如何包装的。
1 | public class LinkedList<E> |
总结:对于一个底层用LinkedList
实现的队列,其删除和添加操作的时间复杂度都是$O(1)$,空间复杂度也都是$O(1)$。