概述

- 来自JDK1.6,底层采用可变容量的环形数组实现一个双端队列,有序存放
- 继承自AbstractCollection,拥有collection通用方法
- 没有实现list接口,不能通过索引操作元素
- 实现deque接口,能够通过两端访问元素,实现FIFO队列,或者FILO栈
- 非线程安全,可以通过synchronizedCollection转换成线程安全
常用方法
- addFirst/offerFirst/addLast/offerLast
- removeFirst/pollFirst…
- getFirst/peekFirst…
源码分析
属性
/**
存储双端队列元素的数组。双端队列的容量就是这个数组的长度,它总是 2 的幂。
*/
transient Object[] elements; // non-private to simplify nested class access
/**
双端队列头部元素的索引(将被 remove() 或 pop() 删除的元素);如果双端队列为空,则为等于 tail 的任意数字。
*/
transient int head;
/**
* 将下一个元素添加到双端队列尾部的索引
*/
transient int tail;
/**
将用于新创建的双端队列的最小容量。必须是 2 的幂
*/
private static final int MIN_INITIAL_CAPACITY = 8;
通过数组+双指针的方式,实现双端队列,最小容量为8且必须2的幂次方
构造器
ArrayDeque()
/**
* 构造一个空数组双端队列,其初始容量足以容纳 16 个元素。
*/
public ArrayDeque() {
elements = new Object[16];
}
ArrayDeque(int numElements)
/**
* 构造一个空数组双端队列,其初始容量足以容纳指定数量的元素。
*/
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
private void allocateElements(int numElements) {
elements = new Object[calculateSize(numElements)];
}
private static int calculateSize(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
}
return initialCapacity;
}
calculateSize使用的非常巧妙,通过对任意int值进行无符号右移操作,经过五次无符号右移和位或操作后,将会得到一个2k-1的值,最后再自增1,得到2k,该2k就是大于initialCapacity的最小的2的幂次方。
ArrayDeque(Collection<? extends E> c)
构造一个包含指定 collection 的元素的双端队列,这些元素按 collection 的迭代器返回的顺序排列。
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
添加方法
添加到尾部
如果被添加的元素为null,则抛出NullPointerException异常;然后插入元素;接着计算新的下一个尾节点索引,并且判断是否需要扩容。
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
计算索引
如果尾节点达到了数组末尾,直接增加尾节点索引将会导致数组长度溢出,因此常常考虑扩容。但是此时该数组的前半部分还有剩余空间,例如在插入尾节点时删除了一些头节点),这种现象称为"假溢出",因为真实的数组空间并没有用完,造成了空间的浪费。
为避免这种情况使用当前索引+1于数组大小进行&运算,当等于头节点时候,进行扩容操作。
为什么数组的初始容量一定是2的幂次方?
如果elements.length为2的幂次方,那么elements.length-1自然就不是2的幂次方。根据上面的计算规律,“数组容量(长度)必须是2的幂次方”这一要求是为了保证计算下一个尾节点索引和头节点索引的值的正确性,即为了保证计算出的索引能够取到[0~elements.length-1]之间的全部值
扩容
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;
}
- 计算新容量,使用无符号左移动
- 如果为负数则抛出异常,为正则为新的长度
- 建立新容量大小的数组,将头索引到尾索引和0到头节点内容拷贝到新数组中(对应之前到插入防止假溢出)
其他方法
offer方法其实是对add方法对一次封装,使得返回值为bool值
public boolean offerLast(E e) {
//内部调用addLast的方法
addLast(e);
return true;
}
添加头部
与添加到尾部的思路类似,但是是通过头节点-1与数组最大长度的比较
head = (head - 1) & (elements.length - 1)
其他方法
- pollLast() 移除最后一个元素
- removeLast() 获取并移除最后一个元素
- pollFirst() 移除头部元素
- removeFirst()获取并移除头部元素
- removeFirstOccurrence(Object o) 移除第一次出现的元素
- removeFirstOccurrence(Object o) 移除最后一次出现的元素
- getFirst() 获取但是不移除(null时候抛出异常)
- peekFirst()获取但是不移除
总结
通过查看ArrayDeque的内部实现,我们可以清晰的认识到ArrayDeque内部通过数组+双索引的方式,使用&操作来定位对于元素的存放删除
LinkedList对比
- ArrayDeque不允许null元素,而LinkedList则允许null元素。
- 如果是使用栈/队列操作,ArrayList明显快于LinkedList
- LinkedList实现了List接口 可以通过索引操作数据,便于增删
本质还是数组/链表存放方式的差异,因而使用场景不同
本文详细解析了Java ArrayDeque双端队列的工作原理,包括其基于环形数组实现、容量管理、添加/移除操作,以及与LinkedList在null元素处理、性能和接口上的区别。同时讨论了ArrayDeque在栈和队列操作中的优势和适用场景。
8万+

被折叠的 条评论
为什么被折叠?



