以前一直作C++開發,在使用標準集合類的類庫時都是使用的STL,覺的這個就是比C語言很是大的進步,很好用;後來玩Java,發現Java中的集合類更是好用,可是因爲Java語言的發展緣由,在使用的過程當中也有不少坑,有不少的細節須要去處理。最近在進行組內代碼評審時,就發現開發人員亂用集合類的狀況。不少開發人員就不明白各個集合類的特性和使用場景,反正列表就用ArrayList
,鍵值就用HashMap
,彷彿在他們眼中Java的集合類就只有ArrayList
和HashMap
這兩種。不怕你們笑話,曾經我也是這麼使用的,今天就用一點時間,好好的對Java集合類的使用進行一次掃盲。html
Java提供的衆多集合類由兩大接口衍生而來:Collection
接口和Map
接口。爲了更好的把握Java集合類的總體結構,我這裏先貼一個Java集合的總體類圖,以便你們對Java集合類有一個總體的印象。java
乍一看這個圖很複雜,其實咱們仔細梳理一下,這個圖仍是很是清晰的。能夠這麼看,在Java的集合類中,主要分爲List
、Map
、Set
和Queue
這四大類,這四大接口類下面,又根據使用場景分爲多個具體的子類。下面就一一進行總結。算法
從類圖上能夠看到,Collection
接口做爲一個很是重要的基礎接口,因此咱們有必要對Collection
接口中的經常使用方法進行一下說明和總結:數組
add
:向集合中添加單個元素addAll
:向集合中批量添加元素clear
:刪除集合中全部元素contains
:判斷集合是否包含某個元素isEmpty
:判斷集合是否爲空iterator
:返回一個集合迭代器;關於迭代器能夠參考這篇《Java中的Enumeration、Iterable和Iterator接口詳解》remove
:從集合中刪除單個元素removeAll
:從集合中批量刪除元素retainAll
:保留指定入參集合中的元素,刪除其它元素size
:獲取集合中元素個數toArray
:將集合轉換爲數組一樣的,Map
接口做爲很是重要的接口,也有必要對其中的一些重要方法進行一些說明:安全
clear
:刪除全部元素containsKey
:判斷是否包含某個鍵containsValue
:判斷是否包含某個值entrySet
:將Map鍵值對以Map.Entry的形式放入Set集合中返回get
:返回key值所對應的對象isEmpty
:判斷是否爲空keySet
:返回全部鍵的Set集合,這裏有一篇文章《JAVA中Map使用keySet()和entrySet()進行遍歷效率的對比》能夠看一看put
:向Map中添加單個元素putAll
:向Map中批量添加元素remove
:刪除Key所對應的對象size
:獲取Map中鍵值對的個數values
:返回全部值的集合說完這兩大經常使用接口的經常使用方法,下面就對這兩大接口衍生出來的經常使用集合類進行說明和總結。數據結構
List
用於定義以列表形式存儲的集合,List
接口爲集合中的每一個對象分配了一個索引,用來標記該對象在List中的位置,並能夠經過索引定位到指定位置的對象。多線程
在咱們開發過程當中,List
類的集合出鏡頻率很是高,對於List
類的集合,咱們須要知道經常使用的有ArrayList
、LinkedList
、Vector
、CopyOnWriteArrayList
,特別是ArrayList
和CopyOnWriteArrayList
這兩貨,更是頻繁出鏡。併發
ArrayList
ArrayList
基於數組實現的非線程安全的集合,在內部實現上,其維護了一個可變長度的對象數組,集合內全部對象存儲於這個數組中,並實現該數組長度的動態伸縮。知道了內部的實現原理,那對於ArrayList
來講,就有如下幾個特性:
若是涉及到頻繁的插入和刪除元素,ArrayList
則不是最好的選擇。分佈式
LinkedList
LinkedList
基於鏈表實現的非線程安全的集合,在內部實現上,其實現了靜態類Node,集合中的每一個對象都由一個Node保存,每一個Node都擁有到本身的前一個和後一個Node引用。對於LinkedList
來講,它具有如下特性:高併發
LinkedList
時應用iterator方式,不要用get(int)方式,不然效率會很低Vector
基於數組實現的線程安全的集合。線程同步(方法被synchronized
修飾),性能比ArrayList
差。當併發量增多時,鎖競爭的問題嚴重,會致使性能降低。
CopyOnWriteArrayList
與Vector
同樣,CopyOnWriteArrayList
也能夠認爲是ArrayList
的線程安全版,不一樣之處在於 CopyOnWriteArrayList
在寫操做時會先複製出一個副本,在新副本上執行寫操做,而後再修改引用。這種機制讓CopyOnWriteArrayList
能夠對讀操做不加鎖,這就使CopyOnWriteArrayList
的讀效率遠高於Vector。 CopyOnWriteArrayList
的理念比較相似讀寫分離,適合讀多寫少的多線程場景。但要注意,CopyOnWriteArrayList
只能保證數據的最終一致性,並不能保證數據的實時一致性,若是一個寫操做正在進行中且並未完成,此時的讀操做沒法保證能讀到這個寫操做的結果。
CopyOnWriteArrayList
寫時複製的集合,在執行寫操做(如:add,set,remove等)時,都會將原數組拷貝一份,而後在新數組上作修改操做。最後集合的引用指向新數組。CopyOnWriteArrayList
和Vector
都是線程安全的,不一樣的是:前者使用ReentrantLock
類,後者使用synchronized
關鍵字。ReentrantLock
提供了更多的鎖投票機制,在鎖競爭的狀況下能表現更佳的性能。就是它讓JVM能更快的調度線程,纔有更多的時間去執行線程。這就是爲何CopyOnWriteArrayList
的性能在大併發量的狀況下優於Vector
的緣由。
對於CopyOnWriteArrayList
來講,很是適合高併發的讀操做(讀多寫少)的場景下使用。若寫的操做很是多,會頻繁複制容器,從而影響性能。
Map
存儲的是鍵值對,它將key和value封裝至一個叫作Entry的對象中。每個Map根據其自身的特色,都有不一樣的Entry實現,以對應Map的內部類形式出現。
根據我如今的開發狀況來看,Map
比List
類的集合更經常使用。對於Map
類的集合有HashMap
、HashTable
、SortedMap
、TreeMap
、WeakHashMap
和ConcurrentSkipListMap
。
HashMap
HashMap
的底層是基於數組+鏈表+紅黑樹
(JDK1.8+)的方式實現的。HashMap
將Entry
對象存儲在一個數組中,並經過哈希表來實現對Entry
的快速訪問。感受這裏不放一張圖,就不能更好的理解HashMap
的實現方式了:
HashMap
的實現原理進行更進一步的剖析。若是對HashMap
的實現源碼感興趣,能夠閱讀《一文讓你完全理解 Java HashMap 和 ConcurrentHashMap》和《Java集合,HashMap底層實現和原理(1.7數組+鏈表與1.8+的數組+鏈表+紅黑樹)》這兩篇文章。對於HashMap
的一些特性這裏進行列舉:
null
建和null
值HashTable
HashTable
是HashMap
的線程安全版,Hashtable
的實現方法裏面都添加了synchronized
關鍵字來確保線程同步。對於HashTable
這種上古的東西,在開發中不建議使用了,由於如今已經提供了ConcurrentHashMap
來使用。
ConcurrentHashMap
ConcurrentHashMap
是HashMap
的線程安全版(自JDK1.5引入),提供比Hashtable
更高效的併發性能。
HashTable
在進行讀寫操做時會鎖住整個Entry數組,這就致使數據越多性能越差。而ConcurrentHashMap
使用分離鎖的思路解決併發性能,其將Entry數組拆分至16個Segment中,以哈希算法決定Entry應該存儲在哪一個Segment。這樣就能夠實如今寫操做時只對一個Segment加鎖,大幅提高了併發寫的性能。在進行讀操做時,ConcurrentHashMap
在絕大部分狀況下都不須要加鎖,其Entry中的value是volatile的,這保證了value被修改時的線程可見性,無需加鎖便能實現線程安全的讀操做。
ConcurrentHashMap
採用了分段鎖技術,其中Segment繼承於ReentrantLock。不會像HashTable
那樣不論是put仍是get操做都須要作同步處理,理論上ConcurrentHashMap
支持 CurrencyLevel (Segment數組數量)的線程併發。每當一個線程佔用鎖訪問一個Segment時,不會影響到其餘的Segment。
Set
用於存儲不含重複元素的集合,幾乎全部的Set實現都是基於同類型Map的。簡單地說,Set是閹割版的Map。每個Set內都有一個同類型的Map實例(CopyOnWriteArraySet
除外,它內置的是CopyOnWriteArrayList
實例),Set把元素做爲key存儲在本身的Map實例中,value則是一個空的Object。Set
的經常使用實現包括HashSet
、TreeSet
和ConcurrentSkipListSet
,因爲實現原理和對應的Map是徹底一致的,因此這裏就再也不贅述。
在實際評審代碼中,發現開發人員不多用Set
類型的集合,即便有存儲不含重複元素的場景,也都是使用ArrayList
集合,而後結合着contains
這種奇葩方式來實現。也就是說,一些基本功不紮實的開發人員,在腦海中就沒有Set
集合的概念。抱着實現功能就OK的心態,管他代碼質量好很差,全憑ArrayList
和HashMap
闖天下。
Queue
用於模擬「隊列」這種數據結構(先進先出FIFO)。隊列的頭部保存着隊列中存放時間最長的元素,隊列的尾部保存着隊列中存放時間最短的元素。新元素插入到隊列的尾部。這種隊列基本都只是在小數據量的狀況下使用,對於互聯網應用來講,基本都是在使用分佈式消息隊列中間件。從文章開頭的類圖中能夠看出,Deque
接口繼承了Queue
接口,Deque
接口表明一個「雙端隊列」,雙端隊列能夠同時從兩端來添加、刪除元素,所以Deque
的實現類既能夠當成隊列使用、也能夠當成棧使用。對於咱們來講,經常使用的Queue
實現類有ArrayDeque
、ConcurrentLinkedQueue
、LinkedBlockingQueue
、ArrayBlockingQueue
、SynchronousQueue
、PriorityQueue
和PriorityBlockingQueue
。
ArrayDeque
是一個基於數組的雙端隊列,和ArrayList
相似,它們的底層都採用一個動態的、可重分配的Object[]數組來存儲集合元素,當集合元素超出該數組的容量時,系統會在底層從新分配一個Object[]數組來存儲集合元素。
ConcurrentLinkedQueue
ConcurrentLinkedQueue
是基於鏈表實現的線程安全、無界非阻塞隊列,隊列中每一個Node擁有到下一個Node的引用。它可以保證入隊和出隊操做的原子性和一致性,但在遍歷和size()操做時只能保證數據的弱一致性。
LinkedBlockingQueue
與ConcurrentLinkedQueue
不一樣,LinkedBlocklingQueue
是一種無界的阻塞隊列。所謂阻塞隊列,就是在入隊時若是隊列已滿,線程會被阻塞,直到隊列有空間供入隊再返回;同時在出隊時,若是隊列已空,線程也會被阻塞,直到隊列中有元素供出隊時再返回。LinkedBlocklingQueue
一樣基於鏈表實現,其出隊和入隊操做都會使用ReentrantLock進行加鎖。因此自己是線程安全的,但一樣的,只能保證入隊和出隊操做的原子性和一致性,在遍歷時只能保證數據的弱一致性。
ArrayBlockingQueue
ArrayBlockingQueue
是一種有界的阻塞隊列,基於數組實現。其同步阻塞機制的實現與LinkedBlocklingQueue
基本一致,區別僅在於前者的生產和消費使用同一個鎖,後者的生產和消費使用分離的兩個鎖。
SynchronousQueue
SynchronousQueue
算是JDK實現的隊列中比較奇葩的一個,它不能保存任何元素,size永遠是0,peek()永遠返回null。向其中插入元素的線程會阻塞,直到有另外一個線程將這個元素取走,反之從其中取元素的線程也會阻塞,直到有另外一個線程插入元素。這種實現機制很是適合傳遞性的場景。也就是說若是生產者線程須要及時確認到本身生產的任務已經被消費者線程取走後才能執行後續邏輯的場景下,適合使用SynchronousQueue
。
PriorityQueue
PriorityQueue
是基於最小堆數據結構,能夠在構造時指定Comparator
或者按照天然順序排序。優先隊列有最大優先隊列和最小優先隊列,分別由最大堆和最小堆實現。PriorityQueue
是非阻塞隊列,也不是線程安全的。
PriorityBlockingQueue
PriorityBlockingQueue
實現原理同PriorityQueue
同樣,可是PriorityBlockingQueue
是阻塞隊列,同時也是線程安全的。
Deque
的實現類包括LinkedList
(前文已經總結過)、ConcurrentLinkedDeque
和LinkedBlockingDeque
,其實現機制與上面所述的ConcurrentLinkedQueue
和LinkedBlockingQueue
很是相似,此處再也不贅述。
這裏對Java中的一些經常使用集合類進行了大概原理性的總結,並無深刻到源碼級別,若是深刻到源碼級別,那就夠講一本書的了,並且花費的精力和時間也太大了,這裏就是淺嘗輒止,有個基本的瞭解便可。瞭解原理,對本身寫的代碼負責。
2019年8月11日 於內蒙古呼和浩特。