Java集合的小抄

來源:互聯網
上載者:User

標籤:csdn   aaa   表示   自己   它的   hashset   https   max   寫鎖   

在儘可能短的篇幅裡,將所有集合與並發集合的特徵、實現方式、效能捋一遍。適合所有"精通Java",其實還不那麼自信的人閱讀。 【轉自:花錢的年華】

 

期望能不止用於面試時,平時選擇資料結構,也能考慮一下其成本與效率,不要看著API合適就用了。

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

 

按數組下標訪問元素-get(i)、set(i,e) 的效能很高,這是數組的基本優勢。

如果按下標插入元素、刪除元素-add(i,e)、 remove(i)、remove(e),則要用System.arraycopy()來複製移動部分受影響的元素,效能就變差了。

越是前面的元素,修改時要移動的元素越多。直接在數組末尾加入元素-常用的add(e),刪除最後一個元素則無影響。

 

1.2 LinkedList

以雙向鏈表實現。鏈表無容量限制,但雙向鏈表本身使用了更多空間,每插入一個元素都要構造一個額外的Node對象,也需要額外的鏈表指標操作。

 

按下標訪問元素-get(i)、set(i,e) 要悲劇的部分遍曆鏈表將指標移動到位 (如果i>數組大小的一半,會從末尾移起)。

插入、刪除元素時修改前後節點的指標即可,不在需要複製移動。但還是要部分遍曆鏈表的指標才能移動到下標所指的位置。

只有在鏈表兩頭的操作-add()、addFirst()、removeLast()或用iterator()上的remove()倒能省掉指標的移動。

Apache Commons 有個TreeNodeList,裡面是棵二叉樹,可以快速移動指標到位。

 

1.3 CopyOnWriteArrayList並發最佳化的ArrayList。基於不可變對象策略,在修改時先複製出一個數組快照來修改,改好了,再讓內部指標指向新數組。

 

因為對快照的修改對讀操作來說不可見,所以讀讀之間不互斥,讀寫之間也不互斥,只有寫寫之間要加鎖互斥。但複製快照的成本昂貴,典型的適合讀多寫少的情境。

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

 

1.4 遺憾無論哪種實現,按值返回下標contains(e), indexOf(e), remove(e) 都需遍曆所有元素進行比較,效能可想像的不會太好。沒有按元素值排序的SortedList。除了CopyOnWriteArrayList,再沒有其他安全執行緒又並發最佳化的實現如ConcurrentLinkedList。湊合著用Set與Queue中的等價類別時,會缺少一些List特有的方法如get(i)。如果更新頻率較高,或數組較大時,還是得用Collections.synchronizedList(list),對所有操作用同一把鎖來保證安全執行緒。

 

2.Map2.1 HashMap

 

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

插入元素時,如果兩條Key落在同一個桶(比如雜湊值1和17模數16後都屬於第一個雜湊桶),我們稱之為雜湊衝突。

JDK的做法是鏈表法,Entry用一個next屬性實現多個Entry以單向鏈表存放。尋找雜湊值為17的key時,先定位到雜湊桶,然後鏈表遍曆桶裡所有元素,逐個比較其Hash值然後key值。

在JDK8裡,新增預設為8的閥值,當一個桶裡的Entry超過閥值,就不以單向鏈表而以紅/黑樹狀結構來存放以加快Key的尋找速度。

當然,最好還是桶裡只有一個元素,不用去比較。所以預設當Entry數量達到桶數量的75%時,雜湊衝突已比較嚴重,就會成倍擴容桶數組,並重新分配所有原來的Entry。擴容成本不低,所以也最好有個預估值。

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

iterator()時順著雜湊桶數組來遍曆,看起來是個亂序。

 

2.2 LinkedHashMap

擴充HashMap,每個Entry增加雙向鏈表,號稱是最占記憶體的資料結構。

支援iterator()時按Entry的插入順序來排序(如果設定accessOrder屬性為true,則所有讀寫訪問都排序)。

插入時,Entry把自己加到Header Entry的前面去。如果所有讀寫訪問都要排序,還要把前後Entry的before/after拼接起來以在鏈表中刪除掉自己,所以此時讀操作也是線程不安全的了。

 

2.3 TreeMap

以紅/黑樹狀結構實現,紅/黑樹狀結構又叫自平衡二叉樹:

對於任一節點而言,其到分葉節點的每一條路徑都包含相同數目的黑結點。

上面的規定,使得樹的層數不會差的太遠,使得所有操作的複雜度不超過 O(lgn),但也使得插入,修改時要複雜的左旋右旋來保持樹的平衡。

支援iterator()時按Key值排序,可按實現了Comparable介面的Key的升序排序,或由傳入的Comparator控制。可想象的,在樹上插入/刪除元素的代價一定比HashMap的大。

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

 

2.4 EnumMap

EnumMap的原理是,在建構函式裡要傳入枚舉類,那它就構建一個與枚舉的所有值等大的數組,按Enum. ordinal()下標來訪問數組。效能與記憶體佔用俱佳。

美中不足的是,因為要實現Map介面,而 V get(Object key)中key是Object而不是泛型K,所以安全起見,EnumMap每次訪問都要先對Key進行類型判斷,在JMC裡錄得不低的採樣命中頻率。

 

2.5 ConcurrentHashMap

並發最佳化的HashMap。

在JDK7裡的經典設計,預設16把寫鎖(可以設定更多),有效分散了阻塞的機率。資料結構為Segment[],每個Segment一把鎖。Segment裡面才是雜湊桶數組。Key先算出它在哪個Segment裡,再去算它在哪個雜湊桶裡。

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

但在JDK8裡,Segment[]的設計被拋棄了,改為精心設計的,只在需要鎖的時候加鎖。

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

 

2.6 ConcurrentSkipListMap

JDK6新增的並發最佳化的SortedMap,以SkipList結構實現。Concurrent包選用它是因為它支援基於CAS的無鎖演算法,而紅/黑樹狀結構則沒有好的無鎖演算法。

原理上,可以想象為多個鏈表組成的N層樓,其中的元素從稀疏到密集,每個元素有往右與往下的指標。從第一層樓開始遍曆,如果右端的值比期望的大,那就往下走一層,繼續往前走。

 

 

典型的空間換時間。每次插入,都要決定在哪幾層插入,同時,要決定要不要多蓋一層樓。

它的size()同樣不能隨便調,會遍曆來統計。

 

3.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()) 實現。

 

 

 4.Queue

Queue是在兩端出入的List,所以也可以用數組或鏈表來實現。

4.1 普通隊列

4.1.1 LinkedList
是的,以雙向鏈表實現的LinkedList既是List,也是Queue。

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

為了支援FIFO,即從數組尾壓入元素(快),從數組頭取出元素(超慢),就不能再使用普通ArrayList的實現了,改為使用迴圈數組。

有隊頭隊尾兩個下標:彈出元素時,隊頭下標遞增;加入元素時,隊尾下標遞增。如果加入元素時已到數組空間的末尾,則將元素賦值到數組[0],同時隊尾下標指向0,再插入下一個元素則賦值到數組[1],隊尾下標指向1。如果隊尾的下標追上隊頭,說明數組所有空間已用完,進行雙倍的數組擴容。

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

平衡最小二元堆積,用一個簡單的數組即可表達,可以快速定址,沒有指標什麼的。最小的在queue[0] ,比如queue[4]的兩個孩子,會在queue[2*4+1] 和 queue[2*(4+1)],即queue[9]和queue[10]。

入隊時,插入queue[size],然後二叉地往上比較調整堆。

出隊時,彈出queue[0],然後把queque[size]拿出來二叉地往下比較調整堆。

初始大小為11,空間不夠時自動50%擴容。

 

4.2 安全執行緒的隊列

4.2.1 ConcurrentLinkedQueue/Deque
無界的並發最佳化的Queue,基於鏈表,實現了依賴於CAS的無鎖演算法。

ConcurrentLinkedQueue的結構是單向鏈表和head/tail兩個指標,因為入隊時需要修改隊尾元素的next指標,以及修改tail指向新入隊的元素兩個CAS動作無法原子,所以需要的特殊的演算法。

4.2.2 PriorityBlockingQueue
無界的PriorityQueue,也是基於數組儲存的二元堆積。一把公用的鎖實現安全執行緒。雖然實現了BlockingQueue介面,但因為無界,其實沒有任何阻塞隊列的特徵,空間不夠時會自動擴容。

4.2.3 DelayQueue
內部包含一個PriorityQueue,同樣是無界的。一把公用的鎖實現安全執行緒。元素需實現Delayed介面,每次調用時需返回當前離觸發時間還有多久,小於0表示該觸發了。

pull()時會用peek()查看隊頭的元素,檢查是否到達觸發時間。ScheduledThreadPoolExecutor用了類似的結構。

 
4.3 安全執行緒的阻塞隊列

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

  立刻報異常 立刻返回布爾 阻塞等待 可設定等待時間
入隊 add(e) offer(e) put(e) offer(e, timeout, unit)
出隊 remove() poll() take() poll(timeout, unit)
查看 element() peek()

 

4.3.1 ArrayBlockingQueue

定長的並發最佳化的BlockingQueue,也是基於迴圈數組實現。有一把公用的鎖與notFull、notEmpty兩個Condition管理隊列滿或空時的阻塞狀態。

4.3.2 LinkedBlockingQueue/Deque

可選定長的並發最佳化的BlockingQueue,基於鏈表實現,所以可以把長度設為Integer.MAX_VALUE成為無界無等待的。

利用鏈表的特徵,分離了takeLock與putLock兩把鎖,繼續用notEmpty、notFull管理隊列滿或空時的阻塞狀態。

 

4.4 同步隊列

SynchronousQueue同步隊列本身無容量,放入元素時,比如等待元素被另一條線程的消費者取走再返回。JDK線程池裡用它。

JDK7還有個LinkedTransferQueue,在普通安全執行緒的BlockingQueue的基礎上,增加一個transfer(e) 函數,效果與SynchronousQueue一樣。

 

5. 參考文檔

    • 紅/黑樹狀結構: https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/03.01.md 

    • 跳錶:http://blog.sina.com.cn/s/blog_72995dcc01017w1t.html

    • 二元堆積:http://blog.csdn.net/lcore/article/details/9100073

    • ConcurrentLinkedQueue:http://www.ibm.com/developerworks/cn/java/j-jtp04186/

Java集合的小抄

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.