Java深海拾遺系列(3)---JDK8中的ArrayDeque原始碼分析
ArrayDeque類檢視
簡介
從類檢視可以看出,ArrayDeque實現了Deque介面,Deque介面繼承了Queue介面,Queue介面繼承自頂級介面集合類Collection。
Queue 也是 Java 集合框架中定義的一種介面,直接繼承自 Collection 介面。除了基本的 Collection 介面規定測操作外,Queue 介面還定義一組針對佇列的特殊操作。通常來說,Queue 是按照先進先出(FIFO)的方式來管理其中的元素的,但是優先佇列是一個例外。
Deque 介面繼承自 Queue介面,但 Deque 支援同時從兩端新增或移除元素,因此又被成為雙端佇列。鑑於此,Deque 介面的實現可以被當作 FIFO佇列使用,也可以當作LIFO佇列(棧)來使用。官方也是推薦使用 Deque 的實現來替代 Stack。
ArrayDeque 是 Deque 介面的一種具體實現,是依賴於可變陣列來實現的。ArrayDeque 沒有容量限制,可根據需求自動進行擴容。ArrayDeque不支援值為 null 的元素。
Queue介面
- Queue是具有佇列特性的介面
- Queue具有先進先出的特點
- Queue所有新元素都插入佇列的末尾,移除元素都移除佇列的頭部
public interface Queue<E> extends Collection<E> {
//Inserts the specified element into this queue,throwing an IllegalStateException if no space is currently available.
boolean add(E e);
//Inserts the specified element into this queue,return false if no space is currently available.
boolean offer(E e);
//removes the head of this queue,return the head of this queue, or NoSuchElementException if this queue is empty
E remove();
//removes the head of this queue,return the head of this queue, or null if this queue is empty
E poll();
//not remove,return the head of this queue,throws NoSuchElementException if this queue is empty
E element();
//not remove,return the head of this queue, or null if this queue is empty
E peek();
}
畫成以下表格
操作 | 丟擲異常 | 返回特殊值 |
---|---|---|
插入 | add() | offer() |
刪除 | remove() | poll() |
查詢 | element() | peek() |
Deque
- Deque是一個雙端佇列
- Deque繼承自Queue
- Deque具有先進先出或後進先出的特點
- Deque支援所有元素在頭部和尾部進行插入、刪除、獲取
public interface Deque<E> extends Queue<E> {
void addFirst(E e);//插入頭部,異常會報錯
boolean offerFirst(E e);//插入頭部,異常返回false
E getFirst();//獲取頭部,異常會報錯
E peekFirst();//獲取頭部,異常不報錯
E removeFirst();//移除頭部,異常會報錯
E pollFirst();//移除頭部,異常不報錯
void addLast(E e);//插入尾部,異常會報錯
boolean offerLast(E e);//插入尾部,異常返回false
E getLast();//獲取尾部,異常會報錯
E peekLast();//獲取尾部,異常不報錯
E removeLast();//移除尾部,異常會報錯
E pollLast();//移除尾部,異常不報錯
}
畫成以下表格,只不過Deque是有頭部和尾部的
操作 | 丟擲異常 | 返回特殊值 |
---|---|---|
插入 | add() | offer() |
刪除 | remove() | poll() |
查詢 | element() | peek() |
ArrayDeque
- 實現於Deque,擁有佇列或者棧特性的介面
- 實現於Cloneable,擁有克隆物件的特性
- 實現於Serializable,擁有序列化的能力
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable{}
ArrayDeque底層結構
/**
* The array in which the elements of the deque are stored.
* The capacity of the deque is the length of this array, which is
* always a power of two. The array is never allowed to become
* full, except transiently within an addX method where it is
* resized (see doubleCapacity) immediately upon becoming full,
* thus avoiding head and tail wrapping around to equal each
* other. We also guarantee that all array cells not holding
* deque elements are always null.
*/
transient Object[] elements; // non-private to simplify nested class access
/**
* The index of the element at the head of the deque (which is the
* element that would be removed by remove() or pop()); or an
* arbitrary number equal to tail if the deque is empty.
*/
transient int head;
/**
* The index at which the next element would be added to the tail
* of the deque (via addLast(E), add(E), or push(E)).
*/
transient int tail;
/**
* The minimum capacity that we'll use for a newly created deque.
* Must be a power of 2.
*/
private static final int MIN_INITIAL_CAPACITY = 8;
ArrayDeque底層使用陣列儲存元素,同時還使用head和tail來表示索引,但注意tail不是尾部元素的索引,而是尾部元素的下一位,即下一個將要被加入的元素的索引
ArrayDeque的初始化
/**
* Constructs an empty array deque with an initial capacity
* sufficient to hold 16 elements.
*/
public ArrayDeque() {
elements = new Object[16];
}
/**
* Constructs an empty array deque with an initial capacity
* sufficient to hold the specified number of elements.
*
* @param numElements lower bound on initial capacity of the deque
*/
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
/**
* Constructs a deque containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator. (The first element returned by the collection's
* iterator becomes the first element, or <i>front</i> of the
* deque.)
*
* @param c the collection whose elements are to be placed into the deque
* @throws NullPointerException if the specified collection is null
*/
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
/**
* Allocates empty array to hold the given number of elements.
*
* @param numElements the number of elements to hold
*/
private void allocateElements(int numElements) {
elements = new Object[calculateSize(numElements)];
}
private static int calculateSize(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
}
return initialCapacity;
}
在初始化中,陣列要求的大小必須為2^n,所以有這麼一個演算法,如果當前的大小大於預設規定的大小時,就會去計算出新的大小,那麼這個計算過程是怎麼樣的呢?>>>
是無符號右移操作,|
是位或操作,經過五次右移和位或操作可以保證得到大小為2^k-1的數。舉例子進行分析
如果initialCapacity為10的時候,那麼二進位制為 1010
經過initialCapacity |= (initialCapacity >>> 1)時,那麼二進位制為 1010 | 0101 = 1111
經過initialCapacity |= (initialCapacity >>> 2)時,那麼二進位制為 1111 | 0011 = 1111
後面計算的結果都是1111,可以理解為將二進位制的低位數都補上1,這樣出來的結果都是2^n-1
最後initialCapacity++,2^n-1+1出來的結果就是2^n
這裡又有人會有疑問了,為什麼initialCapacity>>>16,右移到5位就可以結束呢?那是因為用的是|=符號,從右移1位到5位累加,其實就是整體右移了15位,剛好int值是16位的數,這就剛好滿足16位二進位制的低位都被補上了1。在進行5次位移操作和位或操作後就可以得到2^k-1,最後加1即可。這個實現還是很巧妙的。
ArrayDeque的插入
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();
//tail中儲存的是即將加入末尾的元素的索引
elements[tail] = e;
//tail向後移動一位
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
//tail和head相遇,空間用盡,需要擴容
doubleCapacity();
}
在儲存的過程中,這裡有個有趣的演算法,就是tail的計算公式(tail = (tail + 1) & (elements.length - 1))
,注意這裡的儲存採用的是環形佇列的形式,也就是當tail到達容量最後一個的時候,tail就為等於0,否則tail的值tail+1
(tail = (tail + 1) & (elements.length - 1))
證明:(elements.length - 1) = 2^n-1 即二進位制的所有低位都為1,假設為 11111111
假設:tail為最後一個元素,則(tail + 1)為 (11111111 + 1) = 100000000
結果:(tail + 1) & (elements.length - 1) = 000000000,tail下一個要新增的索引為0
其插入過程中,如果剛好是最後一個元素時,示例如下圖
那麼為什麼(tail + 1) & (elements.length - 1)
就能保證按照環形取得正確的下一個索引值呢?這就和前面說到的 ArrayDeque 對容量的特殊要求有關了。下面對其正確性加以驗證:
1 2 3 4 5 |
length = 2^n,二進位制表示為: 第 n 位為1,低位 (n-1位) 全為0 length - 1 = 2^n-1,二進位制表示為:低位(n-1位)全為1 如果 tail + 1 <= length - 1,則位與後低 (n-1) 位保持不變,高位全為0 如果 tail + 1 = length,則位與後低 n 全為0,高位也全為0,結果為 0 |
可見,在容量保證為 2^n 的情況下,僅僅通過位與操作就可以完成環形索引的計算,而不需要進行邊界的判斷,在實現上更為高效。
ArrayDeque的擴容
private void doubleCapacity() {
assert head == tail; //擴容時頭部索引和尾部索引肯定相等
int p = head;
int n = elements.length;
//頭部索引到陣列末端(length-1處)共有多少元素
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的刪除
ArrayDeque支援從頭尾兩端移除元素,remove方法是通過poll來實現的。因為是基於陣列的,在瞭解了環的原理後這段程式碼就比較容易理解了
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;
}
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;
}
ArrayDeque的查詢
@SuppressWarnings("unchecked")
public E peekFirst() {
// elements[head] is null if deque empty
return (E) elements[head];
}
@SuppressWarnings("unchecked")
public E peekLast() {
return (E) elements[(tail - 1) & (elements.length - 1)];
}
總結
ArrayDeque是Deque 介面的一種具體實現,是依賴於可變陣列來實現的。ArrayDeque 沒有容量限制,可根據需求自動進行擴容。ArrayDeque 可以作為棧來使用,效率要高於Stack;ArrayDeque 也可以作為佇列來使用,效率相較於基於雙向連結串列的LinkedList也要更好一些
參考:
1,https://blog.csdn.net/qq_30379689/article/details/80558771
相關文章
- Java深海拾遺系列(5)---函式式介面Functional InterfaceJava函式Function
- Java深海拾遺系列(5)--- 精度計算中的BigDecimal,double和floatJavaDecimal
- Java 容器原始碼分析之 Deque 與 ArrayDequeJava原始碼
- 【Java原始碼】集合類-ArrayDequeJava原始碼
- Java集合框架原始碼剖析:ArrayDequeJava框架原始碼
- Java Web 拾遺JavaWeb
- 【java web】--Ajax拾遺JavaWeb
- 原始碼|jdk原始碼之棧、佇列及ArrayDeque分析原始碼JDK佇列
- HashMap原始碼分析(JDK8)HashMap原始碼JDK
- 【Java學習筆記】拾遺Java筆記
- mongoose 拾遺Go
- 重拾RunLoop之原始碼分析1OOP原始碼
- Java容器系列-LinkedList 原始碼分析Java原始碼
- JAVA 拾遺 — CPU Cache 與快取行Java快取
- 前端技能拾遺前端
- Linux拾遺Linux
- [MASM拾遺]OffsetASM
- .NET 基礎拾遺(3): 字串、集合和流字串
- Java 集合系列之 LinkedList原始碼分析Java原始碼
- JAVA拾遺 — JMH與8個測試陷阱Java
- Java知識拾遺:三大框架的技術起源Java框架
- Element原始碼分析系列3-Button(按鈕)原始碼
- 物件導向拾遺物件
- C語言拾遺C語言
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- Java容器類原始碼分析之Iterator與ListIterator迭代器(基於JDK8)Java原始碼JDK
- Java 容器系列(六):LinkedList 原始碼分析02Java原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- 【Java】NIO中Selector的建立原始碼分析Java原始碼
- 【Java】NIO中Channel的註冊原始碼分析Java原始碼
- Java 8 中 ArrayList 的變化原始碼分析Java原始碼
- Spark 原始碼分析系列Spark原始碼
- ArrayDeque(JDK雙端佇列)原始碼深度剖析JDK佇列原始碼
- golang拾遺:嵌入型別Golang型別
- docker拾遺-之再入坑Docker
- Unix廣告拾遺 by Dennis Ritchie
- 【JS拾遺】函式的引數JS函式
- Java原始碼系列 -- HashSetJava原始碼