點擊藍色「Java建設者 」關注我喲java
加個「星標」,及時閱讀最新技術文章node
這篇文章歷通過 5 次的打磨和修復,只爲把最好的文章爲你們分享。
web
集合在咱們平常開發使用的次數數不勝數,ArrayList
/LinkedList
/HashMap
/HashSet
······信手拈來,擡手就拿來用,在 IDE 上龍飛鳳舞,可是做爲一名合格的優雅的程序猿,僅僅瞭解怎麼使用API
是遠遠不夠的,若是在調用API
時,知道它內部發生了什麼事情,就像開了透視
外掛同樣,洞穿一切,這種感受才真的爽,並且這樣就不是集合提供什麼功能給咱們使用,而是咱們選擇使用它的什麼功能了。面試

這是Java建設者第 108 篇原創文章

1數組
集合框架總覽 緩存
下圖堪稱集合框架的上帝視角,講到集合框架不得不看的就是這幅圖,固然,你會以爲眼花繚亂,不知如何看起,這篇文章帶你一步一步地秒殺上面的每個接口、抽象類和具體類。咱們將會從最頂層的接口開始講起,一步一步往下深刻,幫助你把對集合的認知構建起一個知識網絡。安全

工欲善其事必先利其器,讓咱們先來過一遍整個集合框架的組成部分:服務器
-
集合框架提供了兩個遍歷接口: Iterator
和ListIterator
,其中後者是前者的優化版
,支持在任意一個位置進行先後雙向遍歷。注意圖中的Collection
應當繼承的是Iterable
而不是Iterator
,後面會解釋Iterable
和Iterator
的區別 -
整個集合框架分爲兩個門派(類型): Collection
和Map
,前者是一個容器,存儲一系列的對象;後者是鍵值對<key, value>
,存儲一系列的鍵值對 -
在集合框架體系下,衍生出四種具體的集合類型: Map
、Set
、List
、Queue
-
Map
存儲<key,value>
鍵值對,查找元素時經過key
查找value
-
Set
內部存儲一系列不可重複的對象,且是一個無序集合,對象排列順序不一 -
List
內部存儲一系列可重複的對象,是一個有序集合,對象按插入順序排列 -
Queue
是一個隊列容器,其特性與List
相同,但只能從隊頭
和隊尾
操做元素 -
JDK 爲集合的各類操做提供了兩個工具類 Collections
和Arrays
,以後會講解工具類的經常使用方法 -
四種抽象集合類型內部也會衍生出許多具備不一樣特性的集合類,不一樣場景下擇優使用,沒有最佳的集合
上面瞭解了整個集合框架體系的組成部分,接下來的章節會嚴格按照上面羅列的順序進行講解,每一步都會有承上啓下
的做用微信
學習
Set
前,最好最好要先學習Map
,由於Set
的操做本質上是對Map
的操做,往下看準沒錯網絡
Iterator Iterable ListIterator
在第一次看這兩個接口,真覺得是如出一轍的,沒發現裏面有啥不一樣,存在即合理,它們兩個仍是有本質上的區別的。
首先來看Iterator
接口:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
提供的API接口含義以下:
-
hasNext()
:判斷集合中是否存在下一個對象 -
next()
:返回集合中的下一個對象,並將訪問指針移動一位 -
remove()
:刪除集合中調用next()
方法返回的對象
在早期,遍歷集合的方式只有一種,經過Iterator
迭代器操做
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator iter = list.iterator();
while (iter.hasNext()) {
Integer next = iter.next();
System.out.println(next);
if (next == 2) { iter.remove(); }
}
再來看Iterable
接口:
public interface Iterable<T> {
Iterator<T> iterator();
// JDK 1.8
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
能夠看到Iterable
接口裏面提供了Iterator
接口,因此實現了Iterable
接口的集合依舊可使用迭代器
遍歷和操做集合中的對象;
而在 JDK 1.8
中,Iterable
提供了一個新的方法forEach()
,它容許使用加強 for 循環遍歷對象。
List<Integer> list = new ArrayList<>();
for (Integer num : list) {
System.out.println(num);
}
咱們經過命令:javap -c
反編譯上面的這段代碼後,發現它只是 Java 中的一個語法糖
,本質上仍是調用Iterator
去遍歷。

翻譯成代碼,就和一開始的Iterator
迭代器遍歷方式基本相同了。
Iterator iter = list.iterator();
while (iter.hasNext()) {
Integer num = iter.next();
System.out.println(num);
}
還有更深層次的探討:爲何要設計兩個接口
Iterable
和Iterator
,而不是保留其中一個就能夠了。簡單講解:
Iterator
的保留可讓子類去實現本身的迭代器,而Iterable
接口更加關注於for-each
的加強語法。具體可參考:Java中的Iterable與Iterator詳解
關於Iterator
和Iterable
的講解告一段落,下面來總結一下它們的重點:
-
Iterator
是提供集合操做內部對象的一個迭代器,它能夠遍歷、移除對象,且只可以單向移動 -
Iterable
是對Iterator
的封裝,在JDK 1.8
時,實現了Iterable
接口的集合可使用加強 for 循環遍歷集合對象,咱們經過反編譯後發現底層仍是使用Iterator
迭代器進行遍歷
等等,這一章還沒完,還有一個ListIterator
。它繼承 Iterator 接口,在遍歷List
集合時能夠從任意索引下標開始遍歷,並且支持雙向遍歷。
ListIterator 存在於 List 集合之中,經過調用方法能夠返回起始下標爲 index
的迭代器
List<Integer> list = new ArrayList<>();
// 返回下標爲0的迭代器
ListIterator<Integer> listIter1 = list.listIterator();
// 返回下標爲5的迭代器
ListIterator<Integer> listIter2 = list.listIterator(5);
ListIterator 中有幾個重要方法,大多數方法與 Iterator 中定義的含義相同,可是比 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 接口是集合框架體系的兩大門派,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)
-
刪除方法: remove(Object var1)
/removeAll(Collection<?> var1)
-
查找方法: contains(Object var1)
/containsAll(Collection<?> var1);
-
查詢集合自身信息: size()
/isEmpty()
-
···
在Collection
接口下,一樣會將集合細分爲不一樣的種類:
-
Set
接口:一個不容許存儲重複元素的無序集合,具體實現有HashSet
/TreeSet
··· -
List
接口:一個可存儲重複元素的有序集合,具體實現有ArrayList
/LinkedList
··· -
Queue
接口:一個可存儲重複元素的隊列,具體實現有PriorityQueue
/ArrayDeque
···

2
Map 集合體系詳解
Map
接口是由<key, value>
組成的集合,由key
映射到惟一的value
,因此Map
不能包含重複的key
,每一個鍵至多映射一個值。下圖是整個 Map 集合體系的主要組成部分,我將會按照平常使用頻率從高到低一一講解。
不得不提的是 Map 的設計理念:定位元素的時間複雜度優化到 O(1)
Map 體系下主要分爲 AbstractMap 和 SortedMap兩類集合
AbstractMap
是對 Map 接口的擴展,它定義了普通的 Map 集合具備的通用行爲,能夠避免子類重複編寫大量相同的代碼,子類繼承 AbstractMap 後能夠重寫它的方法,實現額外的邏輯,對外提供更多的功能。
SortedMap
定義了該類 Map 具備 排序
行爲,同時它在內部定義好有關排序的抽象方法,當子類實現它時,必須重寫全部方法,對外提供排序功能。
HashMap
HashMap 是一個最通用的利用哈希表存儲元素的集合,將元素放入 HashMap 時,將key
的哈希值轉換爲數組的索引
下標肯定存放位置,查找時,根據key
的哈希地址轉換成數組的索引
下標肯定查找位置。
HashMap 底層是用數組 + 鏈表 + 紅黑樹這三種數據結構實現,它是非線程安全的集合。

發送哈希衝突時,HashMap 的解決方法是將相同映射地址的元素連成一條鏈表
,若是鏈表的長度大於8
時,且數組的長度大於64
則會轉換成紅黑樹
數據結構。
關於 HashMap 的簡要總結:
-
它是集合中最經常使用的 Map
集合類型,底層由數組 + 鏈表 + 紅黑樹
組成 -
HashMap不是線程安全的 -
插入元素時,經過計算元素的 哈希值
,經過哈希映射函數轉換爲數組下標
;查找元素時,一樣經過哈希映射函數獲得數組下標定位元素的位置
LinkedHashMap
LinkedHashMap 能夠看做是 HashMap
和 LinkedList
的結合:它在 HashMap 的基礎上添加了一條雙向鏈表,默認
存儲各個元素的插入順序,但因爲這條雙向鏈表,使得 LinkedHashMap 能夠實現 LRU
緩存淘汰策略,由於咱們能夠設置這條雙向鏈表按照元素的訪問次序
進行排序

LinkedHashMap 是 HashMap 的子類,因此它具有 HashMap 的全部特色,其次,它在 HashMap 的基礎上維護了一條雙向鏈表
,該鏈表存儲了全部元素,默認
元素的順序與插入順序一致。若accessOrder
屬性爲true
,則遍歷順序按元素的訪問次序進行排序。
// 頭節點
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()
方法中能夠看到下面這段代碼,其做用是挪動結點的位置:
if (this.accessOrder) {
this.afterNodeAccess(e);
}
只要調用了get()
且accessOrder = true
,則會將該節點更新到鏈表尾部
,具體的邏輯在afterNodeAccess()
中,感興趣的可翻看源碼,篇幅緣由這裏再也不展開。
如今若是要實現一個LRU
緩存策略,則須要作兩件事情:
-
指定 accessOrder = true
能夠設定鏈表按照訪問順序排列,經過提供的構造器能夠設定accessOrder
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
-
重寫 removeEldestEntry()
方法,內部定義邏輯,一般是判斷容量
是否達到上限,如果則執行淘汰。
這裏就要貼出一道大廠面試必考題目:146. LRU緩存機制,只要跟着個人步驟,就能順利完成這道大廠題了。
關於 LinkedHashMap 主要介紹兩點:
-
它底層維護了一條 雙向鏈表
,由於繼承了 HashMap,因此它也不是線程安全的 -
LinkedHashMap 可實現 LRU
緩存淘汰策略,其原理是經過設置accessOrder
爲true
並重寫removeEldestEntry
方法定義淘汰元素時需知足的條件
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));
TreeMap 底層使用了數組+紅黑樹實現,因此裏面的存儲結構能夠理解成下面這幅圖哦。

圖中紅黑樹的每個節點都是一個Entry
,在這裏爲了圖片的簡潔性,就不標明 key 和 value 了,注意這些元素都是已經按照key
排好序了,整個數據結構都是保持着有序
的狀態!
關於天然
排序與定製
排序:
-
天然排序:要求 key
必須實現Comparable
接口。
因爲Integer
類實現了 Comparable 接口,按照天然排序規則是按照key
從小到大排序。
TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(2, "TWO");
treeMap.put(1, "ONE");
System.out.print(treeMap);
// {1=ONE, 2=TWO}
-
定製排序:在初始化 TreeMap 時傳入新的 Comparator
,不要求key
實現 Comparable 接口
TreeMap<Integer, String> treeMap = new TreeMap<>((o1, o2) -> Integer.compare(o2, o1));
treeMap.put(1, "ONE");
treeMap.put(2, "TWO");
treeMap.put(4, "FOUR");
treeMap.put(3, "THREE");
System.out.println(treeMap);
// {4=FOUR, 3=THREE, 2=TWO, 1=ONE}
經過傳入新的Comparator
比較器,能夠覆蓋默認的排序規則,上面的代碼按照key
降序排序,在實際應用中還能夠按照其它規則自定義排序。
compare()
方法的返回值有三種,分別是:0
,-1
,+1
(1)若是返回0
,表明兩個元素相等,不須要調換順序
(2)若是返回+1
,表明前面的元素須要與後面的元素調換位置
(3)若是返回-1
,表明前面的元素不須要與後面的元素調換位置
而什麼時候返回+1
和-1
,則由咱們本身去定義,JDK默認是按照天然排序,而咱們能夠根據key
的不一樣去定義降序仍是升序排序。
關於 TreeMap 主要介紹了兩點:
-
它底層是由 紅黑樹
這種數據結構實現的,因此操做的時間複雜度恆爲O(logN)
-
TreeMap 能夠對 key
進行天然排序或者自定義排序,自定義排序時須要傳入Comparator
,而天然排序要求key
實現了Comparable
接口 -
TreeMap 不是線程安全的。
WeakHashMap
WeakHashMap 平常開發中比較少見,它是基於普通的Map
實現的,而裏面Entry
中的鍵在每一次的垃圾回收
都會被清除掉,因此很是適合用於短暫訪問、僅訪問一次的元素,緩存在WeakHashMap
中,並儘早地把它回收掉。
當Entry
被GC
時,WeakHashMap 是如何感知到某個元素被回收的呢?
在 WeakHashMap 內部維護了一個引用隊列queue
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
這個 queue 裏包含了全部被GC
掉的鍵,當JVM開啓GC
後,若是回收掉 WeakHashMap 中的 key,會將 key 放入queue 中,在expungeStaleEntries()
中遍歷 queue,把 queue 中的全部key
拿出來,並在 WeakHashMap 中刪除掉,以達到同步。
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
// 去 WeakHashMap 中刪除該鍵值對
}
}
}
再者,須要注意 WeakHashMap 底層存儲的元素的數據結構是數組 + 鏈表
,沒有紅黑樹哦,能夠換一個角度想,若是還有紅黑樹,那乾脆直接繼承 HashMap ,而後再擴展就完事了嘛,然而它並無這樣作:
public class WeakHashMap<K, V> extends AbstractMap<K, V> implements Map<K, V> {
}
因此,WeakHashMap 的數據結構圖我也爲你準備好啦。

圖中被虛線標識的元素將會在下一次訪問 WeakHashMap 時被刪除掉,WeakHashMap 內部會作好一系列的調整工做,因此記住隊列的做用就是標誌那些已經被GC
回收掉的元素。
關於 WeakHashMap 須要注意兩點:
-
它的鍵是一種弱鍵,放入 WeakHashMap 時,隨時會被回收掉,因此不能確保某次訪問元素必定存在 -
它依賴普通的 Map
進行實現,是一個非線程安全的集合 -
WeakHashMap 一般做爲緩存使用,適合存儲那些只需訪問一次、或只需保存短暫時間的鍵值對
Hashtable
Hashtable 底層的存儲結構是數組 + 鏈表
,而它是一個線程安全的集合,可是由於這個線程安全,它就被淘汰掉了。
下面是Hashtable存儲元素時的數據結構圖,它只會存在數組+鏈表,當鏈表過長時,查詢的效率太低,並且會長時間鎖住 Hashtable。

這幅圖是否有點眼熟哈哈哈哈,本質上就是 WeakHashMap 的底層存儲結構了。你千萬別問爲何 WeakHashMap 不繼承 Hashtable 哦,Hashtable 的
性能
在併發環境下很是差,在非併發環境下能夠用HashMap
更優。
HashTable 本質上是 HashMap 的前輩,它被淘汰的緣由也主要由於兩個字:性能
HashTable 是一個 線程安全 的 Map,它全部的方法都被加上了 synchronized 關鍵字,也是由於這個關鍵字,它註定成爲了時代的棄兒。
HashTable 底層採用 數組+鏈表 存儲鍵值對,因爲被棄用,後人也沒有對它進行任何改進
HashTable 默認長度爲 11
,負載因子爲 0.75F
,即元素個數達到數組長度的 75% 時,會進行一次擴容,每次擴容爲原來數組長度的 2
倍
HashTable 全部的操做都是線程安全的。

3
Collection 集合體系詳解
Collection 集合體系的頂層接口就是Collection
,它規定了該集合下的一系列行爲約定。
該集合下能夠分爲三大類集合:List,Set和Queue
Set
接口定義了該類集合不容許存儲重複的元素,且任何操做時均須要經過哈希函數映射到集合內部定位元素,集合內部的元素默認是無序的。
List
接口定義了該類集合容許存儲重複的元素,且集合內部的元素按照元素插入的順序有序排列,能夠經過索引訪問元素。
Queue
接口定義了該類集合是以隊列
做爲存儲結構,因此集合內部的元素有序排列,僅能夠操做頭結點元素,沒法訪問隊列中間的元素。
上面三個接口是最普通,最抽象的實現,而在各個集合接口內部,還會有更加具體的表現,衍生出各類不一樣的額外功能,使開發者可以對比各個集合的優點,擇優使用。

Set 接口
Set
接口繼承了Collection
接口,是一個不包括重複元素的集合,更確切地說,Set 中任意兩個元素不會出現 o1.equals(o2)
,並且 Set 至多只能存儲一個 NULL
值元素,Set 集合的組成部分能夠用下面這張圖歸納:

在 Set 集合體系中,咱們須要着重關注兩點:
-
存入可變元素時,必須很是當心,由於任意時候元素狀態的改變都有可能使得 Set 內部出現兩個相等的元素,即
o1.equals(o2) = true
,因此通常不要更改存入 Set 中的元素,不然將會破壞了equals()
的做用! -
Set 的最大做用就是判重,在項目中最大的做用也是判重!
接下來咱們去看它的實現類和子類: AbstractSet
和 SortedSet
AbstractSet 抽象類
AbstractSet
是一個實現 Set 的一個抽象類,定義在這裏能夠將全部具體 Set 集合的相同行爲在這裏實現,避免子類包含大量的重複代碼
全部的 Set 也應該要有相同的 hashCode()
和 equals()
方法,因此使用抽象類把該方法重寫後,子類無需關心這兩個方法。
public abstract class AbstractSet<E> implements Set<E> {
// 判斷兩個 set 是否相等
public boolean equals(Object o) {
if (o == this) { // 集合自己
return true;
} else if (!(o instanceof Set)) { // 集合不是 set
return false;
} else {
// 比較兩個集合的元素是否所有相同
}
}
// 計算全部元素的 hashcode 總和
public int hashCode() {
int h = 0;
Iterator i = this.iterator();
while(i.hasNext()) {
E obj = i.next();
if (obj != null) {
h += obj.hashCode();
}
}
return h;
}
}
SortedSet 接口
SortedSet
是一個接口,它在 Set 的基礎上擴展了排序的行爲,因此全部實現它的子類都會擁有排序功能。
public interface SortedSet<E> extends Set<E> {
// 元素的比較器,決定元素的排列順序
Comparator<? super E> comparator();
// 獲取 [var1, var2] 之間的 set
SortedSet<E> subSet(E var1, E var2);
// 獲取以 var1 開頭的 Set
SortedSet<E> headSet(E var1);
// 獲取以 var1 結尾的 Set
SortedSet<E> tailSet(E var1);
// 獲取首個元素
E first();
// 獲取最後一個元素
E last();
}
HashSet
HashSet 底層藉助 HashMap
實現,咱們能夠觀察它的多個構造方法,本質上都是 new 一個 HashMap
這也是這篇文章爲何先講解 Map 再講解 Set 的緣由!先學習 Map,有助於理解 Set
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;
}
咱們看到 PRESENT
就是一個靜態常量:使用 PRESENT 做爲 HashMap 的 value 值,使用HashSet的開發者只需關注於須要插入的 key
,屏蔽了 HashMap 的 value

上圖能夠觀察到每一個Entry
的value
都是 PRESENT 空對象,咱們就不用再理會它了。
HashSet 在 HashMap 基礎上實現,因此不少地方能夠聯繫到 HashMap:
-
底層數據結構:HashSet 也是採用 數組 + 鏈表 + 紅黑樹
實現 -
線程安全性:因爲採用 HashMap 實現,而 HashMap 自己線程不安全,在HashSet 中沒有添加額外的同步策略,因此 HashSet 也線程不安全 -
存入 HashSet 的對象的狀態最好不要發生變化,由於有可能改變狀態後,在集合內部出現兩個元素 o1.equals(o2)
,破壞了equals()
的語義。
LinkedHashSet
LinkedHashSet 的代碼少的可憐,不信我給你我粘出來

少歸少,仍是不能鬧,LinkedHashSet
繼承了HashSet
,咱們跟隨到父類 HashSet 的構造方法看看
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
this.map = new LinkedHashMap(initialCapacity, loadFactor);
}
發現父類中 map 的實現採用LinkedHashMap
,這裏注意不是HashMap
,而 LinkedHashMap 底層又採用 HashMap + 雙向鏈表 實現的,因此本質上 LinkedHashSet 仍是使用 HashMap 實現的。
LinkedHashSet -> LinkedHashMap -> HashMap + 雙向鏈表

而 LinkedHashMap 是採用 HashMap
和雙向鏈表
實現的,這條雙向鏈表中保存了元素的插入順序。因此 LinkedHashSet 能夠按照元素的插入順序遍歷元素,若是你熟悉LinkedHashMap
,那 LinkedHashSet 也就更不在話下了。
關於 LinkedHashSet 須要注意幾個地方:
-
它繼承了 HashSet
,而 HashSet 默認是採用 HashMap 存儲數據的,可是 LinkedHashSet 調用父類構造方法初始化 map 時是 LinkedHashMap 而不是 HashMap,這個要額外注意一下 -
因爲 LinkedHashMap 不是線程安全的,且在 LinkedHashSet 中沒有添加額外的同步策略,因此 LinkedHashSet 集合也不是線程安全的
TreeSet
TreeSet 是基於 TreeMap 的實現,因此存儲的元素是有序的,底層的數據結構是數組 + 紅黑樹
。

而元素的排列順序有2
種,和 TreeMap 相同:天然排序和定製排序,經常使用的構造方法已經在下面展現出來了,TreeSet 默認按照天然排序,若是須要定製排序,須要傳入Comparator
。
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
TreeSet 應用場景有不少,像在遊戲裏的玩家戰鬥力排行榜
public class Player implements Comparable<Integer> {
public String name;
public int score;
@Override
public int compareTo(Student o) {
return Integer.compareTo(this.score, o.score);
}
}
public static void main(String[] args) {
Player s1 = new Player("張三", 100);
Player s2 = new Player("李四", 90);
Player s3 = new Player("王五", 80);
TreeSet<Player> set = new TreeSet();
set.add(s2); set.add(s1); set.add(s3);
System.out.println(set);
}
// [Student{name='王五', score=80}, Student{name='李四', score=90}, Student{name='張三', score=100}]
對 TreeSet 介紹了它的主要實現方式和應用場景,有幾個值得注意的點。
-
TreeSet 的全部操做都會轉換爲對 TreeMap 的操做,TreeMap 採用紅黑樹實現,任意操做的平均時間複雜度爲 O(logN)
-
TreeSet 是一個線程不安全的集合 -
TreeSet 常應用於對不重複的元素定製排序,例如玩家戰力排行榜
注意:TreeSet判斷元素是否重複的方法是判斷compareTo()方法是否返回0,而不是調用 hashcode() 和 equals() 方法,若是返回 0 則認爲集合內已經存在相同的元素,不會再加入到集合當中。

4
List 接口
List 接口和 Set 接口齊頭並進,是咱們平常開發中接觸的不少的一種集合類型了。整個 List 集合的組成部分以下圖
List
接口直接繼承 Collection 接口,它定義爲能夠存儲重複元素的集合,而且元素按照插入順序有序排列,且能夠經過索引訪問指定位置的元素。常見的實現有:ArrayList、LinkedList、Vector和Stack
AbstractList 和 AbstractSequentialList
AbstractList 抽象類實現了 List 接口,其內部實現了全部的 List 都需具有的功能,子類能夠專一於實現本身具體的操做邏輯。
// 查找元素 o 第一次出現的索引位置
public int indexOf(Object o)
// 查找元素 o 最後一次出現的索引位置
public int lastIndexOf(Object o)
//···
AbstractSequentialList 抽象類繼承了 AbstractList,在原基礎上限制了訪問元素的順序只可以按照順序訪問,而不支持隨機訪問,若是須要知足隨機訪問的特性,則繼承 AbstractList。子類 LinkedList 使用鏈表實現,因此僅能支持順序訪問,顧繼承了 AbstractSequentialList
而不是 AbstractList。
Vector

在如今已是一種過期的集合了,包括繼承它的
vectorStack
集合也如此,它們被淘汰的緣由都是由於性能低下。
JDK 1.0 時代,ArrayList 還沒誕生,你們都是使用 Vector 集合,但因爲 Vector 的每一個操做都被 synchronized 關鍵字修飾,即便在線程安全的狀況下,仍然進行無心義的加鎖與釋放鎖,形成額外的性能開銷,作了無用功。
public synchronized boolean add(E e);
public synchronized E get(int index);
在 JDK 1.2 時,Collection 家族出現了,它提供了大量高性能、適用於不一樣場合的集合,而 Vector 也是其中一員,但因爲 Vector 在每一個方法上都加了鎖,因爲須要兼允許多老的項目,很難在此基礎上優化Vector
了,因此漸漸地也就被歷史淘汰了。
如今,在線程安全的狀況下,不須要選用 Vector 集合,取而代之的是 ArrayList 集合;在併發環境下,出現了 CopyOnWriteArrayList
,Vector 徹底被棄用了。
Stack

是一種
Stack後入先出(LIFO)
型的集合容器,如圖中所示,大雄
是最後一個進入容器的,top指針指向大雄,那麼彈出元素時,大雄也是第一個被彈出去的。
Stack 繼承了 Vector 類,提供了棧頂的壓入元素操做(push)和彈出元素操做(pop),以及查看棧頂元素的方法(peek)等等,但因爲繼承了 Vector,正所謂跟錯老大沒福報,Stack 也漸漸被淘汰了。
取而代之的是後起之秀 Deque
接口,其實現有 ArrayDeque
,該數據結構更加完善、可靠性更好,依靠隊列也能夠實現LIFO
的棧操做,因此優先選擇 ArrayDeque 實現棧。
Deque<Integer> stack = new ArrayDeque<Integer>();
ArrayDeque 的數據結構是:數組
,並提供頭尾指針下標對數組元素進行操做。本文也會講到哦,客官請繼續往下看,莫着急!:smile:
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

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<>();

5
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()
出現了不一樣的策略
AbstractQueue 抽象類
AbstractQueue 類中提供了各個 API 的基本實現,主要針對各個不一樣的處理策略給出基本的方法實現,定義在這裏的做用是讓子類
根據其方法規範
(操做失敗時拋出異常仍是返回默認值)實現具體的業務邏輯。

LinkedList
LinkedList 在上面已經詳細解釋了,它實現了 Deque
接口,提供了針對頭結點和尾結點的操做,而且每一個結點都有前驅和後繼指針,具有了雙向隊列的全部特性。
ArrayDeque
使用數組實現的雙端隊列,它是無界的雙端隊列,最小的容量是8
(JDK 1.8)。在 JDK 11 看到它默認容量已是 16
了。

在平常使用得很少,值得注意的是它與
ArrayDequeLinkedList
的對比:LinkedList
採用鏈表實現雙端隊列,而 ArrayDeque
使用數組實現雙端隊列。
在文檔中做者寫到:ArrayDeque 做爲棧時比 Stack 性能好,做爲隊列時比 LinkedList 性能好
因爲雙端隊列只能在頭部和尾部操做元素,因此刪除元素和插入元素的時間複雜度大部分都穩定在 O(1)
,除非在擴容時會涉及到元素的批量複製操做。可是在大多數狀況下,使用它時應該指定一個大概的數組長度,避免頻繁的擴容。
我的觀點:鏈表的插入、刪除操做涉及到指針的操做,我我的認爲做者是以爲數組下標的移動要比指針的操做要廉價,並且數組採用連續的內存地址空間,而鏈表元素的內存地址是不連續的,因此數組操做元素的效率在尋址上會比鏈表要快。請批判看待觀點。
PriorityQueue
PriorityQueue 基於優先級堆實現的優先級隊列,而堆是採用數組實現:

文檔中的描述告訴咱們:該數組中的元素經過傳入 Comparator
進行定製排序,若是不傳入Comparator
時,則按照元素自己天然排序
,但要求元素實現了Comparable
接口,因此 PriorityQueue 不容許存儲 NULL 元素。
PriorityQueue 應用場景:元素自己具備優先級,須要按照優先級處理元素
-
例如遊戲中的VIP玩家與普通玩家,VIP 等級越高的玩家越先安排進入服務器玩耍,減小玩家流失。
public static void main(String[] args) {
Student vip1 = new Student("張三", 1);
Student vip3 = new Student("洪七", 2);
Student vip4 = new Student("老八", 4);
Student vip2 = new Student("李四", 1);
Student normal1 = new Student("王五", 0);
Student normal2 = new Student("趙六", 0);
// 根據玩家的 VIP 等級進行降序排序
PriorityQueue<Student> queue = new PriorityQueue<>((o1, o2) -> o2.getScore().compareTo(o1.getScore()));
queue.add(vip1);queue.add(vip4);queue.add(vip3);
queue.add(normal1);queue.add(normal2);queue.add(vip2);
while (!queue.isEmpty()) {
Student s1 = queue.poll();
System.out.println(s1.getName() + "進入遊戲; " + "VIP等級: " + s1.getScore());
}
}
public static class Student implements Comparable<Student> {
private String name;
private Integer score;
public Student(String name, Integer score) {
this.name = name;
this.score = score;
}
@Override
public int compareTo(Student o) {
return this.score.compareTo(o.getScore());
}
}
執行上面的代碼能夠獲得下面這種有趣的結果,能夠看到氪金
令人帶來快樂。

VIP 等級越高(優先級越高)就越優先安排進入遊戲(優先處理),相似這種有優先級的場景還有很是多,各位能夠發揮本身的想象力。
PriorityQueue 總結:
-
PriorityQueue 是基於優先級堆實現的優先級隊列,而堆是用數組維護的
-
PriorityQueue 適用於元素按優先級處理的業務場景,例如用戶在請求人工客服須要排隊時,根據用戶的VIP等級進行
插隊
處理,等級越高,越先安排客服。
章節結束各集合總結:(以 JDK1.8 爲例)
數據類型 | 插入、刪除時間複雜度 | 查詢時間複雜度 | 底層數據結構 | 是否線程安全 |
---|---|---|---|---|
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) | 紅黑樹 | 否 |
LinkedHashSet | 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) | 數組+鏈表 | 是(已淘汰) |
文末總結
這一篇文章對各個集合都有些點到即止
的味道,此文的目的是對整個集合框架有一個較爲總體的瞭解,分析了最經常使用的集合的相關特性,以及某些特殊集合的應用場景例如TreeSet
、TreeMap
這種可定製排序的集合。
-
Collection
接口提供了整個集合框架最通用的增刪改查以及集合自身操做的抽象方法,讓子類去實現 -
Set
接口決定了它的子類都是無序、無重複元素的集合,其主要實現有HashSet、TreeSet、LinkedHashSet。 -
HashSet
底層採用HashMap
實現,而TreeSet
底層使用TreeMap
實現,大部分 Set 集合的操做都會轉換爲 Map 的操做,TreeSet 能夠將元素按照規則進行排序。 -
List
接口決定了它的子類都是有序、可存儲重複元素的集合,常見的實現有 ArrayList,LinkedList,Vector -
ArrayList
使用數組實現,而 LinkedList 使用鏈表實現,因此它們兩個的使用場景幾乎是相反的,頻繁查詢的場景使用 ArrayList,而頻繁插入刪除的場景最好使用 LinkedList -
LinkedList
和ArrayDeque
均可用於雙端隊列,而 Josh Bloch and Doug Lea 認爲ArrayDeque
具備比LinkedList
更好的性能,ArrayDeque
使用數組實現雙端隊列,LinkedList
使用鏈表實現雙端隊列。 -
Queue
接口定義了隊列的基本操做,子類集合都會擁有隊列的特性:先進先出,主要實現有:LinkedList,ArrayDeque -
PriorityQueue
底層使用二叉堆維護的優先級隊列,而二叉堆是由數組實現的,它能夠按照元素的優先級進行排序,優先級越高的元素,排在隊列前面,優先被彈出處理。 -
Map
接口定義了該種集合類型是以<key,value>
鍵值對形式保存,其主要實現有:HashMap,TreeMap,LinkedHashMap,Hashtable -
LinkedHashMap 底層多加了一條雙向鏈表,設置 accessOrder
爲true
並重寫方法則能夠實現LRU
緩存 -
TreeMap 底層採用數組+紅黑樹實現,集合內的元素默認按照天然排序,也能夠傳入 Comparator
定製排序
看到這裏很是不容易,感謝你願意閱讀個人文章,但願能對你有所幫助,你能夠參考着文末總結的順序,每當我提到一個集合時,回想它的重要知識點是什麼,主要就是底層數據結構
,線程安全性
,該集合的一兩個特有性質
,只要可以回答出來個大概,我相信以後運用這些數據結構,你可以熟能生巧。
本文對整個集合體系的全部經常使用的集合類都分析了,這裏並無對集合內部的實現深刻剖析,我想先從最宏觀的角度讓你們瞭解每一個集合的的做用,應用場景,以及簡單的對比,以後會抽時間對常見的集合進行源碼剖析,盡情期待,感謝閱讀!
最後有些話想說:這篇文章花了我半個月去寫,也是意義重大,多謝
cxuan
哥一直指導我寫文章,一步一步地去打磨出一篇好的文章真的很是不容易,寫下的每個字都可以讓別人看得懂是一件很是難的事情,總結出最精華的知識分享給大家也是很是難的一件事情,希望可以一直進步下去!不忘初心,熱愛分享,喜好寫做
往期精彩回顧

本文分享自微信公衆號 - Java建設者(javajianshe)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。