Java集合源码分析之Queue(三):ArrayDeque_一点课堂(多岸学院)

在介绍了QueueDeque概念以后,这是要进行分析的第一个实现类。ArrayDeque可能你们用的都比较少,但其实现里有许多亮点仍是值得咱们关注的。java

Deque的定义为double ended queue,也就是容许在两侧进行插入和删除等操做的队列。这个定义看起来很简单,那么咱们怎么实现它呢?咱们最容易想到的就是使用双向链表。咱们在前文介绍过单链表,其每一个数据单元都包含一个数据元素和一个指向下一个元素位置的指针next,这样的链表只能从前向后遍历。若是咱们要把它变成双向的,只须要添加一个能够指向上一个元素位置的指针previous,同时记录下其尾节点便可。LinkedList的实现就是采用了这一实现方案。数组

ArrayDeque又是什么,它的结构又是怎样的呢?咱们先看下其文档吧:安全

Resizable-array implementation of the Deque interface. Array deques have no capacity restrictions; they grow as necessary to support usage. They are not thread-safe; in the absence of external synchronization, they do not support concurrent access by multiple threads. Null elements are prohibited. This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.多线程

文档中并无过多的介绍实现细节,但说它是Resizable-array implementation of the Deque interface,也就是用可动态调整大小的数组来实现了Deque,听起来是否是像ArrayList?但ArrayDeque对数组的操做方式和ArrayList有较大的差异。下面咱们就深刻其源码看看它是如何巧妙的使用数组的,以及为什么说函数

faster than Stack when used as a stack, and faster than LinkedList when used as a queue.学习

构造函数与重要成员变量

ArrayDeque共有四个成员变量,其中两个咱们在分析ArrayList时已经见过了,还有两个咱们须要认真研究一下:this

//存放元素,长度和capacity一致,而且老是2的次幂
//这一点,咱们放在后面解释
transient Object[] elements; 

//capacity最小值,也是2的次幂
private static final int MIN_INITIAL_CAPACITY = 8;

//标记队首元素所在的位置
transient int head;

//标记队尾元素所在的位置
transient int tail;

其构造函数共有三个:线程

//默认构造函数,将elements长度设为16,至关于最小capacity的两倍
public ArrayDeque() {
    elements = new Object[16];
}

//带初始大小的构造
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

//从其余集合类导入初始数据
public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

这里看到有两个构造函数都用到了allocateElements方法,这是一个很是经典的方法,咱们接下来就先重点研究它。指针

寻找最近的2次幂

在定义elements变量时说,其长度老是2的次幂,但用户传入的参数并不必定符合规则,因此就须要根据用户的输入,找到比它大的最近的2次幂。好比用户输入13,就把它调整为16,输入31,就调整为32,等等。考虑下,咱们有什么方法能够实现呢?rest

来看下ArrayDeque是怎么作的吧:

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];
}

看到这段迷之代码了吗?在HashMap中也有一段相似的实现。但要读懂它,咱们须要先掌握如下几个概念:

  • 在java中,int的长度是32位,有符号int能够表示的值范围是 (-2)^31^ 到 2^31^-1,其中最高位是符号位,0表示正数,1表示负数。
  • >>>:无符号右移,忽略符号位,空位都以0补齐。
  • |:位或运算,按位进行或操做,逢1为1。

咱们知道,计算机存储任何数据都是采用二进制形式,因此一个int值为80的数在内存中多是这样的:

0000 0000 0000 0000 0000 0000 0101 0000

比80大的最近的2次幂是128,其值是这样的:

0000 0000 0000 0000 0000 0000 1000 0000

咱们多找几组数据就能够发现规律:

  • 每一个2的次幂用二进制表示时,只有一位为 1,其他位均为 0(不包含符合位)
  • 要找到比一个数大的2的次幂(在正数范围内),只须要将其最高位左移一位(从左往右第一个 1 出现的位置),其他位置 0 便可。

但从实践上讲,没有可行的方法可以进行以上操做,即便经过&操做符能够将某一位置 0 或置 1,也没法确认最高位出现的位置,也就是基于最高位进行操做不可行。

但还有一个很整齐的数字能够被咱们利用,那就是 2^n^-1,咱们看下128-1=127的表示形式:

0000 0000 0000 0000 0000 0000 0111 1111

把它和80对比一下:

0000 0000 0000 0000 0000 0000 0101 0000 //80 0000 0000 0000 0000 0000 0000 0111 1111 //127

能够发现,咱们只要把80从最高位起每一位全置为1,就能够获得离它最近且比它大的 2^n^-1,最后再执行一次+1操做便可。具体操做步骤为(为了演示,这里使用了很大的数字): 原值:

0011 0000 0000 0000 0000 0000 0000 0010

  1. 无符号右移1位

0001 1000 0000 0000 0000 0000 0000 0001

  1. 与原值|操做:

0011 1000 0000 0000 0000 0000 0000 0011

能够看到最高2位都是1了,也仅能保证前两位为1,这时就能够直接移动两位

  1. 无符号右移2位

0000 1110 0000 0000 0000 0000 0000 0000

  1. 与原值|操做:

0011 1110 0000 0000 0000 0000 0000 0011

此时就能够保证前4位为1了,下一步移动4位

  1. 无符号右移4位

0000 0011 1110 0000 0000 0000 0000 0000

  1. 与原值|操做:

0011 1111 1110 0000 0000 0000 0000 0011

此时就能够保证前8位为1了,下一步移动8位

  1. 无符号右移8位

0000 0000 0011 1111 1110 0000 0000 0000

  1. 与原值|操做:

0011 1111 1111 1111 1110 0000 0000 0011

此时前16位都是1,只须要再移位操做一次,便可把32位都置为1了。

  1. 无符号右移16位

0000 0000 0000 0000 0011 1111 1111 1111

  1. 与原值|操做:

0011 1111 1111 1111 1111 1111 1111 1111

  1. 进行+1操做:

0100 0000 0000 0000 0000 0000 0000 0000

如此通过11步操做后,咱们终于找到了合适的2次幂。写成代码就是:

initialCapacity |= (initialCapacity >>>  1);
    initialCapacity |= (initialCapacity >>>  2);
    initialCapacity |= (initialCapacity >>>  4);
    initialCapacity |= (initialCapacity >>>  8);
    initialCapacity |= (initialCapacity >>> 16);
    initialCapacity++;

不过为了防止溢出,致使出现负值(若是把符号位置为1,就为负值了)还须要一次校验:

if (initialCapacity < 0)   // Too many elements, must back off
     initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements

至此,初始化的过程就完毕了。

重要操做方法

add分析

Deque主要定义了一些关于First和Last的操做,如add、remove、get等。咱们看看它是如何实现的吧。

//在队首添加一个元素,非空
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

//在队尾添加一个元素,非空
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

这里,又有一段迷之代码须要咱们认真研究了,这也是ArrayDeque值得咱们研究的地方之一,经过位运算提高效率。

elements[head = (head - 1) & (elements.length - 1)] = e;

很明显这是一个赋值操做,并且应该是给head以前的位置赋值,因此head = (head - 1)是合理的操做,那这个& (elements.length - 1)又表示什么呢?

在以前的定义与初始化中,elements.length要求为2的次幂,也就是 2^n^ 形式,那这个& (elements.length - 1)也就是 2^n^-1 了,在内存中用二进制表示就是从最高位起每一位都是1。咱们还以以前的127为例:

0000 0000 0000 0000 0000 0000 0111 1111

&就是按位与,全1才为1。那么任意一个正数和127进行按位与操做后,都只有最右侧7位被保留了下来,其余位所有置0(除符号位),而对一个负数而言,则会把它的符号位置为0,&操做后会变成正数。好比-1的值是1111 ... 1111(32个1),和127按位操做后结果就变成了127 。因此,对于正数它就是取模,对于负数,它就是把元素插入了数组的结尾。因此,这个数组并非向前添加元素就向前扩展,向后添加就向后扩展,它是循环的,相似这样:

file

初始时,head与tail都指向a[0],这时候数组是空的。当执行addFirst()方法时,head指针移动一位,指向a[elements.length-1],并赋值,也就是给a[elements.length-1]赋值。当执行addLast()操做时,先给a[0]赋值,再将tail指针移动一位,指向a[1]。因此执行完以后head指针位置是有值的,而tail位置是没有值的。

随着添加操做执行,数组总会占满,那么怎么判断它满了而后扩容呢?首先,若是head==tail,则说明数组是空的,因此在添加元素时必须保证head与tail不相等。假如如今只有一个位置能够添加元素了,相似下图:

file

此时,tail指向了a[8],head已经填充到a[9]了,只有a[8]是空闲的。很显然,不论是addFirst仍是addLast,再添加一个元素后都会致使head==tail。这时候就不得不扩容了,由于head==tail是判断是否为空的条件。扩容就比较简单了,直接翻倍,咱们看代码:

private void doubleCapacity() {
    //只有head==tail时才能够扩容
    assert head == tail;
    int p = head;
    int n = elements.length;
    //在head以后,还有多少元素
    int r = n - p; // number of elements to the right of p
    //直接翻倍,由于capacity初始化时就已是2的倍数了,这里无需再考虑
    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;
}

分析完add,那么get以及remove等都大同小异,感兴趣能够查看源码。咱们还要看看在Deque中定义的removeFirstOccurrenceremoveLastOccurrence方法的具体实现。

Occurrence相关

removeFirstOccurrenceremoveLastOccurrence分别用于找到元素在队首或队尾第一次出现的位置并删除。其实现原理是一致的,咱们分析一个便可:

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;
}

这里就是遍历全部元素,而后经过delete方法删除,咱们看看delete实现:

private boolean delete(int i) {
    //检查
    checkInvariants();
    final Object[] elements = this.elements;
    final int mask = elements.length - 1;
    final int h = head;
    final int t = tail;
    //待删除元素前面的元素个数
    final int front = (i - h) & mask;
    //待删除元素后面的元素个数
    final int back  = (t - i) & mask;

    // Invariant: head <= i < tail mod circularity
    //确认 i 在head和tail之间
    if (front >= ((t - h) & mask))
        throw new ConcurrentModificationException();

    // Optimize for least element motion
    //尽可能最少操做数据
    //前面数据比较少
    if (front < back) {
        if (h <= i) {
            //这时 h 和 i 之间最近距离没有跨过位置0
            System.arraycopy(elements, h, elements, h + 1, front);
        } else { // Wrap around
            System.arraycopy(elements, 0, elements, 1, i);
            elements[0] = elements[mask];
            System.arraycopy(elements, h, elements, h + 1, mask - h);
        }
        elements[h] = null;
        head = (h + 1) & mask;
        return false;
    } else {
        if (i < t) { // Copy the null tail as well
         //这时 t 和 i 之间最近距离没有跨过位置0
            System.arraycopy(elements, i + 1, elements, i, back);
             tail = t - 1;
        } else { // Wrap around
            System.arraycopy(elements, i + 1, elements, i, mask - i);
            elements[mask] = elements[0];
            System.arraycopy(elements, 1, elements, 0, t);
            tail = (t - 1) & mask;
        }
        return true;
    }
}

总结

ArrayDeque经过循环数组的方式实现的循环队列,并经过位运算来提升效率,容量大小始终是2的次幂。当数据充满数组时,它的容量将翻倍。做为Stack,由于其非线程安全因此效率高于java.util.Stack,而做为队列,由于其不须要结点支持因此更快(LinkedList使用Node存储数据,这个对象频繁的new与clean,使得其效率略低于ArrayDeque)。但队列更多的用来处理多线程问题,因此咱们更多的使用BlockingQueue,关于多线程的问题,之后再认真研究。


【感谢您能看完,若是可以帮到您,麻烦点个赞~】

更多经验技术欢迎前来共同窗习交流: 一点课堂-为梦想而奋斗的在线学习平台 http://www.yidiankt.com/

![关注公众号,回复“1”免费领取-【java核心知识点】] file

QQ讨论群:616683098

QQ:3184402434

想要深刻学习的同窗们能够加我QQ一块儿学习讨论~还有全套资源分享,经验探讨,等你哦! 在这里插入图片描述

相关文章
相关标签/搜索