Java中的Queue

以下内容基于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;
    }
}

其中,addremoveelement三个方法是实现的Queue接口的,而且这三个方法里面,都是基于对应的offerpollpeak方法实现的,然后根据返回值来决定是否抛出异常。这个抽象类不允许队列内部有null元素,继承该抽象类的具体类至少要实现offerpollpeak, sizeiterator这几个方法,而且在offer中不允许插入空值。

clear

该方法实现也很简单,就是while循环调用poll方法,直到返回false,说明已清空完毕。

addAll

注意,该方法的实现不允许添加自身或空集合,否则分别抛出代码中所示的两种异常。该方法基于for-each循环,然后调用add方法来添加元素。

ArrayQueue

后面补充

Deque

这是一个双端队列接口,允许在两端进行出队入队操作,Deque就是"double ended queue"的简写。该接口的大部分实现对元素数量没有限制,但也可以提供容量限制性队列实现。

Summary of Deque methods

对于insert, remove, examine操作,该接口定义了两组方法,一组是在某些失败原因情况下会抛出异常,另一组是不会抛异常,但是会返回特定值,如null

Summary of Deque methods
First Element (Head) Last Element (Tail)
Throws exceptionSpecial valueThrows exceptionSpecial value
InsertaddFirst(e)offerFirst(e)addLast(e)offerLast(e)
RemoveremoveFirst()pollFirst()removeLast()pollLast()
ExaminegetFirst()peekFirst()getLast()peekLast()

Comparison of Queue and Deque methods

Deque接口继承与Queue接口,当Deque当作一般队列使用时,FIFO行为将会产生,在Queue中定义的方法完全等价于在Deque中定义的一些方法,对应关系如下表:

Comparison of Queue and Deque methods
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实现的。当作为栈来使用的时候,统一在队列头部进行入栈出栈操作。方法对应关系如下表所示:

Comparison of Stack and Deque methods
Stack Method Equivalent Deque Method
push(e)addFirst(e)
pop()removeFirst()
peek()getFirst()

ArrayDeque

注意和上面的ArrayQueue区分,ArrayDequeDeque的实现类:

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数组中的下标,所谓对头元素就是会被removepop方法移除的那个元素。如果队列为空(数组可能不为空),那么该值为[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的值就应该等于最后一个数组元素的下标。

添加元素后,如果发现headtail相等了,那么调用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;
}

这里的CapactityArrayList中的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返回

Dequeelement方法对应ArrayDequegetFirst方法:

public E element() {
    return getFirst();
}

迭代器

后面补充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值