簡介
Queue是一種很常見的數據結構類型,在java里面Queue是一個接口,它只是定義了一個基本的Queue應該有哪些功能規約。實際上有多個Queue的實現,有的是采用線性表實現,有的基於鏈表實現。還有的適用於多線程的環境。java中具有Queue功能的類主要有如下幾個:AbstractQueue, ArrayBlockingQueue, ConcurrentLinkedQueue, LinkedBlockingQueue, DelayQueue, LinkedList, PriorityBlockingQueue, PriorityQueue和ArrayDqueue。在本文中,我們主要討論常用的兩種實現:LinkedList和ArrayDeque。
Queue
Queue本身是一種先入先出的模型(FIFO),和我們日常生活中的排隊模型很類似。根據不同的實現,他們主要有數組和鏈表兩種實現形式。如下圖:
因為在隊列里和我們日常的模型很近似,每次如果要出隊的話,都是從隊頭移除。而如果每次要加入新的元素,則要在隊尾加。所以我們要在隊列里保存隊頭和隊尾。
在jdk里幾個常用隊列實現之間的類關系圖如下:
我們可以看到,Deque也是一個接口,它繼承了Queue的接口規范。我們要討論的LinkedList和ArrayDeque都是實現Deque接口,所以,可以說他們倆都是雙向隊列。具體的實現我們會在后面討論。Queue作為一個接口,它聲明的幾個基本操作無非就是入隊和出隊的操作,具體定義如下:
public interface Queue<E> extends Collection<E> { boolean add(E e); // 添加元素到隊列中,相當於進入隊尾排隊。 boolean offer(E e); //添加元素到隊列中,相當於進入隊尾排隊. E remove(); //移除隊頭元素 E poll(); //移除隊頭元素 E element(); //獲取但不移除隊列頭的元素 E peek(); //獲取但不移除隊列頭的元素 }
Deque
按照我們一般的理解,Deque是一個雙向隊列,這將意味着它不過是對Queue接口的增強。如果仔細分析Deque接口代碼的話,我們會發現它里面主要包含有4個部分的功能定義。1. 雙向隊列特定方法定義。 2. Queue方法定義。 3. Stack方法定義。 4. Collection方法定義。
第3,4部分的方法相當於告訴我們,具體實現Deque的類我們也可以把他們當成Stack和普通的Collection來使用。這也是接口定義規約帶來的好處。這里我們就不再贅述。
我們重點來對Queue相關的定義方法做一下概括:
add相關的方法有如下幾個:
boolean add(E e); boolean offer(E e); void addFirst(E e); void addLast(E e); boolean offerFirst(E e); boolean offerLast(E e);
這里定義了add, offer兩個方法,從doc說明上來看,兩者的基本上沒什么區別。之所以定義了這兩個方法是因為Deque繼承了Collection, Queue兩個接口,而這兩個接口中都定義了增加元素的方法聲明。他們本身的目的是一樣的,只是在隊列里頭,添加元素肯定只是限於在隊列的頭或者尾添加。而offer作為一個更加適用於隊列場景中的方法,也有存在的意義。他們的實現基本上一樣,只是名字不同罷了。
remove相關的方法:
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E remove();
E poll();
這里remove相關的方法poll和remove也很類似,他們存在的原因也和前面一樣。
get元素相關的方法:
E getFirst();
E getLast();
E peekFirst();
E peekLast();
E element();
E peek();
peek和element方法和前面提到的差別有點不一樣,element方法是在隊列為空的時候拋異常,而element則是返回null。
ok,有了前面這些對方法操作的分門別類,我們后面分析起具體實現就更方便了。
ArrayDeque
有了我們前面幾篇分析的基礎,我們可以很容易猜到ArrayDeque的內部實現機制。它的內部使用一個數組來保存具體的元素,然后分別使用head, tail來指示隊列的頭和尾。他們的定義如下:
private transient E[] elements; private transient int head; private transient int tail; private static final int MIN_INITIAL_CAPACITY = 8;
ArrayDeque的默認長度為8,這么定義成2的指數值也是有一定好處的。在后面調整數組長度的時候我們會看到。關於tail需要注意的一點是tail所在的索引位置是null值,在它前面的元素才是隊列中排在最后的元素。
調整元素長度
在調整元素長度部分,ArrayDeque采用了兩個方法來分配。一個是allocateElements,還有一個是doubleCapacity。allocateElements方法用於構造函數中根據指定的參數設置初始數組的大小。而doubleCapacity則用於當數組元素不夠用了擴展數組長度。下面是allocateElements方法的實現:
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 = (E[]) new Object[initialCapacity]; }
這部分代碼里最讓人困惑的地方就是對initialCapacity做的這一大堆移位和或運算。首先通過無符號右移1位,與原來的數字做或運算,然后在右移2、4、8、16位。這么做的目的是使得最后生成的數字盡可能每一位都是1。而且很顯然,如果這個數字是每一位都為1,后面再對這個數字加1的話,則生成的數字肯定為2的若干次方。而且這個數字也肯定是大於我們的numElements值的最小2的指數值。這么說來有點繞。我們前面折騰了大半天,就為了求一個2的若干次方的數字,使得它要大於我們指定的數字,而且是最接近這個數字的數。這樣子到底是為什么呢?因為我們后面要擴展數組長度的話,有了它這個基礎我們就可以判斷這個數字是不是到了2的多少多少次方,它增長下去最大的極限也不過是2的31次方。這樣他每次的增長剛好可以把數組可以允許的長度給覆蓋了,不會出現空間的浪費。比如說,我正好有一個數組,它的長度比Integer.MAX_VALUE的一半要大幾個元素,如果我們這個時候設置的值不是讓它為2的整數次方,那么直接對它空間翻倍就導致空間不夠了,但是我們完全可以設置足夠空間來容納的。
我們現在再來看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 = (E[])a; head = 0; tail = n; }
有了前面的討論,它只要擴展空間容量的時候左移一位,這就相當於空間翻倍了。如果長度超出了允許的范圍,就會發生溢出,返回的結果就會成為一個負數。這就是為什么有 if (newCapacity < 0)這一句來拋異常。
添加元素
我們先看看兩個主要添加元素的方法add和offer:
public boolean add(E e) { addLast(e); return true; } public void addLast(E e) { if (e == null) throw new NullPointerException(); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity(); } public boolean offer(E e) { return offerLast(e); } public boolean offerLast(E e) { addLast(e); return true; }
很顯然,他們兩個方法的底層實現實際上是一樣的。這里要注意的一個地方就是我們由於不斷的入隊和出隊,可能head和tail都會移動到超過數組的末尾。這個時候如果有空閑的空間,我們會把頭或者尾跳到數組的頭開始繼續移動。所以添加元素並確定元素的下標是一個將元素下標值和數組長度進行求模運算的過程。addLast方法通過和當前數組長度減1求與運算來得到最新的下標值。它的效果相當於tail = (tail + 1) % elements.length;
public void addFirst(E e) { if (e == null) throw new NullPointerException(); elements[head = (head - 1) & (elements.length - 1)] = e; if (head == tail) doubleCapacity(); } public boolean offerFirst(E e) { addFirst(e); return true; }
addFirst和offerFirst是在head元素的之前插入元素,所以他們的位置為 (head - 1) & (elements.length - 1)。
取元素
獲取元素主要包括如下幾個方法:
public E element() { return getFirst(); } public E getFirst() { E x = elements[head]; if (x == null) throw new NoSuchElementException(); return x; } public E peek() { return peekFirst(); } public E peekFirst() { return elements[head]; // elements[head] is null if deque empty } public E getLast() { E x = elements[(tail - 1) & (elements.length - 1)]; if (x == null) throw new NoSuchElementException(); return x; } public E peekLast() { return elements[(tail - 1) & (elements.length - 1)]; }
這部分代碼算是最簡單的,無非就是取tail元素或者head元素的值。
關於隊列的幾種運算方法定義的特別雜亂,很容易讓人搞混。如果從一個最簡單的單向隊列角度來看的話,我們可以把Queue中的enqueue方法對應到addLast方法,因為我們每次添加元素就是在隊尾增加。deque方法則對應到removeFirst方法。雖然也可以用其他的方法來實現,不過具體的實現細節和他們基本上是一樣的。
LinkedList
現在,我們在來看看LinkedList對應Queue的實現部分。在前面一篇文章中,已經討論過LinkedList里面Node的結構。它本身包含元素值,prev、next兩個引用。對鏈表的增加和刪除元素的操作不像數組,不存在要考慮下標的問題,也不需要擴展數組空間,因此就簡單了很多。先看查找元素部分:
public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; } public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; }
這里唯一值得注意的一點就是last引用是指向隊列最末尾和元素,和前面ArrayDeque的情況不一樣。
添加元素的方法如下:
linkLast的方法在前一篇文章里已經分析過,就不再重復。
刪除元素的方法主要有remove():
這部分的代碼看似比較長,實際上是遍歷整個鏈表,如果找到要刪除的元素,則移除該元素。這部分的難點在unlink方法里面。我們分別用要刪除元素的前面和后面的引用來判斷各種當prev和next為null時的各種情況。雖然不是很復雜,但是很繁瑣。
總結
從我們實際中的考量來看,Queue和Deque他們本身不僅定義了作為一個隊列需要的基本功能。同時因為隊列也是屬於整個集合類這一個大族里面的,所以他們也必須要具備集合類的一些常用功能,比如元素查找,刪除,迭代器等。我們讀一些集合類的代碼時,尤其是一些接口的定義,會發現一個比較有意思的事情。就是通常一些子接口把父接口的方法又重新定義了一遍。這樣似乎違背了面向對象里繼承的原則。后來經過一些討論,發現主要原因是一些jdk版本的更新,有的新類是后面新增加的。這些新的接口有的是為了保持兼容,有的是為了保證后續生成文檔里方便用戶知道它也有同樣的功能而不需要再去查它的父類,就直接把父類的東西給搬過來了。比較有意思,讀代碼還讀出點歷史感了。
參考資料
http://docs.oracle.com/javase/7/docs/api/java/util/Queue.html
http://docs.oracle.com/javase/7/docs/api/java/util/Deque.html
http://blog.donews.com/maverick/archive/2005/08/31/534269.aspx