集合也是一種容器,在開發過程中的應用數不勝數,除了常見的HashMap、ArrayList、LinkedList和HashSet等等,瞭解這些集合API的同時,也應該瞭解這些集合內部發生了什麼事情,這樣就不再是集合提供了什麼功能給我們用,而是我們選擇了它的什麼功能。
一、Java集合架構圖
1.集合框架提供兩個遍歷介面:Iterator和ListIterator,後者是前者的優化版,支援在集合任意一個位置進行前後雙向遍歷;
2.整個集合框架分為兩個型別:Collection和Map,前者是一個容器,儲存一系列的物件;後者是鍵值對<key, value>,儲存一系列的鍵值對;
3.在現有的集合框架體系下,衍生出四種具體集合型別:Map、Set、List、Queue;
4.Map儲存<key,value>鍵值對,查詢元素時通過key查詢value;
5.Set內部儲存一系列不可重複的物件,且是一個無序集合,物件排列順序不一;
6.List內部儲存一系列可重複的物件,是一個有序集合,物件按插入順序排列;
7.Queue是一個佇列容器,其特性與List相同,但只能從隊頭和隊尾操作元素;
8.JDK 為集合的各種操作提供了兩個工具類Collections和Arrays;
二、集合的迭代器
目前Java裡有三個迭代器:Iterator Iterable ListIterator。
1. 首先看Iterator介面:
public interface Iterator<E> { boolean hasNext(); E next(); void remove(); }
提供的API介面含義如下:
hasNext():判斷集合中是否還有下一個物件。
next():返回集合中的下一個物件,並將訪問指標移動一位。
remove():刪除集合中呼叫next()方法返回的物件。
在JDK早期版本里,遍歷集合的方式只有一種,那就是通過Iterator迭代器操作,具體例項如下:
List<Integer> list = new ArrayList<>(); list.add(10); list.add(20); list.add(30); Iterator iter = list.iterator(); while (iter.hasNext()) { Integer next = iter.next(); System.out.println(next); if (next == 20) { iter.remove(); } }
2. Iterable介面
public interface Iterable<T> { Iterator<T> iterator(); // since JDK 1.8 default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } // since JDK 1.8 default Spliterator<T> spliterator() { return Spliterators.spliteratorUnknownSize(iterator(), 0); } }
Iterable介面裡面提供了一個Iterator()方法返回迭代器,因此實現了Iterable介面的集合依舊可以使用迭代器遍歷和操作集合中的物件。在 JDK 1.8中,Iterable介面新增了一個方法forEach(),它允許使用增強 for 迴圈遍歷物件。
為什麼要設計兩個介面Iterable和Iterator?Iterator介面的保留可以讓子類去實現自己的迭代器,而Iterable介面更加關注於for-each的增強語法。
3. ListIterator
ListIterator繼承 Iterator 介面,僅存在於 List 集合之中,通過呼叫方法可以返回起始下標為 index的迭代器。ListIterator 中有幾個重要方法,大多數方法與 Iterator 中定義的含義相同,此外根據返回的迭代器,且可以實現雙向遍歷。
public interface ListIterator<E> extends Iterator<E> { boolean hasNext(); E next(); boolean hasPrevious(); E previous(); int nextIndex(); int previousIndex(); void remove(); // 替換當前下標的元素,即訪問過的最後一個元素 void set(E e); void add(E e); }
三、Map和Collection介面
Map介面和Collection介面是Java的集合框架中的兩個重要門派,Collection儲存的是集合元素本身,Map儲存的是<key,value>鍵值對,但是部分Collection子類使用了Map來實現的。例如:HashSet底層使用了HashMap,TreeSet底層使用了TreeMap,LinkedHashSet底層使用了LinkedHashMap
Map介面的資料結構是<key, value>形式,key 對映value,一個key對應一個value,key不可重複,value可重複。Map介面細分為不同的種類:
SortedMap介面:該類對映可以對<key, value>按照自己的規則進行排序,具體實現有 TreeMap
AbsractMap抽象類:它為子類提供好一些通用的API實現,所有的具體Map如HashMap都會繼承它
Collection介面提供了所有集合的通用方法(注意這裡不包括Map):
新增方法:add(E e) / addAll(Collection<? extends E> var1)/ addAll(int index, Collection<? extends E> var1)
查詢方法:contains(Object var1) / containsAll(Collection<?> var1)/
查詢集合自身資訊:size() / isEmpty()
刪除方法:remove(O o)/removeAll(Collection<?> var1)/求交集:retainAll(Collection c)
... ...
Collection介面將集合細分為不同的種類:
Set介面:一個不允許儲存重複元素的無序集合,具體實現有HashSet / TreeSet···
List介面:一個可儲存重複元素的有序集合,具體實現有ArrayList / LinkedList···
Queue介面:一個可儲存重複元素的佇列,具體實現有PriorityQueue / ArrayDeque···
1. Map介面詳解
Map 體系下主要分為 AbstractMap 和 SortedMap兩類集合
AbstractMap是對Map介面的擴充套件,定義了普通Map集合具有的通用方法,可避免子類重複編寫大量相同程式碼,子類繼承 AbstractMap 後可重寫它的方法,並實現額外邏輯,對外可提供更多的功能。
SortedMap介面定義了Map具有排序行為,當子類實現它時,必須重寫所有方法,對外提供排序功能。
1.1 HashMap
HashMap 是一個最通用的利用雜湊表儲存元素的集合,有元素加入HashMap時,會將key的雜湊值轉換為陣列的索引下標確定存放位置,在查詢元素時,根據key的雜湊地址轉換成陣列的索引下標確定查詢位置。
HashMap 底層是用陣列 + 連結串列 + 紅黑樹這三種資料結構實現,是非執行緒安全的集合。
加入元素髮生雜湊衝突時,HashMap會將相同地址的元素連成一條連結串列,若連結串列長度大於8,且陣列長度大於64會轉換成紅黑樹資料結構。
關於 HashMap 的簡要總結:
1. 它是集合中最常用的Map集合型別,底層由陣列 + 連結串列 + 紅黑樹組成;
2. HashMap不是執行緒安全的;
3. 插入元素時,通過計算元素雜湊值,通過雜湊對映函式轉換為陣列下標;查詢元素時,通過雜湊對映函式得到陣列下標定位元素的位置。
1.2 LinkedHashMap
LinkedHashMap可以看作是 HashMap 和 LinkedList 的結合:它是在 HashMap 的基礎上新增了一條雙向連結串列,預設儲存各個元素的插入順序,但由於這條雙向連結串列,使得 LinkedHashMap 可以實現 LRU快取淘汰策略,因為我們可以設定這條雙向連結串列按照元素的訪問次序進行排序
// 頭節點 transient LinkedHashMap.Entry<K, V> head; // 尾結點 transient LinkedHashMap.Entry<K, V> tail;
利用 LinkedHashMap 可以實現 LRU 快取淘汰策略,因為它提供了一個方法:
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) { return false; }
該方法可以移除最靠近連結串列頭部的一個節點,而在get(key)方法原始碼如下,其作用是挪動結點的位置:
public V get(Object key) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null) return null; if (accessOrder) afterNodeAccess(e); return e.value; }
只要呼叫了get(key)且accessOrder = true,則會將該節點更新到連結串列尾部,具體的邏輯在afterNodeAccess()中,原始碼如下:
void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } }
指定accessOrder = true,可以設定連結串列按照訪問順序排列,通過提供的構造器可以設定accessOrder。
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
重寫removeEldestEntry()方法,內部定義邏輯,通常是判斷容量是否達到上限,若是則執行淘汰。
關於 LinkedHashMap 總結兩點:
1. 底層維護了一條雙向連結串列,繼承了 HashMap,所以不是執行緒安全的
2. LinkedHashMap 可實現LRU快取淘汰策略,其原理是通過設定accessOrder為true並重寫removeEldestEntry方法定義淘汰元素時需滿足的條件。
1.3 TreeMap
TreeMap 是 SortedMap 的子類,所以它具有排序功能,是基於紅黑樹資料實現的,每個鍵值對<key, value>都是一個結點,預設情況下按照key自然排序,另一種是可以通過傳入定製的Comparator進行自定義規則排序。
// 按照 key 自然排序,Integer 的自然排序是升序 TreeMap<Integer, Object> naturalSort = new TreeMap<>(); // 定製排序,按照 key 降序排序 TreeMap<Integer, Object> customSort = new TreeMap<>((o1, o2) -> Integer.compare(o2, o1));
圖中紅黑樹的每一個節點都是一個Entry,在這裡不標明 key 和 value 了,這些元素都是已按照key排好序了,整個資料結構都是保持著有序狀態!
關於自然排序與定製排序:
自然排序:要求key必須實現Comparable介面。
由於Integer類實現了Comparable 介面,按照自然排序規則是按照key從小到大排序。
TreeMap<Integer, String> treeMap = new TreeMap<>(); treeMap.put(2, "貳"); treeMap.put(1, "壹"); System.out.print(treeMap); // {1=壹, 2=貳}
定製排序:在初始化 TreeMap 時傳入新的Comparator,不要求key實現 Comparable 介面
TreeMap<Integer, String> treeMap = new TreeMap<>((o1, o2) -> Integer.compare(o2, o1)); treeMap.put(1, "壹"); treeMap.put(2, "貳"); treeMap.put(4, "肆"); treeMap.put(3, "叄"); System.out.println(treeMap); // {4=肆, 3=叄, 2=貳, 1=壹}
關於 TreeMap 主要介紹了三點:
1. 它底層是由紅黑樹實現的,操作的時間複雜度為O(logN);
2. TreeMap 可以對key進行自然排序或者自定義排序,自定義排序時需要傳入Comparator,而自然排序要求key實現了Comparable介面;
3. TreeMap 不是執行緒安全的。
1.4 WeakHashMap
WeakHashMap底層儲存的元素的資料結構是陣列 + 連結串列,沒有紅黑樹。日常開發中用的很少,是基於Map實現,Entry中鍵在每一次垃圾回收都會被清除,適合用於短暫訪問或訪問一次的元素,快取在WeakHashMap中,並儘早地把它回收掉。
public class WeakHashMap<K, V> extends AbstractMap<K, V> implements Map<K, V> { }
當Entry被GC時,WeakHashMap 是如何感知到某個元素被回收的呢?
在 WeakHashMap 內部維護了一個引用佇列queue:
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
queue裡包含了所有被GC掉的key,當JVM開啟GC後,如果回收掉WeakHashMap中的key,會將key放入queue 中,在expungeStaleEntries()中遍歷 queue,把queue中的所有key拿出來,並在WeakHashMap中刪除掉,以達到同步。
圖中被虛線標識的元素將會在下一次訪問 WeakHashMap 時被刪除,WeakHashMap 內部會做好一系列的調整工作,所以佇列的作用是標誌已經被GC掉的元素。
關於 WeakHashMap 需要注意三點:
1. 它的鍵是一種弱鍵,放入 WeakHashMap 時,隨時會被回收掉,所以不能確保某次訪問元素一定存在;
2. 它依賴普通的Map進行實現,是一個非執行緒安全的集合;
3. WeakHashMap 通常作為快取使用,適合儲存那些只需訪問一次、或只需儲存短暫時間的鍵值對。
1.5 HashTable
Hashtable底層儲存結構是陣列+連結串列,是一個執行緒安全的集合。當連結串列過長時,查詢效率過低,會長時間鎖住Hashtable,所以在併發環境下,效能很差,現在基本上被淘汰了,也很少用了。執行緒安全,它所有的方法都被加上了 synchronized 關鍵字。HashTable 預設長度為 11,負載因子為 0.75F,即元素個數達到陣列長度的 75% 時,會進行一次擴容,每次擴容為原來陣列長度的 2 倍
2. Collection 集合體系詳解
Collection 集合體系的頂層介面就是Collection,它規定了該集合下的一系列方法。
該集合下可以分為三大類集合:List,Set和Queue
Set介面:不可儲存重複的元素,且任何操作均需要通過雜湊函式對映到集合內部定位元素,集合內部元素預設無序。
List介面:可儲存重複的元素,且集合內部的元素按照元素插入的順序有序排列,可以通過索引訪問元素。
Queue介面:是以佇列作為儲存結構,集合內部的元素有序排列,僅可以操作頭結點元素,無法訪問佇列中間的元素。
上面三個介面是最普通,最抽象的實現,而在各個集合介面內部,還會有更加具體的表現,衍生出各種不同的額外功能,使開發者能夠對比各個集合的優勢,擇優使用。
2.1 Set介面
Set介面繼承了Collection介面,是一個不包括重複元素的集合,更確切地說,Set 中任意兩個元素不會出現 o1.equals(o2),而且 Set 至多隻能儲存一個 NULL 值元素。
在Set集合體系中,我們需要著重關注兩點:
存入可變元素時,必須非常小心,因為任意時候元素狀態的改變都有不可能使得 Set 內部出現兩個相等的元素,即 o1.equals(o2) = true,所以一般不要更改存入 Set 中的元素,否則將會破壞了 equals() 的作用!
Set 的最大作用就是判重,在專案中最大的作用也是判重!
HashSet
HashSet 底層藉助 HashMap 實現,我們可以觀察它的多個構造方法,本質上都是 new 一個 HashMap
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable { public HashSet() { this.map = new HashMap(); } public HashSet(int initialCapacity, float loadFactor) { this.map = new HashMap(initialCapacity, loadFactor); } public HashSet(int initialCapacity) { this.map = new HashMap(initialCapacity); } }
我們可以觀察add()方法和remove()方法是如何將 HashSet 的操作嫁接到 HashMap 的。
private static final Object PRESENT = new Object(); public boolean add(E e) { return this.map.put(e, PRESENT) == null; } public boolean remove(Object o) { return this.map.remove(o) == PRESENT; }
HashMap 的 value 值,使用HashSet的開發者只需關注於需要插入的 key,遮蔽了 HashMap 的 value
HashSet 在 HashMap 基礎上實現,所以很多地方可以聯絡到 HashMap:
底層資料結構:HashSet 也是採用陣列 + 連結串列 + 紅黑樹實現
執行緒安全性:由於採用 HashMap 實現,而 HashMap 本身執行緒不安全,在HashSet 中沒有新增額外的同步策略,所以 HashSet 也執行緒不安全
存入 HashSet 的物件的狀態最好不要發生變化,因為有可能改變狀態後,在集合內部出現兩個元素o1.equals(o2),破壞了 equals()的語義。
LinkedHashSet
LinkedHashSet 的程式碼少的可憐,如下:
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { private static final long serialVersionUID = -2851667679971038690L; public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); } public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); } public LinkedHashSet() { super(16, .75f, true); } public LinkedHashSet(Collection<? extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); addAll(c); } @Override public Spliterator<E> spliterator() { return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED); } }
關於 LinkedHashSet 需要注意幾個地方:
它繼承了 HashSet,而 HashSet 預設是採用 HashMap 儲存資料的,但是 LinkedHashSet 呼叫父類構造方法初始化 map 時是 LinkedHashMap 而不是 HashMap,這個要額外注意一下;
由於 LinkedHashMap 不是執行緒安全的,且在 LinkedHashSet 中沒有新增額外的同步策略,所以 LinkedHashSet 集合也不是執行緒安全的。
TreeMap
TreeSet 是基於 TreeMap 的實現,所以儲存的元素是有序的,底層的資料結構是陣列 + 紅黑樹。
而元素的排列順序有2種,和 TreeMap 相同:自然排序和定製排序,常用的構造方法已經在下面展示出來了,TreeSet 預設按照自然排序,如果需要定製排序,需要傳入Comparator。
public TreeSet() { this(new TreeMap<E,Object>()); } public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); }
對 TreeSet 介紹了它的主要實現方式和應用場景,有幾個值得注意的點。
TreeSet 的所有操作都會轉換為對 TreeMap 的操作,TreeMap 採用紅黑樹實現,任意操作的平均時間複雜度為 O(logN);
TreeSet 是一個執行緒不安全的集合;
TreeSet 常應用於對不重複的元素定製排序,例如玩家戰力排行榜。
2.2 List
List 介面直接繼承 Collection 介面,它定義為可以儲存重複元素的集合,並且元素按照插入順序有序排列,且可以通過索引訪問指定位置的元素。常見的實現有:ArrayList、LinkedList、Vector和Stack。
AbstractList 和 AbstractSequentialList
AbstractList 抽象類實現了 List 介面,其內部實現了所有的 List 都需具備的功能,子類可以專注於實現自己具體的操作邏輯。
AbstractSequentialList 抽象類繼承了 AbstractList,在原基礎上限制了訪問元素的順序只能夠按照順序訪問,而不支援隨機訪問,如果需要滿足隨機訪問的特性,則繼承 AbstractList。子類 LinkedList 使用連結串列實現,所以僅能支援順序訪問,顧繼承了 AbstractSequentialList而不是 AbstractList。
// 查詢元素 o 第一次出現的索引位置 public int indexOf(Object o) // 查詢元素 o 最後一次出現的索引位置 public int lastIndexOf(Object o) //···
Vector 和 Stack
Vector 在現在已經是一種過時的集合了,包括繼承它的 Stack 集合也如此,它們被淘汰的原因都是因為效能低下。
JDK 1.0 時代,ArrayList 還沒誕生,大家都是使用 Vector 集合,但由於 Vector 的每個操作都被 synchronized 關鍵字修飾,即使線上程安全的情況下,仍然進行無意義的加鎖與釋放鎖,造成額外的效能開銷,做了無用功。
在 JDK 1.2 時,Collection 家族出現了,它提供了大量高效能、適用於不同場合的集合,而 Vector 也是其中一員,但由於 Vector 在每個方法上都加了鎖,由於需要相容許多老的專案,很難在此基礎上優化Vector了,所以漸漸地也就被歷史淘汰了。
現在,線上程安全的情況下,不需要選用 Vector 集合,取而代之的是 ArrayList 集合;在併發環境下,出現了 CopyOnWriteArrayList,Vector 完全被棄用了。
Stack是一種後入先出(LIFO)型的集合容器,如圖中所示,大雄是最後一個進入容器的,top指標指向大雄,那麼彈出元素時,大雄也是第一個被彈出去的。
Stack 繼承了 Vector 類,提供了棧頂的壓入元素操作(push)和彈出元素操作(pop),以及檢視棧頂元素的方法(peek)等等,但由於繼承了 Vector,正所謂跟錯老大沒福報,Stack 也漸漸被淘汰了。
取而代之的是後起之秀 Deque介面,其實現有 ArrayDeque,該資料結構更加完善、可靠性更好,依靠佇列也可以實現LIFO的棧操作,所以優先選擇 ArrayDeque 實現棧。
ArrayList
ArrayList 以陣列作為儲存結構,它是執行緒不安全的集合;具有查詢快、在陣列中間或頭部增刪慢的特點,所以它除了執行緒不安全這一點,其餘可以替代Vector,而且執行緒安全的 ArrayList 可以使用 CopyOnWriteArrayList代替 Vector。
關於 ArrayList 有幾個重要的點需要注意的:
具備隨機訪問特點,訪問元素的效率較高,ArrayList 在頻繁插入、刪除集合元素的場景下效率較低。
底層資料結構:ArrayList 底層使用陣列作為儲存結構,具備查詢快、增刪慢的特點
執行緒安全性:ArrayList 是執行緒不安全的集合
ArrayList 首次擴容後的長度為 10,呼叫 add() 時需要計算容器的最小容量。可以看到如果陣列elementData為空陣列,會將最小容量設定為10,之後會將陣列長度完成首次擴容到 10。
// new ArrayList 時的預設空陣列 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 預設容量 private static final int DEFAULT_CAPACITY = 10; // 計算該容器應該滿足的最小容量 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
集合從第二次擴容開始,陣列長度將擴容為原來的 1.5 倍,即:newLength = oldLength * 1.5
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
LinkedList
LinkedList 底層採用雙向連結串列資料結構儲存元素,由於連結串列的記憶體地址非連續,所以它不具備隨機訪問的特點,但由於它利用指標連線各個元素,所以插入、刪除元素只需要操作指標,不需要移動元素,故具有增刪快、查詢慢的特點。它也是一個非執行緒安全的集合。
由於以雙向連結串列作為資料結構,它是執行緒不安全的集合;儲存的每個節點稱為一個Node,下圖可以看到 Node 中儲存了next和prev指標,item是該節點的值。在插入和刪除時,時間複雜度都保持為 O(1)
關於 LinkedList,除了它是以連結串列實現的集合外,還有一些特殊的特性需要注意的。
優勢:LinkedList 底層沒有擴容機制,使用雙向連結串列儲存元素,所以插入和刪除元素效率較高,適用於頻繁操作元素的場景
劣勢:LinkedList 不具備隨機訪問的特點,查詢某個元素只能從 head 或 tail 指標一個一個比較,所以查詢中間的元素時效率很低
查詢優化:LinkedList 查詢某個下標 index 的元素時做了優化,若 index > (size / 2),則從 head 往後查詢,否則從 tail 開始往前查詢,程式碼如下所示:
LinkedList.Node<E> node(int index) { LinkedList.Node x; int i; if (index < this.size >> 1) { // 查詢的下標處於連結串列前半部分則從頭找 x = this.first; for(i = 0; i < index; ++i) { x = x.next; } return x; } else { // 查詢的下標處於陣列的後半部分則從尾開始找 x = this.last; for(i = this.size - 1; i > index; --i) { x = x.prev; } return x; } }
雙端佇列:使用雙端連結串列實現,並且實現了 Deque 介面,使得 LinkedList 可以用作雙端佇列。下圖可以看到 Node 是集合中的元素,提供了前驅指標和後繼指標,還提供了一系列操作頭結點和尾結點的方法,具有雙端佇列的特性。
LinkedList 集合最讓人樹枝的是它的連結串列結構,但是我們同時也要注意它是一個雙端佇列型的集合。
Deque<Object> deque = new LinkedList<>();
Queue
Queue佇列,在 JDK 中有兩種不同型別的集合實現:單向佇列(AbstractQueue) 和 雙端佇列(Deque)。
Queue 中提供了兩套增加、刪除元素的 API,當插入或刪除元素失敗時,會有兩種不同的失敗處理策略。
方法及失敗策略 | 插入方法 | 刪除方法 | 查詢方法 |
---|---|---|---|
丟擲異常 | add() | remove() | get() |
返回預設值 | offer() | poll() | peek() |
選取哪種方法的決定因素:插入和刪除元素失敗時,希望丟擲異常還是返回布林值,
add() 和 offer() 對比:
在佇列長度大小確定的場景下,佇列放滿元素後,新增下一個元素時,add() 會丟擲 IllegalStateException異常,而 offer() 會返回 false 。
remove() 和 poll() 對比:
在佇列為空的場景下, remove() 會丟擲 NoSuchElementException異常,而 poll() 則返回 null 。
get()和peek()對比:
在佇列為空的情況下,get()會丟擲NoSuchElementException異常,而peek()則返回null。
Deque
Deque 介面的實現非常好理解:從單向佇列演變為雙向佇列,內部額外提供雙向佇列的操作方法即可:
Deque介面額外提供了針對佇列的頭結點和尾結點操作的方法,而插入、刪除方法同樣也提供了兩套不同的失敗策略。除了add()和offer(),remove()和poll()以外,還有get()和peek()出現了不同的策略
ArrayDeque
使用陣列實現的雙端佇列,它是無界的雙端佇列,最小的容量是8(JDK 1.8)。在 JDK 11 看到它預設容量已經是 16了。ArrayDeque 在日常使用得不多,值得注意的是它與 LinkedList 的對比:LinkedList 採用連結串列實現雙端佇列,而 ArrayDeque 使用陣列實現雙端佇列。
/** * 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;
由於雙端佇列只能在頭部和尾部操作元素,所以刪除元素和插入元素的時間複雜度大部分都穩定在 O(1) ,除非在擴容時會涉及到元素的批量複製操作。但是在大多數情況下,使用它時應該指定一個大概的陣列長度,避免頻繁的擴容。
個人觀點:連結串列的插入、刪除操作涉及到指標的操作,我個人認為作者是覺得陣列下標的移動要比指標的操作要廉價,而且陣列採用連續的記憶體地址空間,而連結串列元素的記憶體地址是不連續的,所以陣列操作元素的效率在定址上會比連結串列要快。請批判看待觀點。
PriorityQueue
PriorityQueue 基於優先順序堆實現的優先順序佇列,而堆是採用陣列實現:
/** * Priority queue represented as a balanced binary heap: the two * children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The * priority queue is ordered by comparator, or by the elements\' * natural ordering, if comparator is null: For each node n in the * heap and each descendant d of n, n <= d. The element with the * lowest value is in queue[0], assuming the queue is nonempty. */ transient Object[] queue; // non-private to simplify nested class access
文件中的描述告訴我們:該陣列中的元素通過傳入 Comparator 進行定製排序,如果不傳入Comparator時,則按照元素本身自然排序,但要求元素實現了Comparable介面,所以 PriorityQueue 不允許儲存 NULL 元素。
PriorityQueue 應用場景:元素本身具有優先順序,需要按照優先順序處理元素
例如VIP玩家與普通玩家,VIP 等級越高的玩家越優先安排玩耍,減少VIP玩家流失。
public static void main(String[] args) { Player vip1 = new Player("范仲淹", 1); Player vip3 = new Player("竇憲", 2); Player vip4 = new Player("弘曆", 4); Player vip2 = new Player("蘇定方", 1); Player p1 = new Player("王陽明", 0); Player p2 = new Player("趙孟頫", 0); // 根據VIP等級降序 PriorityQueue<Player> queue = new PriorityQueue<>((o1, o2) -> o2.getScore().compareTo(o1.getScore())); queue.add(vip1);queue.add(vip4);queue.add(vip3); queue.add(p1);queue.add(p2);queue.add(vip2); while (!queue.isEmpty()) { Player s1 = queue.poll(); System.out.println(s1.getName() + "進入遊戲; " + "VIP等級: " + s1.getScore()); } } public static class Player implements Comparable<Player> { private String name; private Integer score; public Player(String name, Integer score) { this.name = name; this.score = score; } @Override public int compareTo(Player o) { return this.score.compareTo(o.getScore()); } }
結果如下:
弘曆進入遊戲; VIP等級: 4 竇憲進入遊戲; VIP等級: 2 范仲淹進入遊戲; VIP等級: 1 蘇定方進入遊戲; VIP等級: 1 王陽明進入遊戲; VIP等級: 0 趙孟頫進入遊戲; VIP等級: 0
VIP 等級越高(優先順序越高)就越優先安排玩(優先處理),類似這種有優先順序的場景還有非常多,各位可以發揮自己的想象力。
PriorityQueue 總結:
PriorityQueue 是基於優先順序堆實現的優先順序佇列,而堆是用陣列維護的
PriorityQueue 適用於元素按優先順序處理的業務場景,例如使用者在請求人工客服需要排隊時,根據使用者的VIP等級進行 插隊 處理,等級越高,越先安排客服。
四、下面來個大總結:
資料型別 | 插入刪除的時間複雜度 | 查詢時間複雜度 | 底層資料結構 | 是否執行緒安全 |
---|---|---|---|---|
Vector | O(N) | O(1) | 陣列 | 是 |
ArrayList | O(N) | O(1) | 陣列 | 否 |
LinkedList | O(1) | O(N) | 雙向連結串列 | 否 |
HashSet | O(1) | O(1) | 陣列+連結串列+紅黑樹 | 否 |
TreeSet | O(LogN) | O(LogN) | 紅黑樹 | 否 |
LingkedHashSet | O(1) | O(1)~ O(N) | 陣列+連結串列+紅黑樹 | 否 |
ArrayDeque | O(N) | O(1) | 陣列 | 否 |
PriorityQueue | O(LogN) | O(LogN) | 堆(陣列) | 否 |
HashMap | O(1)~ O(N) | O(1)~ O(N) | 陣列+連結串列+紅黑樹 | 否 |
TreeMap | O(LogN) | O(LogN) | 陣列+紅黑樹 | 否 |
HashTable | O(1) / O(N) | O(1) / O(N) | 陣列+連結串列 | 是 |