Java集合類總結

前言

之前一直做C++開發,在使用標準集合類的類庫時都是使用的STL,覺的這個就是比C語言非常大的進步,很好用;后來玩Java,發現Java中的集合類更是好用,但是由于Java語言的發展原因,在使用的過程中也有很多坑,有很多的細節需要去處理。最近在進行組內代碼評審時,就發現開發人員亂用集合類的情況。很多開發人員就不明白各個集合類的特性和使用場景,反正列表就用ArrayList,鍵值就用HashMap,仿佛在他們眼中Java的集合類就只有ArrayListHashMap這兩種。不怕大家笑話,曾經我也是這么使用的,今天就用一點時間,好好的對Java集合類的使用進行一次掃盲。

Java集合概述

Java提供的眾多集合類由兩大接口衍生而來:Collection接口和Map接口。為了更好的把握Java集合類的整體結構,我這里先貼一個Java集合的整體類圖,以便大家對Java集合類有一個整體的印象。

乍一看這個圖很復雜,其實我們仔細梳理一下,這個圖還是非常清晰的??梢哉餉純?,在Java的集合類中,主要分為List、Map、SetQueue這四大類,這四大接口類下面,又根據使用場景分為多個具體的子類。下面就一一進行總結。

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類的集合,我們需要知道常用的有ArrayList、LinkedList、Vector、CopyOnWriteArrayList,特別是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類的集合有HashMap、HashTable、SortedMap、TreeMap、WeakHashMapConcurrentSkipListMap。

  • 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的常用實現包括HashSet、TreeSetConcurrentSkipListSet,由于實現原理和對應的Map是完全一致的,所以這里就不再贅述。

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

Queue

Queue用于模擬“隊列”這種數據結構(先進先出FIFO)。隊列的頭部保存著隊列中存放時間最長的元素,隊列的尾部保存著隊列中存放時間最短的元素。新元素插入到隊列的尾部。這種隊列基本都只是在小數據量的情況下使用,對于互聯網應用來說,基本都是在使用分布式消息隊列中間件。從文章開頭的類圖中可以看出,Deque接口繼承了Queue接口,Deque接口代表一個“雙端隊列”,雙端隊列可以同時從兩端來添加、刪除元素,因此Deque的實現類既可以當成隊列使用、也可以當成棧使用。對于我們來說,常用的Queue實現類有ArrayDeque、ConcurrentLinkedQueue、LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue、PriorityQueuePriorityBlockingQueue。

  • 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日 于內蒙古呼和浩特。

posted @ 2019-08-11 12:02 ^_^果凍^_^ 閱讀(...) 評論(...) 編輯 收藏