Java集合類總結

前言

以前一直作C++開發,在使用標準集合類的類庫時都是使用的STL,覺的這個就是比C語言很是大的進步,很好用;後來玩Java,發現Java中的集合類更是好用,可是因爲Java語言的發展緣由,在使用的過程當中也有不少坑,有不少的細節須要去處理。最近在進行組內代碼評審時,就發現開發人員亂用集合類的狀況。不少開發人員就不明白各個集合類的特性和使用場景,反正列表就用ArrayList,鍵值就用HashMap,彷彿在他們眼中Java的集合類就只有ArrayListHashMap這兩種。不怕你們笑話,曾經我也是這麼使用的,今天就用一點時間,好好的對Java集合類的使用進行一次掃盲。html

Java集合概述

Java提供的衆多集合類由兩大接口衍生而來:Collection接口和Map接口。爲了更好的把握Java集合類的總體結構,我這裏先貼一個Java集合的總體類圖,以便你們對Java集合類有一個總體的印象。java

乍一看這個圖很複雜,其實咱們仔細梳理一下,這個圖仍是很是清晰的。能夠這麼看,在Java的集合類中,主要分爲ListMapSetQueue這四大類,這四大接口類下面,又根據使用場景分爲多個具體的子類。下面就一一進行總結。算法

Collection接口說明

從類圖上能夠看到,Collection接口做爲一個很是重要的基礎接口,因此咱們有必要對Collection接口中的經常使用方法進行一下說明和總結:數組

  • add:向集合中添加單個元素
  • addAll:向集合中批量添加元素
  • clear:刪除集合中全部元素
  • contains:判斷集合是否包含某個元素
  • isEmpty:判斷集合是否爲空
  • iterator:返回一個集合迭代器;關於迭代器能夠參考這篇《Java中的Enumeration、Iterable和Iterator接口詳解
  • remove:從集合中刪除單個元素
  • removeAll:從集合中批量刪除元素
  • retainAll:保留指定入參集合中的元素,刪除其它元素
  • size:獲取集合中元素個數
  • toArray:將集合轉換爲數組

Map接口說明

一樣的,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類的集合出鏡頻率很是高,對於List類的集合,咱們須要知道經常使用的有ArrayListLinkedListVectorCopyOnWriteArrayList,特別是ArrayListCopyOnWriteArrayList這兩貨,更是頻繁出鏡。併發

  • 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等)時,都會將原數組拷貝一份,而後在新數組上作修改操做。最後集合的引用指向新數組。CopyOnWriteArrayListVector都是線程安全的,不一樣的是:前者使用ReentrantLock類,後者使用synchronized關鍵字。ReentrantLock提供了更多的鎖投票機制,在鎖競爭的狀況下能表現更佳的性能。就是它讓JVM能更快的調度線程,纔有更多的時間去執行線程。這就是爲何CopyOnWriteArrayList的性能在大併發量的狀況下優於Vector的緣由。

    對於CopyOnWriteArrayList來講,很是適合高併發的讀操做(讀多寫少)的場景下使用。若寫的操做很是多,會頻繁複制容器,從而影響性能。

Map

Map存儲的是鍵值對,它將key和value封裝至一個叫作Entry的對象中。每個Map根據其自身的特色,都有不一樣的Entry實現,以對應Map的內部類形式出現。

根據我如今的開發狀況來看,MapList類的集合更經常使用。對於Map類的集合有HashMapHashTableSortedMapTreeMapWeakHashMapConcurrentSkipListMap

  • HashMap
    HashMap的底層是基於數組+鏈表+紅黑樹(JDK1.8+)的方式實現的。HashMapEntry對象存儲在一個數組中,並經過哈希表來實現對Entry的快速訪問。感受這裏不放一張圖,就不能更好的理解HashMap的實現方式了:

    經過上圖你們應該有一個總體的理解,我這裏也不會對HashMap的實現原理進行更進一步的剖析。若是對HashMap的實現源碼感興趣,能夠閱讀《一文讓你完全理解 Java HashMap 和 ConcurrentHashMap》和《Java集合,HashMap底層實現和原理(1.7數組+鏈表與1.8+的數組+鏈表+紅黑樹)》這兩篇文章。對於HashMap的一些特性這裏進行列舉:
    • 當儲存對象時,咱們將鍵值對傳遞給put(key,value)方法時,它調用鍵對象key的hashCode()方法來計算hashcode,而後找到bucket位置,來儲存值對象value
    • hash表裏能夠存儲元素的位置稱爲桶(bucket),若是經過key計算hash值發生衝突時,那麼將採用鏈表的形式,來存儲元素
    • HashMap的擴容操做是一項很耗時的任務,因此若是能估算Map的容量,最好給它一個默認初始值,避免進行屢次擴容;當數量達到了16 * 0.75 = 12就須要將當前16的容量進行擴容,而擴容這個過程涉及到 rehash、複製數據等操做,因此很是消耗性能
    • 容許使用null建和null
    • 非線程安全
  • HashTable
    HashTableHashMap的線程安全版,Hashtable的實現方法裏面都添加了synchronized關鍵字來確保線程同步。對於HashTable這種上古的東西,在開發中不建議使用了,由於如今已經提供了ConcurrentHashMap來使用。

  • ConcurrentHashMap
    ConcurrentHashMapHashMap的線程安全版(自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用於存儲不含重複元素的集合,幾乎全部的Set實現都是基於同類型Map的。簡單地說,Set是閹割版的Map。每個Set內都有一個同類型的Map實例(CopyOnWriteArraySet除外,它內置的是CopyOnWriteArrayList實例),Set把元素做爲key存儲在本身的Map實例中,value則是一個空的Object。Set的經常使用實現包括HashSetTreeSetConcurrentSkipListSet,因爲實現原理和對應的Map是徹底一致的,因此這裏就再也不贅述。

在實際評審代碼中,發現開發人員不多用Set類型的集合,即便有存儲不含重複元素的場景,也都是使用ArrayList集合,而後結合着contains這種奇葩方式來實現。也就是說,一些基本功不紮實的開發人員,在腦海中就沒有Set集合的概念。抱着實現功能就OK的心態,管他代碼質量好很差,全憑ArrayListHashMap闖天下。

Queue

Queue用於模擬「隊列」這種數據結構(先進先出FIFO)。隊列的頭部保存着隊列中存放時間最長的元素,隊列的尾部保存着隊列中存放時間最短的元素。新元素插入到隊列的尾部。這種隊列基本都只是在小數據量的狀況下使用,對於互聯網應用來講,基本都是在使用分佈式消息隊列中間件。從文章開頭的類圖中能夠看出,Deque接口繼承了Queue接口,Deque接口表明一個「雙端隊列」,雙端隊列能夠同時從兩端來添加、刪除元素,所以Deque的實現類既能夠當成隊列使用、也能夠當成棧使用。對於咱們來講,經常使用的Queue實現類有ArrayDequeConcurrentLinkedQueueLinkedBlockingQueueArrayBlockingQueueSynchronousQueuePriorityQueuePriorityBlockingQueue

  • 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(前文已經總結過)、ConcurrentLinkedDequeLinkedBlockingDeque,其實現機制與上面所述的ConcurrentLinkedQueueLinkedBlockingQueue很是相似,此處再也不贅述。

總結

這裏對Java中的一些經常使用集合類進行了大概原理性的總結,並無深刻到源碼級別,若是深刻到源碼級別,那就夠講一本書的了,並且花費的精力和時間也太大了,這裏就是淺嘗輒止,有個基本的瞭解便可。瞭解原理,對本身寫的代碼負責。

2019年8月11日 於內蒙古呼和浩特。

相關文章
相關標籤/搜索