Java1.8-ArrayDeque源码解析

概述

  首先解释下,Queue(队列),队列是一种先进先出的数据结构(First In First Out,FIFO),经常和栈一起讨论,而栈是一种先进后出的数据结构(FILO)。队列有头指针head和尾指针tail,数据从队尾入队,从队头出队。费尽心力,画了一张图,真是灵魂画手。


Java1.8-ArrayDeque源码解析_第1张图片
栈与队列

  而Deque被称为双端队列,队列的原则是只能一头入队,一头出队,而双端队列则是两端都可以入队和出队的队列。队列和栈两者很像,所以像我们今天要学习的这个ArrayDeque,就同时实现了它们两个的功能,可以称为是一种基于数组的双端队列。

我们可以先看下文档:
https://docs.oracle.com/javase/8/docs/api/java/util/ArrayDeque.html
通过文档和我们实际使用,我们大概知道ArrayDeque队列有以下性质:

  1. 首先该类是JDK1.6才引入的,算引入的比较晚了,在以前版本的话,要使用队列的功能,一般要用LinkedList,或者自己封装实现;
  2. 队列不是线程安全的,并且不允许元素为空;
  3. 当用作堆栈时,这个类的速度可能比堆栈快,当用作队列时,它比LinkedList更快;
  4. 队列底层是由数组来实现的,没有容量限制,并且数组容量可以自动进行扩容。
  5. 队列有两个指针,头指针和尾指针,我们对队列进行的操作一般都是通过操作这两个指针进行的。
  6. 为了满足可以同时在数组两端进行插入和移除操作,该数组还必须是一个循环的数组,也就是说数组的任何一点都可以看作起点和终点。

继承结构和属性

public interface Deque extends Queue {
}

public class ArrayDeque extends AbstractCollection
                           implements Deque, Cloneable, Serializable {
    // 队列中存放数据的数组
    transient Object[] elements;
    // 头指针,或头索引
    transient int head;
    // 尾指针,或尾索引
    transient int tail;
    // 最小初始化数组容量
    private static final int MIN_INITIAL_CAPACITY = 8;
}

从继承结构我们可以看出,Deque是继承自Queue的。

构造方法
/**
 * 默认构造方法,初始容量为16
 */
public ArrayDeque() {
    elements = new Object[16];
}

/**
 * 用户指定数组容量
 */
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

private void allocateElements(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    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];
}

  其中allocateElements方法是计算实际所需容量的。首先,numElements是用户指定的队列大小,allocateElements中这么多无符号右移运算操作,只是为了计算离numElements最近的且大于numElements的2的次方。比如,用户指定队列是15,那最终计算的结果就是2的4次方16。因为ArrayDeque规定,数组容量大小必须是2的幂。

The capacity of the deque is the length of this array, which is always a power of two.

  首先,我们不妨想一下,对于用户指定的大小numElements,那离它最近的且大于numElements的2的次方应该是多少呢。

  仔细想了之后,我们就会发觉,这个值应该是我们就将numElements转为二进制后,该二进制所有位都是1,然后再加1的值。比如numElements是10001,那距离它最近的那个值就将是11111 + 1。

  1. initialCapacity |= (initialCapacity >>> 1); 这里是将高2位设置为1;
  2. initialCapacity |= (initialCapacity >>> 2); 这里是将高4位设置为1;
  3. initialCapacity |= (initialCapacity >>> 4); 这里是将高8位设置为1;
  4. initialCapacity |= (initialCapacity >>> 8); 这里是将高16位设置为1;
  5. initialCapacity |= (initialCapacity >>> 16); 这里是将高32位,也就是int的全部位设置为1。
  6. 这样,最终所有的位都变为了1,然后再加1就是2的次幂了。
addFirst和addLast

分别在头部和尾部插入元素:

/**
 * 在队列头部插入元素,也就是在head之前位置插入元素
 */
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    // 如果两者相等,说明数组已经放满了元素,需要扩容
    if (head == tail)
        doubleCapacity();
}

/**
 * 在队列尾部插入元素,也就是在tail位置插入元素
 */
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

  在头部插入元素,也就是在head之前,head自减之后然后进行位与操作。因为elements.length一直是2的次幂,所以elements.length-1之后的二进制全是1,所以这里就相当于进行的是取余操作。而在尾部插入的话,其实和头部类似,只不过是直接插入到了尾部,然后tail加1。添加完之后,两个方法都会检测数组是否已满,如果满了,则进行扩容操作。这里判断是否已满的条件是head == tail。

doubleCapacity方法
private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

这里,大概分以下几个步骤:

  1. 检测tail是否等于head;
  2. 创建一个原数组容量2倍的新数组;
    3 将原数组从head位置一分为二,分别拷贝到新数组中;比如数组容量16,head位置是7,则先将原数组下标从7开始,拷贝16-7个元素到新的下标从0开始,元素个数为16-7的数组中。然后将原数组下标从0开始到7的元素拷贝到新数组下标从(16-7)开始,元素个数是7的数组中。这么做的目的就是为了拷贝到新数组的时候保持元素原来的顺序。
  3. 重新确定头尾指针的位置。

  与之相关联的两个方法 offerFirstofferLast 方法,和addFirst,addLast唯一不同的是提供了是否添加成功的返回值。当然,都是返回的true。

pollFirst和pollLast
public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    // Element is null if deque empty
    if (result == null)
        return null;
    elements[h] = null;     // Must null out slot
    head = (h + 1) & (elements.length - 1);
    return result;
}

public E pollLast() {
    int t = (tail - 1) & (elements.length - 1);
    @SuppressWarnings("unchecked")
    E result = (E) elements[t];
    if (result == null)
        return null;
    elements[t] = null;
    tail = t;
    return result;
}

  这两个方法其实没什么好说的,就是分别移除头部和尾部的元素并返回该元素。另外,removeFirstremoveLast 这两个方法也是移除并返回元素,和pollFirst不同的是该接口会判断返回的值是否为null,如果为null,就抛出异常。也就是说,该方法不允许要移除的元素为null。
  还有两个方法,peekFirst,peekLast也是返回头元素或尾元素,只是不移除元素。

remove,removeFirstOccurrence方法
public boolean remove(Object o) {
    return removeFirstOccurrence(o);
}

/**
 * 遍历查找数据,然后在队列中删除该数据
 */
public boolean removeFirstOccurrence(Object o) {
    if (o == null)
        return false;
    int mask = elements.length - 1;
    int i = head;
    Object x;
    while ( (x = elements[i]) != null) {
        if (o.equals(x)) {
            delete(i);
            return true;
        }
        i = (i + 1) & mask;
    }
    return false;
}
  1. remove方法是删除队列中的指定元素,内部调用的是removeFirstOccurrence。这个方法是删除指定元素第一次出现的位置,该方法内部是通过遍历查询该元素,遍历的方向是从head下标到tail下标。
  2. 同样,和该方法类似的还有removeLastOccurrence方法,该方法是删除指定元素最后一次出现的位置,计算方式和removeFirstOccurrence恰好相反。
  3. 根据队列的定义,队列中一般不建议删除既非队头也非队尾的元素,要删除一般都是使用removeFirst或pollFirst就满足了,所以这两个方法并不经常用,并且这两个方法的查询都是线性时间。
size和isEmpty
/**
 * 计算队列的长度
 */
public int size() {
    return (tail - head) & (elements.length - 1);
}

/**
 * 通过判断head是否等于tail来判断队列是否为空
 */
public boolean isEmpty() {
    return head == tail;
}

  至于getFirst,getLast,peekFirst,peekLast这些就是获取元素的方法,当然ArrayDeque还实现了队列Queue的各个方法。比如add,offer,remove等等,这些方法基本上都是调用以上Deque的方法实现的,没什么好说的。
  ArrayDeque也提供了一些转换为数组的方法,底层是通过System.arraycopy实现的,不过这些都挺简单的,就不一一说了。

总结

  1. 一般情况下,ArrayDeque的效率还是挺高的,大多数方法都是在常数时间内运行,除了一些例如removeFirstOccurrence,removeLastOccurrence和一些批量操作是线性时间。
  2. 虽然LinkedList也提供了Deque的实现,不过官方还是建议我们使用ArrayDeque来实现队列的功能。所以如果我们有类似的功能,建议使用ArrayDeque来实现。

本文参考自:
ArrayDeque源码分析
https://docs.oracle.com/javase/8/docs/api/java/util/ArrayDeque.html

你可能感兴趣的:(Java1.8-ArrayDeque源码解析)