以下内容基于JDK11,有些文字来自官方API的翻译,另外是一些自己的理解。
Queue
这是一个队列接口,只定义常规队列的基本操作,注意不是所有的队列都是FIFO(先进先出)的,还可能是双端队列、优先级队列等。
public interface Queue<E> extends Collection<E> {
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}
这六个方法分成三组,每两个一组,每组提供的功能都会一致,但是有些差异。
add & offer
这两个方法是往队列中添加元素,但是可能抛出下面几种异常:
- ClassCastException:如果参数e的类型不能被加入到队列中,会抛出这种异常。
- NullPointerException:如果队列中不允许存在null值,而又准备添加null值时,就会抛出这种异常。
- IllegalArgumentException:如果参数e对象的某些属性导致不能被加入到队列中,会抛出这种异常。
- IllegalStateException:注意,这是add方法和offer方法的差异:当队列已满时,add方法会抛出该异常,但是offer方法不会。在需要提供空间严格性队列的时候,最好使用add方法,因为会第一时间抛出异常。
如果成功添加元素进入队列,那么这两个方法返回true,否则返回false。
remove & poll
这两个方法都对队列执行出队操作(对头出队),但是当队列为空时,remove方法会抛出NoSuchElementException异常,而poll方法只会返回null。
element & peak
这两个方法都是返回对头元素,而不执行出队操作。如果队列为空,element方法会抛出NoSuchElementException异常,但是peak方法则不会。
AbstractQueue
这是一个抽象类,实现Queue接口,并提供了下面几个方法的大概实现。
public abstract class AbstractQueue<E>
extends AbstractCollection<E>
implements Queue<E> {
protected AbstractQueue() {
}
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public void clear() {
while (poll() != null)
;
}
public boolean addAll(Collection<? extends E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
}
其中,add,remove,element三个方法是实现的Queue接口的,而且这三个方法里面,都是基于对应的offer,poll,peak方法实现的,然后根据返回值来决定是否抛出异常。这个抽象类不允许队列内部有null元素,继承该抽象类的具体类至少要实现offer,poll,peak, size,iterator这几个方法,而且在offer中不允许插入空值。
clear
该方法实现也很简单,就是while循环调用poll方法,直到返回false,说明已清空完毕。
addAll
注意,该方法的实现不允许添加自身或空集合,否则分别抛出代码中所示的两种异常。该方法基于for-each循环,然后调用add方法来添加元素。
ArrayQueue
后面补充
Deque
这是一个双端队列接口,允许在两端进行出队入队操作,Deque就是"double ended queue"的简写。该接口的大部分实现对元素数量没有限制,但也可以提供容量限制性队列实现。
Summary of Deque methods
对于insert, remove, examine操作,该接口定义了两组方法,一组是在某些失败原因情况下会抛出异常,另一组是不会抛异常,但是会返回特定值,如null。
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() |
Comparison of Queue and Deque methods
Deque接口继承与Queue接口,当Deque当作一般队列使用时,FIFO行为将会产生,在Queue中定义的方法完全等价于在Deque中定义的一些方法,对应关系如下表:
Queue Method | Equivalent Deque Method |
---|---|
add(e) | addLast(e) |
offer(e) | offerLast(e) |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
Comparison of Stack and Deque methods
另外,Deque还提供了栈的功能,即LIFO(后进先出)特性,应该使用这种方式而不要使用传统的Stack,因为其基于vector实现的。当作为栈来使用的时候,统一在队列头部进行入栈出栈操作。方法对应关系如下表所示:
Stack Method | Equivalent Deque Method |
---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | getFirst() |
ArrayDeque
注意和上面的ArrayQueue区分,ArrayDeque是Deque的实现类:
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
它是Deque的一种非固定空间大小的实现,是可以更改空间大小的。也就说明和ArrayList一样,能够自动扩容。不是线程安全的,不支持同步操作。不准插入null值。当作为队列来使用时,效率比LinkedList高,当作为栈时,效率比Stack高。
ArrayDeque定义的大部分操作运行时间都是常数级,以下几个是线性增长的:
- remove(Object)
- removeFirstOccurrence
- removeLastOccurrence
- contains
- iterator.remove
- 以及上面方法对应的批处理方法(如果存在)
和ArrayList一样,采用fail-fast机制来检测并发修改异常。在迭代器创建后的任何时候,采用非迭代器内部的remove方法来修改对象的话,那么将会抛出ConcurrentModificationException异常。但是这种机制也是尽力而为,不能真正确保线程安全。
属性字段
elements
transient Object[] elements;
这个字段是用来保存元素的数组,任何不持有元素的对象的该字段都为null。在有元素存储时,该数组总是最后至少存在一个空槽,值为null。
head
transient int head;
该字段保存队头元素在elements数组中的下标,所谓对头元素就是会被remove或pop方法移除的那个元素。如果队列为空(数组可能不为空),那么该值为[0, elements.length())区间中的任意整数值。
tail
transient int tail;
该值是下一次添加元素时的元素的下标,注意elements[tail] is always null。
MAX_ARRAY_SIZE
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
和ArrayList一样,ArrayDeque同样有这个属性。
构造器
public ArrayDeque() {
elements = new Object[16];
}
无参构造器,那么会数组会默认开辟16个长度的空间。
public ArrayDeque(int numElements) {
elements =
new Object[(numElements < 1) ? 1 :
(numElements == Integer.MAX_VALUE) ? Integer.MAX_VALUE :
numElements + 1];
}
传入一个值,表示初始化元素的数量。另外对numElements的操作在代码中很清晰,不在赘述。
public ArrayDeque(Collection<? extends E> c) {
this(c.size());
copyElements(c);
}
先调用上面第二个构造器, 以开辟空间,然后调用copyElements方法,复制元素。
private void copyElements(Collection<? extends E> c) {
c.forEach(this::addLast);
}
可以看到是调用的for-each循环,依次执行addLast入队。
入队
在队头入队
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
final Object[] es = elements;
es[head = dec(head, es.length)] = e;
if (head == tail)
grow(1);
}
在决定插入元素的下标的时候,调用了dec方法:
static final int dec(int i, int modulus) {
if (--i < 0) i = modulus - 1;
return i;
}
如果head的值已经为0了,那么下一次head的值就应该等于最后一个数组元素的下标。
添加元素后,如果发现head和tail相等了,那么调用grow方法就行扩容,后面会讲到。
在队尾入队
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
final Object[] es = elements;
es[tail] = e;
if (head == (tail = inc(tail, es.length)))
grow(1);
}
调用了inc方法来处理tail:
static final int inc(int i, int modulus) {
if (++i >= modulus) i = 0;
return i;
}
批量入队
public boolean addAll(Collection<? extends E> c) {
final int s, needed;
if ((needed = (s = size()) + c.size() + 1 - elements.length) > 0)
grow(needed);
copyElements(c);
return size() > s;
}
如果空间不够就扩容,然后调用copyElements复制元素。在上面构造器部分,第三个构造器也是对容器批量入队,也调用了这个方法,方法内部是通过调用的addLast方法来实现的。
传统add入队
public boolean add(E e) {
addLast(e);
return true;
}
调用addLast方法,符合传统FIFO特性。
offer入队
另外,offer系列方法也可以入队,但是调用的都是上面的两个方法:
public boolean offer(E e) {
return offerLast(e);
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
这里是offer依靠add来实现,和AbstractQueue的实现不同,刚好相反。
push入队
public void push(E e) {
addFirst(e);
}
扩容
ArrayDeque方法通过grow方法来实现的:
private void grow(int needed) {
// overflow-conscious code
final int oldCapacity = elements.length;
int newCapacity;
// Double capacity if small; else grow by 50%
int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);
if (jump < needed
|| (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)
newCapacity = newCapacity(needed, jump);
final Object[] es = elements = Arrays.copyOf(elements, newCapacity);
// Exceptionally, here tail == head needs to be disambiguated
if (tail < head || (tail == head && es[head] != null)) {
// wrap around; slide first leg forward to end of array
int newSpace = newCapacity - oldCapacity;
System.arraycopy(es, head,
es, head + newSpace,
oldCapacity - head);
for (int i = head, to = (head += newSpace); i < to; i++)
es[i] = null;
}
}
内部有一个分支调用了newCapacity方法:
private int newCapacity(int needed, int jump) {
final int oldCapacity = elements.length, minCapacity;
if ((minCapacity = oldCapacity + needed) - MAX_ARRAY_SIZE > 0) {
if (minCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
return Integer.MAX_VALUE;
}
if (needed > jump)
return minCapacity;
return (oldCapacity + jump - MAX_ARRAY_SIZE < 0)
? oldCapacity + jump
: MAX_ARRAY_SIZE;
}
这里的Capactity和ArrayList中的Capacity方法行为类似,这里传入两个参数,第一个是确确实实需要的量,第二个是依赖于当前数组的长度的量,在grow方法的第6行中定义,如果jump满足不了needed或者是旧容量大小加上jump大于了MAX_ARRAY_SIZE, 就会调用newCapacity方法,进一步判断,如果oldCapacity + needed溢出了,说明需要的容量太大了,无法满足,直接抛出异常,否则返回Integer的最大值(其实离溢出也不远了)。如果容量需要没那么大,而且需求量大于jump值,直接返回旧容量加上需求量,否则比较旧容量加上jump值与MAX_ARRAY_SIZE。
说了这么多,其实在需求量没那么大的情况下,如果需求量小于系统提供的扩容量,那么就按照推荐的jump值大小进行扩容,否则就按照实际需要多少就扩容多少。
grow返回得到扩容量后,调用Arrays.copyOf复制数组。如果出现意外情况(两种情况):
- tail < head:队尾在前,对头在后
- tail == head && es[head] != null:当“tail==head”时,有两种情况,一是队列为空,二是队列已满。上面讲到ArrayDeque对象的elements数组必须要求有一个空值(tail下标表示的那个位置)。那么这里属性不为空,就说明被新加入的元素覆盖掉了,调用System.arraycopy复制数组,然后修改head的值, 并把之前被移走的空间赋值为null。
确实复杂,慢慢分析~
移除
移除并返回被移除的对象。
队头出队
public E pollFirst() {
final Object[] es;
final int h;
E e = elementAt(es = elements, h = head);
if (e != null) {
es[h] = null;
head = inc(h, es.length);
}
return e;
}
队尾出队
public E pollLast() {
final Object[] es;
final int t;
E e = elementAt(es = elements, t = dec(tail, es.length));
if (e != null)
es[tail = t] = null;
return e;
}
默认出队对头
public E poll() {
return pollFirst();
}
默认调用的是pollFirst,符合传统的FIFO特性。
remove出队
public E remove() {
return removeFirst();
}
public E removeFirst() {
E e = pollFirst();
if (e == null)
throw new NoSuchElementException();
return e;
}
public E removeLast() {
E e = pollLast();
if (e == null)
throw new NoSuchElementException();
return e;
}
pop出队
public E pop() {
return removeFirst();
}
移除给定对象第一次出现的元素
给定一个对象,根据特定顺序,移除遍历到该对象第一次出现的元素。
从头到尾
public boolean removeFirstOccurrence(Object o) {
if (o != null) {
final Object[] es = elements;
for (int i = head, end = tail, to = (i <= end) ? end : es.length;
; i = 0, to = end) {
for (; i < to; i++)
if (o.equals(es[i])) {
delete(i);
return true;
}
if (to == end) break;
}
}
return false;
}
从尾到头
public boolean removeLastOccurrence(Object o) {
if (o != null) {
final Object[] es = elements;
for (int i = tail, end = head, to = (i >= end) ? end : 0;
; i = es.length, to = end) {
for (i--; i > to - 1; i--)
if (o.equals(es[i])) {
delete(i);
return true;
}
if (to == end) break;
}
}
return false;
}
这里面都调用了delete方法:
boolean delete(int i) {
final Object[] es = elements;
final int capacity = es.length;
final int h, t;
// number of elements before to-be-deleted elt
final int front = sub(i, h = head, capacity);
// number of elements after to-be-deleted elt
final int back = sub(t = tail, i, capacity) - 1;
if (front < back) {
// move front elements forwards
if (h <= i) {
System.arraycopy(es, h, es, h + 1, front);
} else { // Wrap around
System.arraycopy(es, 0, es, 1, i);
es[0] = es[capacity - 1];
System.arraycopy(es, h, es, h + 1, front - (i + 1));
}
es[h] = null;
head = inc(h, capacity);
return false;
} else {
// move back elements backwards
tail = dec(t, capacity);
if (i <= tail) {
System.arraycopy(es, i + 1, es, i, back);
} else { // Wrap around
System.arraycopy(es, i + 1, es, i, capacity - (i + 1));
es[capacity - 1] = es[0];
System.arraycopy(es, 1, es, 0, t - 1);
}
es[tail] = null;
return true;
}
}
返回元素
static final <E> E elementAt(Object[] es, int i) {
return (E) es[i];
}
这个elementAt方法是所有获取元素的公共方法。
返回队头元素
public E getFirst() {
E e = elementAt(elements, head);
if (e == null)
throw new NoSuchElementException();
return e;
}
public E peekFirst() {
return elementAt(elements, head);
}
public E peek() {
return peekFirst();
}
如果队列为空,第一个方法要抛出异常,而第二个返回null值。
返回队尾元素
public E getLast() {
final Object[] es = elements;
E e = elementAt(es, dec(tail, es.length));
if (e == null)
throw new NoSuchElementException();
return e;
}
public E peekLast() {
final Object[] es;
return elementAt(es = elements, dec(tail, es.length));
}
如果队列为空,第一个方法要抛出异常,而第二个返回null值。
element返回
Deque的element方法对应ArrayDeque的getFirst方法:
public E element() {
return getFirst();
}
迭代器
后面补充