Java源碼刨析之ArrayDeque
前言
在本篇文章當(dāng)中主要跟大家介紹JDK
給我們提供的一種用數(shù)組實(shí)現(xiàn)的雙端隊(duì)列,在之前的文章LinkedList源碼剖析當(dāng)中我們已經(jīng)介紹了一種雙端隊(duì)列,不過與ArrayDeque
不同的是,LinkedList
的雙端隊(duì)列使用雙向鏈表實(shí)現(xiàn)的。
雙端隊(duì)列整體分析
我們通常所談?wù)摰降年?duì)列都是一端進(jìn)一端出,而雙端隊(duì)列的兩端則都是可進(jìn)可出。下面是雙端隊(duì)列的幾個(gè)操作:
數(shù)據(jù)從雙端隊(duì)列左側(cè)進(jìn)入。
數(shù)據(jù)從雙端隊(duì)列右側(cè)進(jìn)入。
數(shù)據(jù)從雙端隊(duì)列左側(cè)彈出。
數(shù)據(jù)從雙端隊(duì)列右側(cè)彈出。
而在ArrayDeque
當(dāng)中也給我們提供了對(duì)應(yīng)的方法去實(shí)現(xiàn),比如下面這個(gè)例子就是上圖對(duì)應(yīng)的代碼操作:
public void test() { ArrayDeque<Integer> deque = new ArrayDeque<>(); deque.addLast(100); System.out.println(deque); deque.addFirst(55); System.out.println(deque); deque.addLast(-55); System.out.println(deque); deque.removeFirst(); System.out.println(deque); deque.removeLast(); System.out.println(deque); }
// 輸出結(jié)果
[100]
[55, 100]
[55, 100, -55]
[100, -55]
[100]
數(shù)組實(shí)現(xiàn)ArrayDeque(雙端隊(duì)列)的原理
ArrayDeque
底層是使用數(shù)組實(shí)現(xiàn)的,而且數(shù)組的長(zhǎng)度必須是2
的整數(shù)次冪,這么操作的原因是為了后面位運(yùn)算好操作。在ArrayDeque
當(dāng)中有兩個(gè)整形變量head
和tail
,分別指向右側(cè)的第一個(gè)進(jìn)入隊(duì)列的數(shù)據(jù)和左側(cè)第一個(gè)進(jìn)行隊(duì)列的數(shù)據(jù),整個(gè)內(nèi)存布局如下圖所示:
其中tail
指的位置沒有數(shù)據(jù),head
指的位置存在數(shù)據(jù)。
當(dāng)我們需要從左往右增加數(shù)據(jù)時(shí)(入隊(duì)),內(nèi)存當(dāng)中數(shù)據(jù)變化情況如下:
當(dāng)我們需要從右往做左增加數(shù)據(jù)時(shí)(入隊(duì)),內(nèi)存當(dāng)中數(shù)據(jù)變化情況如下:
當(dāng)我們需要從右往左刪除數(shù)據(jù)時(shí)(出隊(duì)),內(nèi)存當(dāng)中數(shù)據(jù)變化情況如下:
當(dāng)我們需要從左往右刪除數(shù)據(jù)時(shí)(出隊(duì)),內(nèi)存當(dāng)中數(shù)據(jù)變化情況如下:
底層數(shù)據(jù)遍歷順序和邏輯順序
上面主要談?wù)摰降臄?shù)組在內(nèi)存當(dāng)中的布局,但是他是具體的物理存儲(chǔ)數(shù)據(jù)的順序,這個(gè)順序和我們的邏輯上的順序是不一樣的,根據(jù)上面的插入順序,我們可以畫出下面的圖,大家可以仔細(xì)分析一下這個(gè)圖的順序問題。
上圖當(dāng)中隊(duì)列左側(cè)的如隊(duì)順序是0, 1, 2, 3,右側(cè)入隊(duì)的順序?yàn)?5, 14, 13, 12, 11, 10, 9, 8,因此在邏輯上我們的隊(duì)列當(dāng)中的數(shù)據(jù)布局如下圖所示:
根據(jù)前面一小節(jié)談到的輸入在入隊(duì)的時(shí)候數(shù)組當(dāng)中數(shù)據(jù)的變化我們可以知道,數(shù)據(jù)在數(shù)組當(dāng)中的布局為:
ArrayDeque類關(guān)鍵字段分析
// 底層用于存儲(chǔ)具體數(shù)據(jù)的數(shù)組 transient Object[] elements; // 這就是前面談到的 head transient int head; // 與上文談到的 tail 含義一樣 transient int tail; // MIN_INITIAL_CAPACITY 表示數(shù)組 elements 的最短長(zhǎng)度 private static final int MIN_INITIAL_CAPACITY = 8;
以上就是ArrayDeque
當(dāng)中的最主要的字段,其含義還是比較容易理解的!
ArrayDeque構(gòu)造函數(shù)分析
默認(rèn)構(gòu)造函數(shù),數(shù)組默認(rèn)申請(qǐng)的長(zhǎng)度為16
。
public ArrayDeque() { elements = new Object[16]; }
指定數(shù)組長(zhǎng)度的初始化長(zhǎng)度,下面列出了改構(gòu)造函數(shù)涉及的所有函數(shù)。
public ArrayDeque(int numElements) { allocateElements(numElements); } 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; }
上面的最難理解的就是函數(shù)calculateSize
了,他的主要作用是如果用戶輸入的長(zhǎng)度小于MIN_INITIAL_CAPACITY
時(shí),返回MIN_INITIAL_CAPACITY
。否則返回比initialCapacity
大的第一個(gè)是2
的整數(shù)冪的整數(shù),比如說如果輸入的是9
返回的16
,輸入4
返回8
。
calculateSize
的代碼還是很難理解的,讓我們一點(diǎn)一點(diǎn)的來分析。首先我們使用一個(gè)2
的整數(shù)次冪的數(shù)進(jìn)行上面移位操作的操作!
從上圖當(dāng)中我們會(huì)發(fā)現(xiàn),我們?cè)谝粋€(gè)數(shù)的二進(jìn)制數(shù)的32位放一個(gè)1
,經(jīng)過移位之后最終32
位的比特?cái)?shù)字全部變成了1
。根據(jù)上面數(shù)字變化的規(guī)律我們可以發(fā)現(xiàn),任何一個(gè)比特經(jīng)過上面移位的變化,這個(gè)比特后面的31
個(gè)比特位都會(huì)變成1
,像下圖那樣:
因此上述的移位操作的結(jié)果只取決于最高一位的比特值為1
,移位操作后它后面的所有比特位的值全為1
,而在上面函數(shù)的最后,我們返回的結(jié)果就是上面移位之后的結(jié)果 +1
。又因?yàn)橐莆恢笞罡呶坏?code>1到最低位的1
之間的比特值全為1
,當(dāng)我們+1
之后他會(huì)不斷的進(jìn)位,最終只有一個(gè)比特位置是1
,因此它是2
的整數(shù)倍。
經(jīng)過上述過程分析,我們就可以立即函數(shù)calculateSize
了。
ArrayDeque關(guān)鍵函數(shù)分析
addLast函數(shù)分析
// tail 的初始值為 0 public void addLast(E e) { if (e == null) throw new NullPointerException(); elements[tail] = e; // 這里進(jìn)行的 & 位運(yùn)算 相當(dāng)于取余數(shù)操作 // (tail + 1) & (elements.length - 1) == (tail + 1) % elements.length // 這個(gè)操作主要是用于判斷數(shù)組是否滿了,如果滿了則需要擴(kuò)容 // 同時(shí)這個(gè)操作將 tail + 1,即 tail = tail + 1 if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity(); }
代碼(tail + 1) & (elements.length - 1) == (tail + 1) % elements.length
成立的原因是任意一個(gè)數(shù) a a a對(duì) 2 n 2^n 2n進(jìn)行取余數(shù)操作和 a a a跟 2 n − 1 2^n - 1 2n−1進(jìn)行&
運(yùn)算的結(jié)果相等,即:
從上面的代碼來看下標(biāo)為tail
的位置是沒有數(shù)據(jù)的,是一個(gè)空位置。
addFirst函數(shù)分析
// head 的初始值為 0 public void addFirst(E e) { if (e == null) throw new NullPointerException(); // 若此時(shí)數(shù)組長(zhǎng)度elements.length = 16 // 那么下面代碼執(zhí)行過后 head = 15 // 下面代碼的操作結(jié)果和下面兩行代碼含義一致 // elements[(head - 1 + elements.length) % elements.length] = e // head = (head - 1 + elements.length) % elements.length elements[head = (head - 1) & (elements.length - 1)] = e; if (head == tail) doubleCapacity(); }
上面代碼操作結(jié)果和上文當(dāng)中我們提到的,在隊(duì)列當(dāng)中從右向左加入數(shù)據(jù)一樣。從上面的代碼看,我們可以發(fā)現(xiàn)下標(biāo)為head
的位置是存在數(shù)據(jù)的。
doubleCapacity函數(shù)分析
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]; // arraycopy(Object src, int srcPos, Object dest, int destPos, int length) // 上面是函數(shù) System.arraycopy 的函數(shù)參數(shù)列表 // 大家可以參考上面理解下面的拷貝代碼 System.arraycopy(elements, p, a, 0, r); System.arraycopy(elements, 0, a, r, p); elements = a; head = 0; tail = n; }
上面的代碼還是比較簡(jiǎn)單的,這里給大家一個(gè)圖示,大家就更加容易理解了:
擴(kuò)容之后將原來數(shù)組的數(shù)據(jù)拷貝到了新數(shù)組當(dāng)中,雖然數(shù)據(jù)在舊數(shù)組和新數(shù)組當(dāng)中的順序發(fā)生變化了,但是他們的相對(duì)順序卻沒有發(fā)生變化,他們的邏輯順序也是一樣的,這里的邏輯可能有點(diǎn)繞,大家在這里可以好好思考一下。
pollLast和pollFirst函數(shù)分析
這兩個(gè)函數(shù)的代碼就比較簡(jiǎn)單了,大家可以根據(jù)前文所談到的內(nèi)容和圖示去理解下面的代碼。
public E pollLast() { // 計(jì)算出待刪除的數(shù)據(jù)的下標(biāo) int t = (tail - 1) & (elements.length - 1); @SuppressWarnings("unchecked") E result = (E) elements[t]; if (result == null) return null; // 將需要?jiǎng)h除的數(shù)據(jù)的下標(biāo)值設(shè)置為 null 這樣這塊內(nèi)存就 // 可以被回收了 elements[t] = null; tail = t; return result; } 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; }
總結(jié)
在本篇文章當(dāng)中,主要跟大家分享了ArrayDeque
的設(shè)計(jì)原理,和他的底層實(shí)現(xiàn)過程。ArrayDeque
底層數(shù)組當(dāng)中的數(shù)據(jù)順序和隊(duì)列的邏輯順序這部分可能比較抽象,大家可以根據(jù)圖示好好體會(huì)一下?。?!
到此這篇關(guān)于Java源碼刨析之ArrayDeque的文章就介紹到這了,更多相關(guān)Java ArrayDeque內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot實(shí)現(xiàn)多線程及線程池監(jiān)控
線程池的監(jiān)控很重要,本文就來介紹一下Springboot實(shí)現(xiàn)多線程及線程池監(jiān)控,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01SpringBoot項(xiàng)目部署到Tomcat的最新步驟
通過使用Spring Boot應(yīng)用程序,我們可以創(chuàng)建一個(gè)war文件來部署到Web服務(wù)器中,這篇文章主要給大家介紹了關(guān)于SpringBoot項(xiàng)目部署到Tomcat的最新步驟,需要的朋友可以參考下2024-01-01Spring循環(huán)依賴實(shí)現(xiàn)過程揭秘
這篇文章主要介紹了Spring循環(huán)依賴實(shí)現(xiàn)過程,Spring的解決循環(huán)依賴是有前置條件的,要解決循環(huán)依賴我們首先要了解Spring Bean對(duì)象的創(chuàng)建過程和依賴注入的方式2023-01-01springboot基于keytool實(shí)現(xiàn)https的雙向認(rèn)證示例教程
這篇文章主要介紹了springboot基于keytool實(shí)現(xiàn)https的雙向認(rèn)證,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06Java數(shù)據(jù)結(jié)構(gòu)與算法學(xué)習(xí)之雙向鏈表
雙向鏈表也叫雙鏈表,是鏈表的一種,它的每個(gè)數(shù)據(jù)結(jié)點(diǎn)中都有兩個(gè)指針,分別指向直接后繼和直接前驅(qū)。所以,從雙向鏈表中的任意一個(gè)結(jié)點(diǎn)開始,都可以很方便地訪問它的前驅(qū)結(jié)點(diǎn)和后繼結(jié)點(diǎn)。本文將為大家詳細(xì)介紹雙向鏈表的特點(diǎn)與使用,需要的可以參考一下2021-12-12