一、概述
隊列,又稱為佇列(queue),是先進先出(FIFO, First-In-First-Out)的線性表。在具體應用中通常用鏈表或者數組來實現。隊列只允許在后端(稱為rear)進行插入操作,在前端(稱為front)進行刪除操作。隊列的操作方式和堆棧類似,唯一的區別在於隊列只允許新數據在后端進行添加。
在Java中隊列又可以分為兩個大類,一種是阻塞隊列和非阻塞隊列。
1、沒有實現阻塞接口:
1)實現java.util.Queue的LinkList,
2)實現java.util.AbstractQueue接口內置的不阻塞隊列: PriorityQueue 和 ConcurrentLinkedQueue
2、實現阻塞接口的
java.util.concurrent 中加入了 BlockingQueue 接口和五個阻塞隊列類。它實質上就是一種帶有一點扭曲的 FIFO 數據結構。不是立即從隊列中添加或者刪除元素,線程執行操作阻塞,直到有空間或者元素可用。
五個隊列所提供的各有不同:
* ArrayBlockingQueue :一個由數組支持的有界隊列。
* LinkedBlockingQueue :一個由鏈接節點支持的可選有界隊列。
* PriorityBlockingQueue :一個由優先級堆支持的無界優先級隊列。
* DelayQueue :一個由優先級堆支持的、基於時間的調度隊列。
* SynchronousQueue :一個利用 BlockingQueue 接口的簡單聚集(rendezvous)機制。
隊列是Java中常用的數據結構,比如在線程池中就是用到了隊列,比如消息隊列等。
由隊列先入先出的特性,我們知道隊列數據的存儲結構可以兩種,一種是基於數組實現的,另一種則是基於單鏈實現。前者在創建的時候就已經確定了數組的長度,所以隊列的長度是固定的,但是可以循環使用數組,所以這種隊列也可以稱之為循環隊列。后者實現的隊列內部通過指針指向形成一個隊列,這種隊列是單向且長度不固定,所以也稱之為非循環隊列。下面我將使用兩種方式分別實現隊列。
二、基於數組實現循環隊列
由於在往隊列中放數據或拉取數據的時候需要移動數組對應的下標,所以需要記錄一下隊尾和隊頭的位置。說一下幾個核心的屬性吧:
1、queue:隊列,object類型的數組,用於存儲數據,長度固定,當存儲的數據數量大於數組當度則拋出異常;
2、head:隊頭指針,int類型,用於記錄隊列頭部的位置信息。
3、tail:隊尾指針,int類型,用於記錄隊列尾部的位置信息。
4、size:隊列長度,隊列長度大於等於0或者小於等於數組長度。
/** * 隊列管道,當管道中存放的數據大於隊列的長度時將不會再offer數據,直至從隊列中poll數據后 */ private Object[] queue; //隊列的頭部,獲取數據時總是從頭部獲取 private int head; //隊列尾部,push數據時總是從尾部添加 private int tail; //隊列長度 private int size; //數組中能存放數據的最大容量 private final static int MAX_CAPACITY = 1<<30; //數組長度 private int capacity; //最大下標 private int maxIndex;
三、數據結構
圖中,紅色部分即為隊列的長度,數組的長度為16。因為這個隊列是循環隊列,所以隊列的頭部不一定要在隊列尾部前面,只要隊列的長度不大於數組的長度就可以了。
四、構造方法
1、MyQueue(int initialCapacity):創建一個最大長度為 initialCapacity的隊列。
2、MyQueue():創建一個默認最大長度的隊列,默認長度為16;
public MyQueue(int initialCapacity){ if (initialCapacity > MAX_CAPACITY) throw new OutOfMemoryError("initialCapacity too large"); if (initialCapacity <= 0) throw new IndexOutOfBoundsException("initialCapacity must be more than zero"); queue = new Object[initialCapacity]; capacity = initialCapacity; maxIndex = initialCapacity - 1; head = tail = -1; size = 0; } public MyQueue(){ queue = new Object[16]; capacity = 16; head = tail = -1; size = 0; maxIndex = 15; }
五、往隊列添加數據
添加數據時,首先判斷隊列的長度是否超出了數組的長度,如果超出了就添加失敗(也可以設置成等待,等到有人消費了隊列里的數據,然后再添加進去)。都是從隊列的尾部添加數據的,添加完數據后tail指針也會相應自增1。具體實現如一下代碼:
/** * 往隊列尾部添加數據 * @param object 數據 */ public void offer(Object object){ if (size >= capacity){ System.out.println("queue's size more than or equal to array's capacity"); return; } if (++tail > maxIndex){ tail = 0; } queue[tail] = object; size++; }
六、向隊列中拉取數據
拉取數據是從隊列頭部拉取的,拉取完之后將該元素刪除,隊列的長度減一,head自增1。代碼如下:
/** * 從隊列頭部拉出數據 * @return 返回隊列的第一個數據 */ public Object poll(){ if (size <= 0){ System.out.println("the queue is null"); return null; } if (++head > maxIndex){ head = 0; } size--; Object old = queue[head]; queue[head] = null; return old; }
七、其他方法
1、查看數據:這個方法跟拉取數據一樣都是獲取到隊列頭部的數據,區別是該方法不會將對頭數據刪除:
/** * 查看第一個數據 * @return */ public Object peek(){ return queue[head]; }
2、清空隊列:
/** * 清空隊列 */ public void clear(){ for (int i = 0; i < queue.length; i++) { queue[i] = null; } tail = head = -1; size = 0; }
完整的代碼如下:
/** * 隊列 */ public class MyQueue { /** * 隊列管道,當管道中存放的數據大於隊列的長度時將不會再offer數據,直至從隊列中poll數據后 */ private Object[] queue; //隊列的頭部,獲取數據時總是從頭部獲取 private int head; //隊列尾部,push數據時總是從尾部添加 private int tail; //隊列長度 private int size; //數組中能存放數據的最大容量 private final static int MAX_CAPACITY = 1<<30; //數組長度 private int capacity; //最大下標 private int maxIndex; public MyQueue(int initialCapacity){ if (initialCapacity > MAX_CAPACITY) throw new OutOfMemoryError("initialCapacity too large"); if (initialCapacity <= 0) throw new IndexOutOfBoundsException("initialCapacity must be more than zero"); queue = new Object[initialCapacity]; capacity = initialCapacity; maxIndex = initialCapacity - 1; head = tail = -1; size = 0; } public MyQueue(){ queue = new Object[16]; capacity = 16; head = tail = -1; size = 0; maxIndex = 15; } /** * 往隊列尾部添加數據 * @param object 數據 */ public void offer(Object object){ if (size >= capacity){ System.out.println("queue's size more than or equal to array's capacity"); return; } if (++tail > maxIndex){ tail = 0; } queue[tail] = object; size++; } /** * 從隊列頭部拉出數據 * @return 返回隊列的第一個數據 */ public Object poll(){ if (size <= 0){ System.out.println("the queue is null"); return null; } if (++head > maxIndex){ head = 0; } size--; Object old = queue[head]; queue[head] = null; return old; } /** * 查看第一個數據 * @return */ public Object peek(){ return queue[head]; } /** * 隊列中存儲的數據量 * @return */ public int size(){ return size; } /** * 隊列是否為空 * @return */ public boolean isEmpty(){ return size == 0; } /** * 清空隊列 */ public void clear(){ for (int i = 0; i < queue.length; i++) { queue[i] = null; } tail = head = -1; size = 0; } @Override public String toString() { if (size <= 0) return "{}"; StringBuilder builder = new StringBuilder(size + 8); builder.append("{"); int h = head; int count = 0; while (count < size){ if (++h > maxIndex) h = 0; builder.append(queue[h]); builder.append(", "); count++; } return builder.substring(0,builder.length()-2) + "}"; } }
八、總結:
1、該隊列為非線程安全的,在多線程環境中可能會發生數據丟失等問題。
2、隊列通過移動指針來確定數組下標的位置,因為是基於數組實現的,所以隊列的長度不能夠超過數組的長度。
3、該隊列是循環隊列,這就意味着數組可以重復被使用,避免了重復創建對象帶來的性能的開銷。
4、添加數據時總是從隊列尾部添加,拉取數據時總是從隊列頭部拉取,拉取完將對象元素刪除。
歡迎大家關注公眾號: 【java解憂雜貨鋪】,里面會不定時發布一些技術博客;關注即可免費領取大量最新,最流行的技術教學視頻: