Java1.8-ArrayDeque源码解析

概述

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


栈与队列

  而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<E> extends Queue<E> {
}

public class ArrayDeque<E> extends AbstractCollection<E>
                           implements Deque<E>, 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容