在上一篇博客 Java容器之LinkedList源碼分析(LinkedList到底是單鏈表還是雙鏈表?) 分析了LinkedList
容器的源碼,LinkedList
實現了Deque
接口,所以它不但是一個List
容器,而且還是一個雙端隊列
容器,並且是基於雙鏈表實現。在此篇博客,將分析基於(循環)數組實現的雙端隊列
容器——ArrayDeque
。
註明:以下源碼分析都是基於jdk 1.8.0_221
版本
Java容器之ArrayDeque源碼分析目錄
一、ArrayDeque
容器概述
ArrayDeque
類的聲明如下:
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
Java
中的ArrayDeque
容器實現了Deque
接口,所以它是一個雙端隊列
(隊頭
、隊尾
都可以進行出隊
、入隊
)容器,並且是底層是通過(循環)數組來存放數據。Java
中的LinkedList
容器也實現了Deque
接口,不過底層是通過維護鏈表保存數據。
二、ArrayDeque
類的主要屬性
/**
* 循環數組
*/
transient Object[] elements; // non-private to simplify nested class access
/**
* 隊頭對應的下標
*/
transient int head;
/**
* 隊尾對應的下標
*/
transient int tail;
/**
* 數組最短的長度(數組的長度需要保持爲2的冪,主要是將求餘操作轉換爲移位操作)
*/
private static final int MIN_INITIAL_CAPACITY = 8;
三、ArrayDeque
類的構造器
/**
* 數組默認初始化長度爲16
*/
public ArrayDeque() {
elements = new Object[16];
}
/**
* 指定數組的長度
*/
public ArrayDeque(int numElements) {
// 不過數組的長度需要保持爲2的次冪,調用allocateElements方法初始化(後面有介紹這個方法)
allocateElements(numElements);
}
/**
* 複製構造方法
*/
public ArrayDeque(Collection<? extends E> c) {
// 先初始化數組,最小長度爲c.size(不過仍然需要轉換爲不必它小的最小2的次冪)
allocateElements(c.size());
addAll(c);
}
四、Deque
接口中的方法實現
/**
* 往隊頭添加元素
*/
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
// 如果隊頭在數組下標0的位置,此時需要循環使用數組,插入到尾端
// 由於elements.length一直是2的次冪,elements.length - 1則是二進制全1的數
// (head - 1) & (elements.length - 1) == (head - 1 + elements.length) % elements.length
elements[head = (head - 1) & (elements.length - 1)] = e;
// 首尾下標相遇,則當前數組已經使用完了,需要擴大(後面有介紹doubleCapacity方法)
if (head == tail)
doubleCapacity();
}
/**
* 從尾端插入元素
*/
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
// 由於tail的下標是隊列尾端的下一個位置,所以可以直接賦值插入
elements[tail] = e;
// 然後tail往後移動一個單位,如果tail在數組的尾端,則需要循環移動到數組的前端
// (tail + 1) & (elements.length - 1) == (tail + 1) % elements.length
// 如果與head相遇,此時需要擴容
if ((tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
/**
* 隊頭出隊元素
*/
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// 如果隊頭爲空(整個數組爲空,此時無法出隊)
if (result == null)
return null;
elements[h] = null;
// 出隊後,head往後移動一個單位(注意循環,)
head = (h + 1) & (elements.length - 1);
return result;
}
/**
* 隊尾出隊元素
*/
public E pollLast() {
// 由於隊尾指向的真隊尾的下一個位置,所以先要前移
// (tail - 1) & (elements.length - 1)相當於 (tail - 1 + elements.length) % elements.length
int t = (tail - 1) & (elements.length - 1);
@SuppressWarnings("unchecked")
E result = (E) elements[t];
// 隊尾爲空(整個數組爲空),無需進行出隊操作
if (result == null)
return null;
// elements[tail - 1] = null
elements[t] = null;
tail = t;
return result;
}
/**
* 獲取隊頭
*/
public E getFirst() {
@SuppressWarnings("unchecked")
E result = (E) elements[head];
if (result == null)
throw new NoSuchElementException();
return result;
}
/**
* 獲取隊尾
*/
public E getLast() {
@SuppressWarnings("unchecked")
E result = (E) elements[(tail - 1) & (elements.length - 1)];
if (result == null)
throw new NoSuchElementException();
return result;
}
/**
* 獲取隊頭
*/
@SuppressWarnings("unchecked")
public E peekFirst() {
// elements[head] is null if deque empty
return (E) elements[head];
}
/**
* 獲取隊尾
*/
@SuppressWarnings("unchecked")
public E peekLast() {
// 由於隊尾指向的真隊尾的下一個位置,所以先要前移tail
return (E) elements[(tail - 1) & (elements.length - 1)];
}
/**
* 隊頭出隊
*/
public E poll() {
return pollFirst();
}
/**
* 獲取隊頭
*/
public E peek() {
return peekFirst();
}
// *** Stack methods ***
/**
* 棧頂(隊頭)入棧
*/
public void push(E e) {
addFirst(e);
}
/**
* 棧頂(隊頭)出棧
*/
public E pop() {
return removeFirst();
}
五、其他方法
1、calculateSize
方法
calculateSize
方法的作用是計算大於numElements
的最小2的冪,比如傳入3,返回4(22);傳入4,則返回8(23);傳入13,返回16(24)。
private static int calculateSize(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// 如果numElements < MIN_INITIAL_CAPACITY(前面介紹個這個屬性,常量8),則不用查找2的冪了,直接返回
if (numElements >= initialCapacity) {
// 我們需要把initialCapacity二進制形式中第一位爲1的後面的位全部置1
// 比如initialCapacity = 0000 0001 1011 0111 1011 1001 1101 0110
initialCapacity = numElements;
// initialCapacity >>> 1 爲0000 0000 1101 1011 1101 1100 1110 1011
// 或操作後 initialCapacity = 0000 0001 1...(後面其實不用管)
initialCapacity |= (initialCapacity >>> 1);
// initialCapacity >>> 1 爲 0000 0000 011...
// 或操作後 initialCapacity = 0000 0001 111.(後面其實不用管)
initialCapacity |= (initialCapacity >>> 2);
// initialCapacity >>> 1 爲 0000 0000 0111 1...
// 或操作後 initialCapacity = 0000 0001 1111 111...(後面其實不用管)
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
// 最後 initialCapacity = 0000 0001 1111 1111 1111 1111 1111 1111
// 加1後,initialCapacity = 0000 0010 0000 0000 0000 0000 0000 0000
initialCapacity++;
// 如果發生了溢出(最高位爲附標誌),則將initialCapacity置爲2 ^ 30
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
return initialCapacity;
}
2、allocateElements
方法
/**
* 初始化數組
*/
private void allocateElements(int numElements) {
// 初始化前,需要計算大於numElements的最小2的次冪(上面介紹過calculateSize方法,別跳着看。。。)
elements = new Object[calculateSize(numElements)];
}
3、doubleCapacity
方法
/**
* 當數組沒有空位置時,擴大爲原來的2倍
*/
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
// 可能head在tail的後面,此時數組尾端的[head, elements.length-1]其實是隊列前半段
int r = n - p;
// 容量擴大爲原來的2倍
int newCapacity = n << 1;
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
// 如果 r > 0,則先複製[head, elements.length-1]這一段
System.arraycopy(elements, p, a, 0, r);
// 然後複製[0, head]這一段
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}
六、總結
經過上面的分析可知,ArrayDeque``雙端隊列的實現是利用數組,並且是
循環數組,比如往
隊頭插入元素,
隊頭已經到
數組的第0個位置時,可以插入到
數組的
尾端;同樣
隊尾插入元素,也可以增長到
數組前段。當數組沒有空餘位置時,此時需要
擴容,將當前數組中的元素全部複製到長度爲原來2倍的數組中(效率低)。
LinkedList容器基於雙鏈表維護
雙端隊列`,可以在頭、尾部無限增長,我不用考慮擴容問題,所以效率較高。