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

1、api

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

2、源码学习

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

队列头
transient int head ;

队列尾
transient int tail ;

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

上面说法可能不太好理解,我们可以打印一下看下结果
@Test
public void testAllocateElements (){
int initialCapacity = 8 ;
int numElements = 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 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 ;
}

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

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

上述方法中的按位与如果不好理解,看下面测试代码(如果列出2进制会更好理解)
@Test
public void testArrayDeque () {
int head = 0 ;
int tail = 0 ;
int length = 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的索引右移一位。
public E pollFirst () {
int h = head ;
@SuppressWarnings ( "unchecked" )
E result = ( 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 ) ;
return result ;
}

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

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

删除包含的第一个对象o,这个方法和ArrayList不同的是,这里的遍历是用按位与的方式i=(i+1)&mask而不是遍历数组,因为可能会夸数组的尾和头进行遍历。取到包含的第一个元素后,调用delete方法删除。后面的contains方法和此方法实现非常类似,只是不需要删除;而和此方法相对应的removeLastOccurrence方法是从队列的尾向前遍历(也是按位与的方式)
public boolean removeFirstOccurrence (Object o) {
if (o == null )
return false;
int mask = elements . length - 1 ;
int i = 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 boolean delete ( int i) {
checkInvariants() ;
final Object[] elements = this . elements ;
final int mask = elements. length - 1 ;
final int h = head ;
final int t = tail ;
final int front = (i - h) & mask ;
final int back = (t - i) & mask ;

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

// 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 int size () {
return ( tail - head ) & ( elements . length - 1 ) ;
}

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

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

spliterator方法以后说

总之这个类最重要的一个技巧就是按位与和数组的配合。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值