简介
ArrayDeque是JDK容器中的一个双端队列实现,不过它内部使用的是数组来对元素进行操作,不允许存储null值,同时可以当做队列,双端队列,栈来进行使用。上篇文章的时候我们分析过LinkedList也是双端队列,不过用的是双向链表结构实现的。
使用示例
public static void main(String[] args) {
ArrayDeque<String> deque = new ArrayDeque<>();
//添加一个元素(内部也是添加一个元素到尾部)
deque.add("adbc");
deque.add("123");
//在最后一个位置添加元素
deque.addLast("last Element");
deque.add("456");
//对队列的最后一个位置添加元素
deque.offer("yahoo");
//往队列的最后一个位置添加元素
deque.offerLast("tencent");
deque.add("789");
//往队列中的第一个位置插入一个元素
deque.push("Hello");
//在第一个位置添加元素
deque.addFirst("first Element");
//删除第一个元素,并且返回该删除的元素
deque.poll();
String data = deque.poll();
}
类结构图
注意: 我们知道ArrayDeque内部是由数组实现的,但是我们 知道数组是从左边到右边的。由此我们可知 tail就是数组的第0个元素,head就是数组中的第 length-1个元素。
代码分析
通过上面的学习以及类结构图,我们能很清楚的知道ArrayDeque基本使用以及复用关系,它的内部api方法其实很多之间也都是复用的。比如说 add() 和 offer()、offerLast()最后都是调用的 addLast()方法来进行实现的;push()和 offerFirst()最后都是调用的addFirst()方法进行实现的。所以我们只需要分析核心方法,其他找到他们的调用关系就行。
- 初始化
public class ArrayDeque {
//队列内部存储元素的 数组
transient Object[] elements;
//用于记录 队列 头部 位置信息
transient int head;
//用于记录 队列 尾部 位置信息
transient int tail;
private static final int MIN_INITIAL_CAPACITY = 8;
public ArrayDeque() {
elements = new Object[16];
}
。。。。。。。
}
根据我们最简单的构造方法,发现首先会创建一个长度为16的数组,同时还有两个很重要的属性head和tail用来指向数组的头和尾巴,同时我们也可以自定义数组的长度
//我们也可以传入我们自定义的容量大小,同时jdk帮我们内部创建一个数组。
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
/**
* 这段代码的核心是找到一个数字 大于等于 numElements同时也是 2的n次幂的数字,同时创建的数组的大小就以这个数字为大小,举个例子,如果我们传入29的话,这个时候会进行运算同时找到32这个数字,同时创建一个大小为32的数组。
*/
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.
//我们传进来的数字小于8的话,那么就创建一个大小为8的数组。否则就找到最接近于numElements(同时还需要大于或者等于numElements),同时还要是2的n次幂的数字。
if (numElements >= initialCapacity) {
initialCapacity = numElements;
//这里的 >>> 是无符号向右移动多少位然后再跟原来的数字进行 或 运算的。
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
//最大只能创建 2的30次幂大小的数组。
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1; // Good luck allocating 2^30 elements
}
elements = new Object[initialCapacity];
}
上面主要分析的时候了构造方法里面创建数组时的一些注意问题,如果是调用者自己传入大小的话,内部代码还会做一个转换,就是会找到最接近于我们传入这个数字,同时还需要是 2^n = numElements,
- 添加元素
尾部添加元素
public void addLast(E e) {
//从这个代码中我们可以看出该队列不可以添加 null元素
if (e == null)
throw new NullPointerException();
//在初始化的时候 tail为0,所以直接在[0] 添加元素就行
elements[tail] = e;
//我们知道 elements length = 2 ^ n,所以 length - 1转换为 二进制的话,所有的数字都是1的。
比如说 32 - 1转换为2进制的话就是 11111的,所以 (tail + 1) & 31的话还是原来的数字。
这里相当于是 tail + 1,也就是 尾部的位置想右边移动了一个位置。
//这里有一个非常关键的地方就是 tail = tail + 1同时要判断跟head的位置是否重合,如果重合的话,就需要进行扩容了。
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
头部添加元素
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
//通过上面的结构图中我们知道head默认指向的是数组的最后一个数字。当 head = 0时 head - 1 为 -1,这个时候 -1 & 31的话还是31,也就是指向数组的最后一个元素。同时 head 指向31。如果再往数组的头部添加元素的话,数组的位置依次向左边移动,相当于 index--的实现。
elements[head = (head - 1) & (elements.length - 1)] = e;
if (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;
}
- 删除元素
删除头部元素
public E pollFirst() {
final Object[] elements = this.elements;
//将head索引赋值给 h
final int h = head;
//然后获取数组 elements 数组的h位置的值
E result = (E) elements[h];
//如果 elements[h] 不为空的话,表示数组中存在该数据,需要将该位置数据置为null。
if (result != null) {
elements[h] = null; // Must null out slot
//head的位置需要向右边移动一位(head = head + 1)。然后返回该元素
head = (h + 1) & (elements.length - 1);
}
return result;
}
删除尾部元素
public E pollLast() {
final Object[] elements = this.elements;
//首先我们需要将 tail 位置向 左边移动一位(也就说 tail = tail - 1)。
final int t = (tail - 1) & (elements.length - 1);
//然后获取数组中的元素
E result = (E) elements[t];
if (result != null) {
//需要将 tail - 1位置的元素置为null
elements[t] = null;
//同时 tail - 1 赋值给tail
tail = t;
}
return result;
}
其实上面的不管在首尾添加数据还是在首尾删除数据,其实跟我们现实中的逻辑都是一样的,就是通过控制 head和tail位置来操纵数组,对数组进行增删操作的。如果我们要判断某个元素是否在队列中,则需要去遍历数组,然后每个元素进行比对匹配。
- 清除所有元素
public void clear() {
int h = head;
int t = tail;
//首先判断首尾不相等。
if (h != t) {
head = tail = 0;
//然后 head赋值 i
int i = h;
int mask = elements.length - 1;
do {
elements[i] = null;
//这里有个需要注意的是,如果 i+1 大于 elements.lenth以后,再 & mask的话,是循环从mask 到 tail的。其实我很好奇为啥不直接 遍历数组,从0开始遍历到 length -1 呢?
i = (i + 1) & mask;
} while (i != t);
}
}
栈
在文章的开头的时候我们讲过可以当做栈来进行使用
//在栈顶添加元素
public void push(E e) {
addFirst(e);
}
//弹出栈顶的元素
public E poll() {
return pollFirst();
}
通过上面的代码我们可以分析的出来, head是作为栈底的,而tail是在栈的最上面。这个时候我们就需要把把 ArrayDeque竖立起来看待了。其原型图:
总结
- ArrayDeque 采用数组方式实现的双端队列,通过内部变量 head和tail来控制位置的偏移
- ArrayDeque容量不足时会进行扩容,每次扩容的容量在原有基础上扩大一倍
- Array还可以当做栈来直接使用