ArrayDeque:环形数组
核心属性:
// 底层通过数组实现
transient Object[] elements;
// 这里的双指针也是经典的左闭右开式,也就是:
// - 尾指针tail,指向的是addLast时应该插入的位置
// - 头指针head,指向的是getFirst时元素的位置
// 即head指向首元素,tail指向尾元素的下一个位置
// 头 指针 也可以理解为索引,数组的下标
transient int head;
// 尾 指针
transient int tail;
// 最小也是8个元素,如果new时指定了小于8的大小,也会new出一个大小为8的数组
private static final int MIN_INITIAL_CAPACITY = 8;
// 默认16个元素
public ArrayDeque() {
elements = new Object[16];
}
ArrayDeque 的数组大小必须是2的幂
- 首节点index:
head - 首节点前的index,即addFirst插入的位置:
(h + 1) & (elements.length - 1) - 尾节点index:
(tail - 1) & (elements.length - 1) - 尾节点后的index,即addLast插入的位置:
tail
仅仅维护了head和tail,在 add/get 时往往需要求出对应的index,并且ArrayDeque为了充分利用空间,采取了环形数组,比如head在5,那么0~4的位置都可以被addLast放进来,为了避免取模操作,提高效率,规定数组大小必须是2的幂从而使得能够利用位运算加速下标的定位。
核心方法
查:getFirst/Last,peekFirst/Last
增:addFirst/Last,offerFirst/Last
删:removeFirst/Last,pollFirst/Last
为空的情况:get抛异常,peek返回null,poll返回空,remove抛异常
offer则是直接调用了add方法。
查:
public E getFirst() {
E result = (E) elements[head];
if (result == null)
throw new NoSuchElementException();
return result;
}
public E getLast() {
// 可以看到 length必须为2的幂
E result = (E) elements[(tail - 1) & (elements.length - 1)];
if (result == null)
throw new NoSuchElementException();
return result;
}
public E peekLast() {
return (E) elements[(tail - 1) & (elements.length - 1)];
}
peek方法就是get方法去掉异常处理的版本!
因此peek,为null直接返回,get为null抛异常。
增:
// 插入元素不允许为null
if (e == null)
throw new NullPointerException();
// tail向右移,遇到head,触发扩容
public void addLast(E e) {
elements[tail] = e;
// 举个例子 如果head为0, 16 & 15 10000 & 01111 得到 0 此时就说明需要扩容
// 那head非0的情况,tail会循环利用head前未被利用到的空间(循环数组),因此当head = tail才扩容
//
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
// head往左移,遇到tail,触发扩容
public void addFirst(E e) {
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
删:
// removeLast调用的这个,result为空抛异常
public E pollLast() {
int t = (tail - 1) & (elements.length - 1);
E result = (E) elements[t];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}
// removeFirst调用的这个,result为空抛异常
public E pollFirst() {
int h = head;
E result = (E) elements[h];
if (result == null)
return null;
elements[h] = null;
head = (h + 1) & (elements.length - 1);
return result;
}
// 还是看个例子吧
public E removeLast() {
E x = pollLast();
if (x == null)
throw new NoSuchElementException();
return x;
}
题外话:负数的与运算
负数在计算机中都是以补码来表示的
正数:原码 = 补码;负数:补码 = 原码取反+1。
在pollLast方法的第一行有这样一行:
int t = (tail - 1) & (elements.length - 1);tail是可以为0的,此时就是 -1 & 11111 ,真正参与位运算的是-1的补码,即:
原码1000 0001 反码 1111 1110 补码1111 1111 此时& 0001 1111 就是 0001 1111
也就顺利得到了数组的最后一个位置 2^n - 1。
扩容机制
触发时机:当add时,发现头尾节点相遇,即 head == tail 触发扩容
翻两倍大小,同时重置头指针为0。
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;
}
小结
ArrayDeque底层是个环形数组,实现了Deque接口,因此适合做栈与队列。ArrayDeque与HashMap一样,底层的数组必须是2的幂,因为有位运算。无参构造默认是长度为16的数组,有参构造最小也是8。