Java ArrayDeque源码剖析

LinkedList实现了队列接口Queue和双端队列接口Deque,Java容器类中还有一个双端队列的实现类ArrayDeque,它是基于数组实现的。我们知道,一般而言,由于需要移动元素,数组的插入和删除效率比较低,但ArrayDeque的效率却非常高,它是怎么实现的呢?本文就来详细探讨。

实现原理

ArrayDeque内部主要有如下实例变量:

private transient E[] elements;
private transient int head;
private transient int tail;

elements就是存储元素的数组。ArrayDeque的高效来源于head和tail这两个变量,它们使得物理上简单的从头到尾的数组变为了一个逻辑上循环的数组,避免了在头尾操作时的移动。

对于一般数组,比如arr,第一个元素为arr[0],最后一个为arr[ar.length-1]。但对于ArrayDeque中的数组,它是一个逻辑上的循环数组,所谓循环是指元素到数组尾之后可以接着从数组头开始,数组的长度、第一个和最后一个元素都与head和tail这两个变量有关,具体来说:
1)如果head和tail相同,则数组为空,长度为0。

2)如果tail大于head,则第一个元素为elements[head],最后一个为elements[tail-1],长度为tail-head,
元素索引从head到tail-1。
3)如果tail小于head,且为0,则第一个元素为elements[head],最后一个为elements[elements.length-
1],元素索引从head到elements.length-1。
4)如果tail小于head,且大于0,则会形成循环,第一个元素为elements[head],最后一个是
elements[tail-1],元素索引从head到elements.length-1,然后再从0到tail-1。

ArrayDeque默认构造方法的代码为:

public ArrayDeque() {
       elements = (E[]) new Object[16];
}

代码不是简单地分配给定的长度,而是调用了allocateElements。这个方法的代码看上去比较复杂,它主要就是在计算应该分配的数组的长度,计算逻辑如下:
1)如果numElements小于8,就是8。
2)在numElements大于等于8的情况下,分配的实际长度是严格大于numElements并且为2的整数次幂的最小数。比如,如果numElements为10,则实际分配16,如果num-Elements为32,则为64。
为什么要严格大于numElements呢?因为循环数组必须时刻至少留一个空位,tail变量指向下一个空位,为了容纳numElements个元素,至少需要numElements+1个位置。

add方法的代码为:

public boolean add(E e) {
       addLast(e);
       return true;
   }

addLast的代码为:

public void addLast(E e) {
       if(e == null)
           throw new NullPointerException();
       elements[tail] = e;
       if( (tail = (tail + 1) & (elements.length - 1)) == head)
           doubleCapacity();
}

将元素添加到tail处,然后tail指向下一个位置,如果队列满了,则调用doubleCapa-city扩展数组。tail的下一个位置是(tail+1)&(elements.length-1),如果与head相同,则队列就满了。
进行与操作保证了索引在正确范围,与(elements.length-1)相与就可以得到下一个正确位置,是因为elements.length是2的幂次方,(elements.length-1)的后几位全是1,无论是正数还是负数,与(elements.length-1)相与都能得到期望的下一个正确位置。

addFirst方法的代码为:

public void addFirst(E e) {
       if(e == null)
           throw new NullPointerException();
       elements[head = (head - 1) & (elements.length - 1)] = e;
       if(head == tail)
           doubleCapacity();
}

在头部添加,要先让head指向前一个位置,然后再赋值给head所在位置。head的前一个位置是(head-1)&(elements.length-1)。刚开始head为0,如果elements.length为8,则(head-1)&(elements.length-1)的结果为7。

removeFirst方法的代码为:

public E removeFirst() {
       E x = pollFirst();
       if(x == null)
           throw new NoSuchElementException();
       return x;
}

主要调用了pollFirst方法,pollFirst方法的代码为:

public E pollFirst() {
       int h = head;
       E result = 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;
}

ArrayDeque实现了双端队列,内部使用循环数组实现,这决定了它有如下特点。
1)在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加N个元素的效率为O(N)。
2)根据元素内容查找和删除的效率比较低,为O(N)。
3)与ArrayList和LinkedList不同,没有索引位置的概念,不能根据索引位置进行操作。ArrayDeque和LinkedList都实现了Deque接口,应该用哪一个呢?如果只需要Deque接口,从两端进行操作,一般而言,ArrayDeque效率更高一些,应该被优先使用;如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除,则应该选LinkedList。
 

以上就是ArrayDeque的基本原理,内部它是一个动态扩展的循环数组,通过head和tail变量维护数组的开始和结尾,数组长度为2的幂次方,使用高效的位操作进行各种判断,以及对head和tail进行维护。

你可能感兴趣的:(Java常用类的源码剖析,java,开发语言)