今天來介紹一個不太常見也不太經常使用的類——ArrayDeque,這是一個很不錯的容器類,若是對它還不瞭解的話,那麼就好好看看這篇文章吧。數組
看完本篇,你將會了解到:數據結構
一、ArrayDeque是什麼?函數
二、ArrayDeque如何使用?源碼分析
三、ArrayDeque的內部結構是怎樣的?性能
四、ArrayDeque的各個方法是如何實現的?學習
五、ArrayDeque是如何擴容的?優化
六、ArrayDeque的容量有什麼限制?ui
七、ArrayDeque和LinkedList相比有什麼優點?this
八、ArrayDeque的應用場景是什麼?spa
ArrayDeque是JDK容器中的一個雙端隊列實現,內部使用數組進行元素存儲,不容許存儲null值,能夠高效的進行元素查找和尾部插入取出,是用做隊列、雙端隊列、棧的絕佳選擇,性能比LinkedList還要好。聽到這裏,不熟悉ArrayDeque的你是否是有點尷尬?JDK中居然還有這麼好的一個容器類?
別慌,如今瞭解還來得及,趁響指尚未彈下去,快上車吧,沒時間解釋了。
來看一個ArrayDeque的使用小栗子:
public class DequeTest { public static void main(String[] args){ // 初始化容量爲4 ArrayDeque<String> arrayDeque = new ArrayDeque<>(4); //添加元素 arrayDeque.add("A"); arrayDeque.add("B"); arrayDeque.add("C"); arrayDeque.add("D"); arrayDeque.add("E"); arrayDeque.add("F"); arrayDeque.add("G"); arrayDeque.add("H"); arrayDeque.add("I"); System.out.println(arrayDeque); // 獲取元素 String a = arrayDeque.getFirst(); String a1 = arrayDeque.pop(); String b = arrayDeque.element(); String b1 = arrayDeque.removeFirst(); String c = arrayDeque.peek(); String c1 = arrayDeque.poll(); String d = arrayDeque.pollFirst(); String i = arrayDeque.pollLast(); String e = arrayDeque.peekFirst(); String h = arrayDeque.peekLast(); String h1 = arrayDeque.removeLast(); System.out.printf("a = %s, a1 = %s, b = %s, b1 = %s, c = %s, c1 = %s, d = %s, i = %s, e = %s, h = %s, h1 = %s", a,a1,b,b1,c,c1,d,i,e,h,h1); System.out.println(); // 添加元素 arrayDeque.push(e); arrayDeque.add(h); arrayDeque.offer(d); arrayDeque.offerFirst(i); arrayDeque.offerLast(c); arrayDeque.offerLast(h); arrayDeque.offerLast(c); arrayDeque.offerLast(h); arrayDeque.offerLast(i); arrayDeque.offerLast(c); System.out.println(arrayDeque); // 移除第一次出現的C arrayDeque.removeFirstOccurrence(c); System.out.println(arrayDeque); // 移除最後一次出現的C arrayDeque.removeLastOccurrence(c); System.out.println(arrayDeque); } }
輸出以下:
[A, B, C, D, E, F, G, H, I] a = A, a1 = A, b = B, b1 = B, c = C, c1 = C, d = D, i = I, e = E, h = H, h1 = H [I, E, E, F, G, H, D, C, H, C, H, I, C] [I, E, E, F, G, H, D, H, C, H, I, C] [I, E, E, F, G, H, D, H, C, H, I]
能夠看到,從ArrayDeque中取出元素的姿式可謂是五花八門,不過別慌,稍後會對這些方法進行一一講解,如今只須要知道,get、peek、element方法都是獲取元素,可是不會將它移除,而pop、poll、remove都會將元素移除並返回,add和push、offer都是插入元素,它們的不一樣點在於插入元素的位置以及插入失敗後的結果。
ArrayDeque的總體繼承結構以下:
ArrayDeque是繼承自Deque接口,Deque繼承自Queue接口,Queue是隊列,而Deque是雙端隊列,也就是能夠從前或者從後插入或者取出元素,也就是比隊列存取更加方便一點,單向隊列只能從一頭插入,從另外一頭取出。
再來看看ArrayDeque的內部結構,其實從名字就能夠看出來,ArrayDeque天然是基於Array的雙端隊列,內部結構天然是數組:
//存儲元素的數組 transient Object[] elements; // 非private訪問限制,以便內部類訪問 /** * 頭部節點序號 */ transient int head; /** * 尾部節點序號,(指向最後一點節點的後一個位置) */ transient int tail; /** * 雙端隊列的最小容量,必須是2的冪 */ private static final int MIN_INITIAL_CAPACITY = 8;
這裏能夠看到,元素都存儲在Object數組中,head記錄首節點的序號,tail記錄尾節點後一個位置的序號,隊列的容量最小爲8,並且必須爲2的冪。看到這裏,有沒有想到HashMap的元素個數限制也必須爲2的冪,嗯,這裏同HashMap同樣,自有妙用,後面會有分析。
從隊列首部插入/取出 | 從隊列尾部插入/取出 | |||
---|---|---|---|---|
失敗拋出異常 | 失敗返回特殊值 | 失敗拋出異常 | 失敗返回特殊值 | |
插入 | addFirst(e) push() | offerFirst(e) | addLast(e) | offerLast(e) |
移除 | removeFirst() pop() | pollFirst() | removeLast() | pollLast() |
獲取 | getFirst() | peekFirst() | getLast() | peekLast() |
嗯,幾乎絕大多數經常使用方法都在這裏了,基本上能夠分紅兩類,一類是以add,remove,get開頭的方法,這類方法失敗後會拋出異常,一類是以offer,poll,peek開頭的方法,這類方法失敗以後會返回特殊值,如null。大部分方法基本上都是能夠根據命名來推斷其做用,如addFirst,固然就是從隊列頭部插入,removeLast,即是從隊列尾部移除,get和peek只獲取元素而不移除,getFirst方法調用時,若是隊列爲空,會拋出NoSuchElementException異常,而peekFirst在隊列爲空時調用則返回null。
一下襬出這麼多方法有些難以接受?別慌別慌,接下來讓咱們從源碼的角度一塊兒來看看這些方法,用圖說話,來解釋咱們最開始那個栗子中到底發生了哪些事情。
先來看看構造函數:
/** * 構造一個初始容量爲16的空隊列 */ public ArrayDeque() { elements = new Object[16]; } /** * 構造一個能容納指定大小的空隊列 */ public ArrayDeque(int numElements) { allocateElements(numElements); } /** * 構造一個包含指定集合全部元素的隊列 */ public ArrayDeque(Collection<? extends E> c) { allocateElements(c.size()); addAll(c); }
因此以前栗子中,
ArrayDeque<String> arrayDeque = new ArrayDeque<>(4);
調用的是第二個構造函數,裏面有這麼一個函數allocateElements,讓咱們來看看它的實現:
1 private void allocateElements(int numElements) { 2 elements = new Object[calculateSize(numElements)]; 3 } 4 5 private static int calculateSize(int numElements) { 6 int initialCapacity = MIN_INITIAL_CAPACITY; 7 if (numElements >= initialCapacity) { 8 initialCapacity = numElements; 9 initialCapacity |= (initialCapacity >>> 1); 10 initialCapacity |= (initialCapacity >>> 2); 11 initialCapacity |= (initialCapacity >>> 4); 12 initialCapacity |= (initialCapacity >>> 8); 13 initialCapacity |= (initialCapacity >>> 16); 14 initialCapacity++; 15 16 if (initialCapacity < 0) 17 initialCapacity >>>= 1; 18 } 19 return initialCapacity; 20 }
allocateElements方法主要用於給內部的數組分配合適大小的空間,calculateSize方法用於計算比numElements大的最小2的冪次方,若是指定的容量大小小於MIN_INITIAL_CAPACITY(值爲8),那麼將容量設置爲8,不然經過屢次無符號右移進行最小2次冪計算。先將initialCapacity賦值爲numElements,接下來,進行5次無符號右移,下面將以一個小栗子介紹這樣運算的妙處。
在Java中,int類型是佔4字節,也就是32位。簡單起見,這裏以一個8位二進制數來演示前三次操做。假設這個二進制數對應的十進制爲89,整個過程以下:
能夠看到最後,除了第一位,其餘位所有變成了1,而後這個結果再加一,即獲得大於89的最小的2次冪,怎麼樣,很巧妙吧,也許你會想,爲何右移的數值要分別是1,2,4,8,16呢?嗯,好問題。其實仔細觀察就會發現,先右移在進行或操做,其實咱們只須要關注第一個不爲0的位便可,下面以64爲例再演示一次:
因此,事實上,在這系列操做中,其餘位只是配角,咱們只須要關注第一個不爲0的位便可,假設其爲第n位,先右移一位而後進行或操做,獲得的結果,第n位和第n-1位確定爲1,這樣就有兩個位爲1了,而後進行右移兩位,再進行或操做,那麼第n位到第n-3位必定都爲1,而後右移4位,依次類推。int爲32位,所以,最後只須要移動16位便可。1+2+4+8+16 = 31,因此通過這一波操做,原數字對應的二進制,操做獲得的結果將是從其第一個不爲0的位開始,日後的位均爲1。而後:
initialCapacity++;
再自增一下,目標完成。觀察到還有下面這一小節代碼:
if (initialCapacity < 0) initialCapacity >>>= 1;
其實它是爲了防止進行這一波操做以後,獲得了負數,即原來第31位爲1,獲得的結果第32位將爲1,第32位爲符號位,1表明負數,這樣的話就必須回退一步,將獲得的數右移一位(即2 ^ 30)。 嗯,那麼這一部分就先告一段落了。
來看看以前的那些函數。
public boolean add(E e) { addLast(e); return true; } /** * 在隊列頭部插入元素,若是元素爲null,則拋出異常 */ public void addFirst(E e) { if (e == null) throw new NullPointerException(); elements[head = (head - 1) & (elements.length - 1)] = e; if (head == tail) doubleCapacity(); } /** * 在隊列尾部插入元素,若是元素爲null,則拋出異常 */ public void addLast(E e) { if (e == null) throw new NullPointerException(); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity(); }
add的幾個函數比較簡單,在頭部或者尾部插入元素,若是直接調用add方法,則是在尾部插入,這時直接在對應位置塞入該元素便可。
elements[tail] = e;
而後把tail記錄其後一個位置,若是tail記錄的位置已是數組的最後一個位置了怎麼辦?嗯,這裏又有一個巧妙的操做,跟HashMap中的取模是同樣的:
tail = (tail + 1) & (elements.length - 1)
由於elements.length是2的冪次方,因此減一後就變成了掩碼,tail若是記錄的是最後一個位置,即 elements.length - 1,tail + 1 則等於elements.length,與 elements.length - 1 作與操做後,就變成了0,嗯,沒錯,這樣就變成了一個循環數組,若是tail與head相等,則表示沒有剩餘空間能夠存放更多元素了,則調用doubleCapacity進行擴容:
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的位置,而後把容量變爲原來的兩倍,而後把head以後的元素複製到新數組的開頭,把剩餘的元素複製到新數組以後。以以前的栗子爲例,新建的ArrayDeque實例容量爲8,而後咱們調用add插入元素,插入H以後,tail指向第一個位置,與head重合,就會觸發擴容。
arrayDeque.add("A"); arrayDeque.add("B"); arrayDeque.add("C"); arrayDeque.add("D"); arrayDeque.add("E"); arrayDeque.add("F"); arrayDeque.add("G"); arrayDeque.add("H"); arrayDeque.add("I");
看圖應該就比較清楚了,而後來看看獲取元素的幾個方法:
// 獲取元素 String a = arrayDeque.getFirst(); String a1 = arrayDeque.pop(); String b = arrayDeque.element(); String b1 = arrayDeque.removeFirst(); String c = arrayDeque.peek(); String c1 = arrayDeque.poll(); String d = arrayDeque.pollFirst(); String i = arrayDeque.pollLast(); String e = arrayDeque.peekFirst(); String h = arrayDeque.peekLast(); String h1 = arrayDeque.removeLast(); System.out.printf("a = %s, a1 = %s, b = %s, b1 = %s, c = %s, c1 = %s, d = %s, i = %s, e = %s, h = %s, h1 = %s", a,a1,b,b1,c,c1,d,i,e,h,h1); System.out.println();
getFirst方法直接取head位置的元素,若是爲null則拋出異常。
public E getFirst() { @SuppressWarnings("unchecked") E result = (E) elements[head]; if (result == null) throw new NoSuchElementException(); return result; }
getLast也是相似,取出tail所在位置的前一個位置,這裏也作了掩碼操做。
public E getLast() { @SuppressWarnings("unchecked") E result = (E) elements[(tail - 1) & (elements.length - 1)]; if (result == null) throw new NoSuchElementException(); return result; }
element方法直接調用的是getFirst方法:
public E element() { return getFirst(); }
remove方法有三個:
public E remove() { return removeFirst(); } public E removeFirst() { E x = pollFirst(); if (x == null) throw new NoSuchElementException(); return x; } public E removeLast() { E x = pollLast(); if (x == null) throw new NoSuchElementException(); return x; }
remove方法實際上是調用的對應的poll方法,poll方法也有三個:
public E poll() { return pollFirst(); } public E pollFirst() { int h = head; @SuppressWarnings("unchecked") E result = (E) elements[h]; // 若是結果爲null則返回null 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; }
其實也很簡單,都是先取出對應的元素,若是爲null則返回null,不然取出對應的元素並對head或tail進行調整。
pop方法調用的是removeFirst方法,removeFIrst方法調用的是pollFirst方法,因此方法看起來這麼多,實際上最後真正調用的就那麼幾個。
public E pop() { return removeFirst(); }
peek方法是取出元素可是不移除,也有三個方法:
public E peek() { return peekFirst(); } @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)]; }
這裏沒有作任何校驗,因此若是若是取到的元素是null,返回的也是null。
再來看看插入元素的其它幾個方法:
public boolean offer(E e) { return offerLast(e); } public boolean offerLast(E e) { addLast(e); return true; } public boolean offerFirst(E e) { addFirst(e); return true; } public void push(E e) { addFirst(e); }
offer方法直接調用的是add方法。
emmm,都是相互調用,爲啥要設置那麼多方法呢?其實主要是爲了模擬不一樣的數據結構,如棧操做:pop,push,peek,隊列操做:add,offer,remove,poll,peek,element,雙端隊列操做:addFirst,addLast,getFirst,getLast,peekFirst,peekLast,removeFirst,removeLast,pollFirst,pollLast。不過確實稍微多了一點。
以前的栗子裏還有用到兩個方法,removeFirstOccurrence和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; } public boolean removeLastOccurrence(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = (tail - 1) & mask; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) { delete(i); return true; } i = (i - 1) & mask; } return false; }
其實都是經過循環遍歷的方式進行查找一個是從head開始日後查找,一個是從tail開始往前查找。
最後,咱們再來看看它的迭代器類。
public Iterator<E> iterator() { return new DeqIterator(); } private class DeqIterator implements Iterator<E> { private int cursor = head; private int fence = tail; private int lastRet = -1; public boolean hasNext() { return cursor != fence; } public E next() { if (cursor == fence) throw new NoSuchElementException(); @SuppressWarnings("unchecked") E result = (E) elements[cursor]; if (tail != fence || result == null) throw new ConcurrentModificationException(); lastRet = cursor; cursor = (cursor + 1) & (elements.length - 1); return result; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); if (delete(lastRet)) { cursor = (cursor - 1) & (elements.length - 1); fence = tail; } lastRet = -1; } public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); Object[] a = elements; int m = a.length - 1, f = fence, i = cursor; cursor = f; while (i != f) { @SuppressWarnings("unchecked") E e = (E)a[i]; i = (i + 1) & m; if (e == null) throw new ConcurrentModificationException(); action.accept(e); } } }
在迭代器類中,cursor記錄的是head的位置,fence記錄的是tail的位置,lastRet記錄的是調用next返回的元素的序號,若是調用了remove方法,lastRet會置爲-1,這裏沒有像其它容器那樣使用modCount來實現fast-fail機制,而是經過在next方法中進行修改判斷。
// 若是移除了尾部元素,會致使 tail != fence // 若是移除了頭部元素,會致使 result == null if (tail != fence || result == null) throw new ConcurrentModificationException();
固然,這種檢測比較弱,若是先移除一個尾部元素,而後再添加一個尾部元素,那麼tail依舊和fence相等,這種狀況就檢測不出來了。
在調用remove方法的時候,調用了一個delete方法,這是ArrayDeque類中的一個私有方法。
private boolean delete(int i) { // 先作不變性檢測,判斷是否當前結構知足刪除需求 checkInvariants(); final Object[] elements = this.elements; // mask即掩碼 final int mask = elements.length - 1; final int h = head; final int t = tail; // front表明i到頭部的距離 final int front = (i - h) & mask; // back表明i到尾部的距離 final int back = (t - i) & mask; // 再次校驗,若是i到頭部的距離大於等於尾部到頭部的距離,表示當前隊列已經被修改了,經過最開始檢測後,i是不該該知足該條件的 if (front >= ((t - h) & mask)) throw new ConcurrentModificationException(); // 爲移動儘可能少的元素作優化,若是離頭部比較近,則將該位置到頭部的元素進行移動,若是離尾部比較近,則將該位置到尾部的元素進行移動 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; } }
因此這個delete仍是花了一點心思的,不只作了兩次校驗,還對元素移動進行了優化。嗯,到此爲止,源碼部分就差很少了。
那麼如今再回到最開始提的問題。
一、ArrayDeque是什麼?ArrayDeque是一個用循環數組實現的雙端隊列。
二、ArrayDeque如何使用?經過add,offer,poll等方法進行操做。
三、ArrayDeque的內部結構是怎樣的?內部結構是一個循環數組。
四、ArrayDeque的各個方法是如何實現的?嗯,見上文。
五、ArrayDeque是如何擴容的?擴容成原來的兩倍,而後將原來的內容複製到新數組中。
六、ArrayDeque的容量有什麼限制?容量必須爲2的冪次方,最小爲8,默認爲16.
七、ArrayDeque和LinkedList相比有什麼優點?ArrayDeque一般來講比LinkedList更高效,由於能夠在常量時間經過序號對元素進行定位,而且省去了對元素進行包裝的空間和時間成本。
八、ArrayDeque的應用場景是什麼?在不少場景下能夠用來代替LinkedList,能夠用作隊列或者棧。
到此,本篇圓滿結束。若是以爲還不錯的話,記得動動小手點個贊,也歡迎關注博主,大家的支持是我寫出更好博客的動力。
有興趣對Java進行更深刻學習和交流的小夥伴,歡迎加入QQ羣交流:529253292