java1.8 常用集合源码学习:ArrayDeque

来源:互联网 发布:python编写桌面程序 编辑:程序博客网 时间:2024/06/09 13:00
1、api

Deque 接口的大小可变数组的实现。数组双端队列没有容量限制;它们可根据需要增加以支持使用。它们不是线程安全的;在没有外部同步时,它们不支持多个线程的并发访问。禁止 null 元素。此类很可能在用作堆栈时快于 Stack,在用作队列时快于 LinkedList
此类的 iterator 方法返回的迭代器是快速失败 的:如果在创建迭代器后的任意时间通过除迭代器本身的 remove 方法之外的任何其他方式修改了双端队列,则迭代器通常将抛出 ConcurrentModificationException。因此,面对并发修改,迭代器很快就会完全失败,而不是冒着在将来不确定的时刻任意发生不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测 bug。
此类及其迭代器实现 Collection 和 Iterator 接口的所有可选 方法。
此类是 Java Collections Framework 的成员。

2、源码学习

内部维护数据的数组:
transientObject[]elements;

队列头
transient inthead;

队列尾
transient inttail;

初始化数组时用于分配数组大小。MIN_INITIAL_CAPACITY为8,也就是数组最小也是8位。在这个方法中有按位或和无符号右移操作,最终得到一个比initialCapacity 大的2的幂次方
private voidallocateElements(intnumElements) {
intinitialCapacity =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=newObject[initialCapacity];
}

上面说法可能不太好理解,我们可以打印一下看下结果
@Test
public voidtestAllocateElements(){
intinitialCapacity =8;
intnumElements =65;
if(numElements >= initialCapacity) {
initialCapacity = numElements;
System.out.println(Integer.toBinaryString(initialCapacity));
initialCapacity |= (initialCapacity >>> 1);
System.out.println(Integer.toBinaryString(initialCapacity));
initialCapacity |= (initialCapacity >>> 2);
System.out.println(Integer.toBinaryString(initialCapacity));
initialCapacity |= (initialCapacity >>> 4);
System.out.println(Integer.toBinaryString(initialCapacity));
initialCapacity |= (initialCapacity >>> 8);
System.out.println(Integer.toBinaryString(initialCapacity));
initialCapacity |= (initialCapacity >>> 16);
System.out.println(Integer.toBinaryString(initialCapacity));
initialCapacity++;

if(initialCapacity <0)// Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
System.out.println(initialCapacity);
System.out.println(Integer.toBinaryString(initialCapacity));
}

结果为(其中4、8、16位因为数比较小,没有使用到):
1000001 //65的二进制表示
1100001 //上个数字右移1位再和他本身做按位或
1111001 //上个数字右移2位再和他本身做按位或
1111111 //上个数字右移4位再和他本身做按位或
1111111 //上个数字右移8位再和他本身做按位或
1111111 //上个数字右移16位再和他本身做按位或
128 //最终得到的容量
10000000 //最终容量的二进制表示

当头尾相交时,需要将数组扩容一倍。
首先确定头尾是相同的。然后取得当前头的索引p,当前数组长度n,p右边的元素数r。新的容量newCapacity 为旧容量的两倍。创建一个新的容量为newCapacity的数组,先将原数组的head右边的所有元素拷贝到新数组,然后再将原数组的head左边的所有元素拷贝到新数组。最后重置head和tail为数组的第一项和原数组的长度
private voiddoubleCapacity() {
asserthead==tail;
intp =head;
intn =elements.length;
intr = n - p;// number of elements to the right of p
intnewCapacity = n <<1;
if(newCapacity <0)
throw newIllegalStateException("Sorry, deque too big");
Object[] a =newObject[newCapacity];
System.arraycopy(elements,p,a,0,r);
System.arraycopy(elements,0,a,r,p);
elements= a;
head=0;
tail= n;
}

拷贝数组,如果头在尾左边(即各在数组的一头),则直接拷贝数组。如果头在尾右边,则先将head右边的元素拷贝进去,再拷贝tail左边的元素
private<T>T[]copyElements(T[] a) {
if(head<tail) {
System.arraycopy(elements,head,a,0,size());
}else if(head>tail) {
intheadPortionLen =elements.length-head;
System.arraycopy(elements,head,a,0,headPortionLen);
System.arraycopy(elements,0,a,headPortionLen,tail);
}
returna;
}

元素加到队列头和元素加到队列尾一起看,确定队列头和队列尾的移动都是int的按位与操作,如果不好理解,先略过,只要知道头的移动方向是向左移动(到头了则会移动到队列尾,再从尾部向左移动),而尾的移动方向是向右移动。另外,头是先移动索引,在设置值,而尾是先设置值再移动。而在头尾相交后,会调用doubleCapacity方法将数字扩容两倍,并将队列重新排列。
public voidaddFirst(Ee) {
if(e ==null)
throw newNullPointerException();
elements[head= (head-1) & (elements.length-1)] = e;
if(head==tail)
doubleCapacity();
}
public voidaddLast(Ee) {
if(e ==null)
throw newNullPointerException();
elements[tail] = e;
if( (tail= (tail+1) & (elements.length-1)) ==head)
doubleCapacity();
}

上述方法中的按位与如果不好理解,看下面测试代码(如果列出2进制会更好理解)
@Test
public voidtestArrayDeque() {
inthead =0;
inttail =0;
intlength =16;
head = (head - 1) & (length -1);
System.out.println("head : " + head);
head = (head - 1) & (length -1);
System.out.println("head : " + head);
tail = (tail + 1) & (length -1);
System.out.println("tail : " + tail);
tail = (tail + 1) & (length -1);
System.out.println("tail : " + tail);
}
输出为:
head : 15
head : 14
tail : 1
tail : 2
也就是一个长度为16的数组,他的头元素会依次变为15、14,尾元素会依次变为1、2,这样一直增加下去,就会出现头尾相交的情况,于是就会调用到数组扩容两倍的方法。在数组刚刚初始化时,实际上队列头尾的索引都是0。

offerFirst、offerLast方法实际就是调用的addFirst和addLast方法

removeFirst和removeLast实际调用的是pollFirst和pollLast方法,只不过在返回元素为null时会抛异常。

pollFirst直接将数组中head位置的元素置位null,然后将head的索引右移一位。
publicEpollFirst() {
inth =head;
@SuppressWarnings("unchecked")
Eresult = (E)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);
returnresult;
}

相对应的pollLast则是先将tail的索引左移一位并且将该位置的元素清除(设置为null)
publicEpollLast() {
intt = (tail-1) & (elements.length-1);
@SuppressWarnings("unchecked")
Eresult = (E)elements[t];
if(result ==null)
return null;
elements[t] = null;
tail= t;
returnresult;
}

getFirst、getLast、peekFirst、peekLast方法都是直接对数组elements操作(取值)

删除包含的第一个对象o,这个方法和ArrayList不同的是,这里的遍历是用按位与的方式i=(i+1)&mask而不是遍历数组,因为可能会夸数组的尾和头进行遍历。取到包含的第一个元素后,调用delete方法删除。后面的contains方法和此方法实现非常类似,只是不需要删除;而和此方法相对应的removeLastOccurrence方法是从队列的尾向前遍历(也是按位与的方式)
public booleanremoveFirstOccurrence(Object o) {
if(o ==null)
return false;
intmask =elements.length-1;
inti =head;
Object x;
while( (x =elements[i]) !=null) {
if(o.equals(x)) {
delete(i);
return true;
}
i = (i +1) & mask;
}
return false;
}

删除队列中间的元素,其中elements代表数组,h是头元素head,t是尾元素tail,front代表待删除元素离头的距离,back代表待删除元素离尾巴的距离。
首先看待删除元素离队列头比队列尾近的情况。这时如果待删除元素在头的右侧,则将数组中从头开始的区域向右移动一个位置移动的长度为front,并且更新头的位置,将原头的位置置位null。如果待删除元素在头的左侧,则先将左侧数据右移一个位置,移动长度为待删除元素的索引(即刚好将其复制),然后将数组最右侧元素复制到数组最左侧元素,然后将队列的头元素往右的元素全部右移一个单位。
待删除元素离队列头比队列尾远的情况的处理则刚好相反。
private booleandelete(inti) {
checkInvariants();
finalObject[] elements =this.elements;
final intmask = elements.length-1;
final inth =head;
final intt =tail;
final intfront = (i - h) & mask;
final intback = (t - i) & mask;

// Invariant: head <= i < tail mod circularity
if(front >= ((t - h) & mask))
throw newConcurrentModificationException();

// Optimize for least element motion
if(front < back) {
if(h <= i) {
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
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;
}
}

size的实现也是按位与
public intsize() {
return(tail-head) & (elements.length-1);
}

isEmpty的实现也是和其他集合类不一样
public booleanisEmpty() {
returnhead==tail;
}

DeqIterator、DescendingIterator中实际也都是按位与的方式去实现核心的next方法,不再赘述

spliterator方法以后说

总之这个类最重要的一个技巧就是按位与和数组的配合。
原创粉丝点击