Java集合的小抄 Java初學者必備

花錢的年華 的部落格發表於2015-07-07

在儘可能短的篇幅裡,將所有集合與併發集合的特徵,實現方式,效能捋一遍。適合所有”精通Java”其實還不那麼自信的人閱讀。

不斷更新中,請儘量訪問部落格原文

List

ArrayList

以陣列實現。節約空間,但陣列有容量限制。超出限制時會增加50%容量,用System.arraycopy()複製到新的陣列,因此最好能給出陣列大小的預估值。預設第一次插入元素時建立大小為10的陣列。

按陣列下標訪問元素–get(i)/set(i,e) 的效能很高,這是陣列的基本優勢。

直接在陣列末尾加入元素–add(e)的效能也高,但如果按下標插入、刪除元素–add(i,e), remove(i), remove(e),則要用System.arraycopy()來移動部分受影響的元素,效能就變差了,這是基本劣勢。

LinkedList

以雙向連結串列實現。連結串列無容量限制,但雙向連結串列本身使用了更多空間,也需要額外的連結串列指標操作。

按下標訪問元素–get(i)/set(i,e) 要悲劇的遍歷連結串列將指標移動到位(如果i>陣列大小的一半,會從末尾移起)。

插入、刪除元素時修改前後節點的指標即可,但還是要遍歷部分連結串列的指標才能移動到下標所指的位置,只有在連結串列兩頭的操作–add(), addFirst(),removeLast()或用iterator()上的remove()能省掉指標的移動。

CopyOnWriteArrayList

併發優化的ArrayList。用CopyOnWrite策略,在修改時先複製一個快照來修改,改完再讓內部指標指向新陣列。

因為對快照的修改對讀操作來說不可見,所以只有寫鎖沒有讀鎖,加上覆制的昂貴成本,典型的適合讀多寫少的場景。如果更新頻率較高,或陣列較大時,還是Collections.synchronizedList(list),對所有操作用同一把鎖來保證執行緒安全更好。

增加了addIfAbsent(e)方法,會遍歷陣列來檢查元素是否已存在,效能可想像的不會太好。

補充

無論哪種實現,按值返回下標–contains(e), indexOf(e), remove(e) 都需遍歷所有元素進行比較,效能可想像的不會太好。

沒有按元素值排序的SortedList,線上程安全類中也沒有無鎖演算法的ConcurrentLinkedList,湊合著用Set與Queue中的等價類時,會缺少一些List特有的方法。

Map

HashMap

以Entry[]陣列實現的雜湊桶陣列,用Key的雜湊值取模桶陣列的大小可得到陣列下標。

插入元素時,如果兩條Key落在同一個桶(比如雜湊值1和17取模16後都屬於第一個雜湊桶),Entry用一個next屬性實現多個Entry以單向連結串列存放,後入桶的Entry將next指向桶當前的Entry。

查詢雜湊值為17的key時,先定位到第一個雜湊桶,然後以連結串列遍歷桶裡所有元素,逐個比較其key值。

當Entry數量達到桶數量的75%時(很多文章說使用的桶數量達到了75%,但看程式碼不是),會成倍擴容桶陣列,並重新分配所有原來的Entry,所以這裡也最好有個預估值。

取模用位運算(hash & (arrayLength-1))會比較快,所以陣列的大小永遠是2的N次方, 你隨便給一個初始值比如17會轉為32。預設第一次放入元素時的初始值是16。

iterator()時順著雜湊桶陣列來遍歷,看起來是個亂序。

在JDK8裡,新增預設為8的閥值,當一個桶裡的Entry超過閥值,就不以單向連結串列而以紅黑樹來存放以加快Key的查詢速度。

LinkedHashMap

擴充套件HashMap增加雙向連結串列的實現,號稱是最佔記憶體的資料結構。支援iterator()時按Entry的插入順序來排序(但是更新不算, 如果設定accessOrder屬性為true,則所有讀寫訪問都算)。

實現上是在Entry上再增加屬性before/after指標,插入時把自己加到Header Entry的前面去。如果所有讀寫訪問都要排序,還要把前後Entry的before/after拼接起來以在連結串列中刪除掉自己。

TreeMap

以紅黑樹實現,篇幅所限詳見入門教程。支援iterator()時按Key值排序,可按實現了Comparable介面的Key的升序排序,或由傳入的Comparator控制。可想象的,在樹上插入/刪除元素的代價一定比HashMap的大。

支援SortedMap介面,如firstKey(),lastKey()取得最大最小的key,或sub(fromKey, toKey), tailMap(fromKey)剪取Map的某一段。

ConcurrentHashMap

併發優化的HashMap,預設16把寫鎖(可以設定更多),有效分散了阻塞的概率,而且沒有讀鎖。
資料結構為Segment[],Segment裡面才是雜湊桶陣列,每個Segment一把鎖。Key先算出它在哪個Segment裡,再算出它在哪個雜湊桶裡。

支援ConcurrentMap介面,如putIfAbsent(key,value)與相反的replace(key,value)與以及實現CAS的replace(key, oldValue, newValue)。

沒有讀鎖是因為put/remove動作是個原子動作(比如put是一個對陣列元素/Entry 指標的賦值操作),讀操作不會看到一個更新動作的中間狀態。

ConcurrentSkipListMap

JDK6新增的併發優化的SortedMap,以SkipList實現。SkipList是紅黑樹的一種簡化替代方案,是個流行的有序集合演算法,篇幅所限見入門教程。Concurrent包選用它是因為它支援基於CAS的無鎖演算法,而紅黑樹則沒有好的無鎖演算法。

很特殊的,它的size()不能隨便調,會遍歷來統計。

補充

關於null,HashMap和LinkedHashMap是隨意的,TreeMap沒有設定Comparator時key不能為null;ConcurrentHashMap在JDK7裡value不能為null(這是為什麼呢?),JDK8裡key與value都不能為null;ConcurrentSkipListMap是所有JDK裡key與value都不能為null。

Set

Set幾乎都是內部用一個Map來實現, 因為Map裡的KeySet就是一個Set,而value是假值,全部使用同一個Object。Set的特徵也繼承了那些內部Map實現的特徵。

  • HashSet:內部是HashMap。
  • LinkedHashSet:內部是LinkedHashMap。
  • TreeSet:內部是TreeMap的SortedSet。
  • ConcurrentSkipListSet:內部是ConcurrentSkipListMap的併發優化的SortedSet。
  • CopyOnWriteArraySet:內部是CopyOnWriteArrayList的併發優化的Set,利用其addIfAbsent()方法實現元素去重,如前所述該方法的效能很一般。

補充:好像少了個ConcurrentHashSet,本來也該有一個內部用ConcurrentHashMap的簡單實現,但JDK偏偏沒提供。Jetty就自己封了一個,Guava則直接用java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 實現。

Queue

Queue是在兩端出入的List,所以也可以用陣列或連結串列來實現。

–普通佇列–

LinkedList

是的,以雙向連結串列實現的LinkedList既是List,也是Queue。它是唯一一個允許放入null的Queue。

ArrayDeque

以迴圈陣列實現的雙向Queue。大小是2的倍數,預設是16。

普通陣列只能快速在末尾新增元素,為了支援FIFO,從陣列頭快速取出元素,就需要使用迴圈陣列:有隊頭隊尾兩個下標:彈出元素時,隊頭下標遞增;加入元素時,如果已到陣列空間的末尾,則將元素迴圈賦值到陣列[0](如果此時隊頭下標大於0,說明隊頭彈出過元素,有空位),同時隊尾下標指向0,再插入下一個元素則賦值到陣列[1],隊尾下標指向1。如果隊尾的下標追上隊頭,說明陣列所有空間已用完,進行雙倍的陣列擴容。

PriorityQueue

用二叉堆實現的優先順序佇列,詳見入門教程,不再是FIFO而是按元素實現的Comparable介面或傳入Comparator的比較結果來出隊,數值越小,優先順序越高,越先出隊。但是注意其iterator()的返回不會排序。

–執行緒安全的佇列–

ConcurrentLinkedQueue/ConcurrentLinkedDeque

無界的併發優化的Queue,基於連結串列,實現了依賴於CAS的無鎖演算法。

ConcurrentLinkedQueue的結構是單向連結串列和head/tail兩個指標,因為入隊時需要修改隊尾元素的next指標,以及修改tail指向新入隊的元素兩個CAS動作無法原子,所以需要的特殊的演算法,篇幅所限見入門教程

PriorityBlockingQueue

無界的併發優化的PriorityQueue,也是基於二叉堆。使用一把公共的讀寫鎖。雖然實現了BlockingQueue介面,其實沒有任何阻塞佇列的特徵,空間不夠時會自動擴容。

DelayQueue

內部包含一個PriorityQueue,同樣是無界的。元素需實現Delayed介面,每次呼叫時需返回當前離觸發時間還有多久,小於0表示該觸發了。
pull()時會用peek()檢視隊頭的元素,檢查是否到達觸發時間。ScheduledThreadPoolExecutor用了類似的結構。

–執行緒安全的阻塞佇列–

BlockingQueue的佇列長度受限,用以保證生產者與消費者的速度不會相差太遠,避免記憶體耗盡。佇列長度設定後不可改變。當入隊時佇列已滿,或出隊時佇列已空,不同函式的效果見下表:

可能報異常 返回布林值 可能阻塞等待 可設定等待時間
入隊 add(e) offer(e) put(e) offer(e, timeout, unit)
出隊 remove() poll() take() poll(timeout, unit)
檢視 element() peek()

ArrayBlockingQueue

定長的併發優化的BlockingQueue,基於迴圈陣列實現。有一把公共的讀寫鎖與notFull、notEmpty兩個Condition管理佇列滿或空時的阻塞狀態。

LinkedBlockingQueue/LinkedBlockingDeque

可選定長的併發優化的BlockingQueue,基於連結串列實現,所以可以把長度設為Integer.MAX_VALUE。利用連結串列的特徵,分離了takeLock與putLock兩把鎖,繼續用notEmpty、notFull管理佇列滿或空時的阻塞狀態。

補充

JDK7有個LinkedTransferQueue,transfer(e)方法保證Producer放入的元素,被Consumer取走了再返回,比SynchronousQueue更好,有空要學習下。

相關文章