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