JAVA集合框架中的常用集合及其特點、適用場景、實現原理簡介

zybing發表於2021-09-09

JDK提供了大量優秀的集合實現供開發者使用,合格的程式設計師必須要能夠透過功能場景和效能需求選用最合適的集合,這就要求開發者必須熟悉Java的常用集合類。本文將就Java Collections Framework中常用的集合及其特點、適用場景、實現原理進行介紹,供學習者參考。當然,要真正深入理解Java的集合實現,還是要推薦去閱讀JDK的原始碼。

Java提供的眾多集合類由兩大介面衍生而來:Collection介面和Map介面

Collection介面
Collection介面定義了一個包含一批物件的集合。介面的主要方法包括:

  • size() - 集合內的物件數量

  • add(E)/addAll(Collection) - 向集合內新增單個/批次物件

  • remove(Object)/removeAll(Collection) - 從集合內刪除單個/批次物件

  • contains(Object)/containsAll(Collection) - 判斷集合中是否存在某個/某些物件

  • toArray() - 返回包含集合內所有物件的陣列

Map介面
Map介面在Collection的基礎上,為其中的每個物件指定了一個key,並使用Entry儲存每個key-value對,以實現透過key快速定位到物件(value)。Map介面的主要方法包括:

  • size() - 集合內的物件數量

  • put(K,V)/putAll(Map) - 向Map內新增單個/批次物件

  • get(K) - 返回Key對應的物件

  • remove(K) - 刪除Key對應的物件

  • keySet() - 返回包含Map中所有key的Set

  • values() - 返回包含Map中所有value的Collection

  • entrySet() - 返回包含Map中所有key-value對的EntrySet

  • containsKey(K)/containsValue(V) - 判斷Map中是否存在指定key/value

在瞭解了Collection和Map兩大介面之後,我們再來看一下這兩個介面衍生出來的常用集合類:

List類集合

圖片描述

圖片.png

List介面繼承自Collection,用於定義以列表形式儲存的集合,List介面為集合中的每個物件分配了一個索引(index),標記該物件在List中的位置,並可以透過index定位到指定位置的物件。

List在Collection基礎上增加的主要方法包括:

  • get(int) - 返回指定index位置上的物件

  • add(E)/add(int, E) - 在List末尾/指定index位置上插入一個物件

  • set(int, E) - 替換置於List指定index位置上的物件

  • indexOf(Object) - 返回指定物件在List中的index位置

  • subList(int,int) - 返回指定起始index到終止index的子List物件

List介面的常用實現類:

ArrayList

ArrayList基於陣列來實現集合的功能,其內部維護了一個可變長的物件陣列,集合內所有物件儲存於這個陣列中,並實現該陣列長度的動態伸縮

ArrayList使用陣列複製來實現指定位置的插入和刪除:

插入:


圖片描述

圖片.png

刪除:


圖片描述

圖片.png

LinkedList

LinkedList基於連結串列來實現集合的功能,其實現了靜態類Node,集合中的每個物件都由一個Node儲存,每個Node都擁有到自己的前一個和後一個Node的引用

LinkedList追加元素的過程示例:

圖片描述

圖片.png

ArrayList vs LinkedList

  • ArrayList的隨機訪問更高,基於陣列實現的ArrayList可直接定位到目標物件,而LinkedList需要從頭Node或尾Node開始向後/向前遍歷若干次才能定位到目標物件

  • LinkedList在頭/尾節點執行插入/刪除操作的效率比ArrayList要高

  • 由於ArrayList每次擴容的容量是當前的1.5倍,所以LinkedList所佔的記憶體空間要更小一些

  • 二者的遍歷效率接近,但需要注意,遍歷LinkedList時應用iterator方式,不要用get(int)方式,否則效率會很低

Vector

Vector和ArrayList很像,都是基於陣列實現的集合,它和ArrayList的主要區別在於

  • Vector是執行緒安全的,而ArrayList不是

  • 由於Vector中的方法基本都是synchronized的,其效能低於ArrayList

  • Vector可以定義陣列長度擴容的因子,ArrayList不能

CopyOnWriteArrayList

與 Vector一樣,CopyOnWriteArrayList也可以認為是ArrayList的執行緒安全版,不同之處在於 CopyOnWriteArrayList在寫操作時會先複製出一個副本,在新副本上執行寫操作,然後再修改引用。這種機制讓 CopyOnWriteArrayList可以對讀操作不加鎖,這就使CopyOnWriteArrayList的讀效率遠高於Vector。 CopyOnWriteArrayList的理念比較類似讀寫分離,適合讀多寫少的多執行緒場景。但要注意,CopyOnWriteArrayList只能保證資料的最終一致性,並不能保證資料的實時一致性,如果一個寫操作正在進行中且並未完成,此時的讀操作無法保證能讀到這個寫操作的結果。

Vector vs CopyOnWriteArrayList

  • 二者均是執行緒安全的、基於陣列實現的List

  • Vector是【絕對】執行緒安全的,CopyOnWriteArrayList只能保證讀執行緒會讀到【已完成】的寫結果,但無法像Vector一樣實現讀操作的【等待寫操作完成後再讀最新值】的能力

  • CopyOnWriteArrayList讀效能遠高於Vector,併發執行緒越多優勢越明顯

  • CopyOnWriteArrayList佔用更多的記憶體空間

Map類集合

圖片描述

圖片.png

Map將key和value封裝至一個叫做Entry的物件中,Map中儲存的元素實際是Entry。只有在keySet()和values()方法被呼叫時,Map才會將keySet和values物件例項化。

每一個Map根據其自身特點,都有不同的Entry實現,以對應Map的內部類形式出現。

前文已經對Map介面的基本特點進行過描述,我們直接來看一下Map介面的常用實現類

HashMap

HashMap將Entry物件儲存在一個陣列中,並透過雜湊表來實現對Entry的快速訪問:

圖片描述

圖片.png

由每個Entry中的key的雜湊值決定該Entry在陣列中的位置。以這種特效能夠實現透過key快速查詢到Entry,從而獲得該key對應的value。在不發生雜湊衝突的前提下,查詢的時間複雜度是O(1)。

如果兩個不同的key計算出的index是一樣的,就會發生兩個不同的key都對應到陣列中同一個位置的情況,也就是所謂的雜湊衝突。HashMap處理哈 希衝突的方法是拉鍊法,也就是說陣列中每個位置儲存的實際是一個Entry連結串列,連結串列中每個Entry都擁有指向連結串列中後一個Entry的引用。在發生雜湊衝突時,將衝突的Entry追加至連結串列的頭部。當HashMap在定址時發現某個key對應的陣列index上有多個Entry,便會遍歷該位置上的 Entry連結串列,直到找到目標的Entry。

圖片描述

圖片.png

HashMap的Entry類:

static class Entry implements Map.Entry {        final K key;
        V value;
        Entry next;        int hash;
}

HashMap由於其快速定址的特點,可以說是最經常被使用的Map實現類

Hashtable

Hashtable 可以說是HashMap的前身(Hashtable自JDK1.0就存在,而HashMap乃至整個Map介面都是JDK1.2引入的新特性),其實現思 路與HashMap幾乎完全一樣,都是透過陣列儲存Entry,以key的雜湊值計算Entry在陣列中的index,用拉鍊法解決雜湊衝突。二者最大的不同在於,Hashtable是執行緒安全的,其提供的方法幾乎都是同步的。

ConcurrentHashMap

ConcurrentHashMap是HashMap的執行緒安全版(自JDK1.5引入),提供比Hashtable更高效的併發效能。

圖片描述

圖片.png

Hashtable 在進行讀寫操作時會鎖住整個Entry陣列,這就導致資料越多效能越差。而ConcurrentHashMap使用分離鎖的思路解決併發效能,其將 Entry陣列拆分至16個Segment中,以雜湊演算法決定Entry應該儲存在哪個Segment。這樣就可以實現在寫操作時只對一個Segment 加鎖,大幅提升了併發寫的效能。

在進行讀操作時,ConcurrentHashMap在絕大部分情況下都不需要加鎖,其Entry中的value是volatile的,這保證了value被修改時的執行緒可見性,無需加鎖便能實現執行緒安全的讀操作。

ConcurrentHashMap的HashEntry類:

static final class HashEntry {        final int hash;        final K key;        volatile V value;        volatile HashEntry next;
}

但是魚與熊掌不可兼得,ConcurrentHashMap的高效能是有代價的(否則Hashtable就沒有存在價值了),那就是它不能保證讀操作的絕對 一致性。ConcurrentHashMap保證讀操作能獲取到已存在Entry的value的最新值,同時也能保證讀操作可獲取到已完成的寫操作的內容,但如果寫操作是在建立一個新的Entry,那麼在寫操作沒有完成時,讀操作是有可能獲取不到這個Entry的。

HashMap vs Hashtable vs ConcurrentHashMap

  • 三者在資料儲存層面的機制原理基本一致

  • HashMap不是執行緒安全的,多執行緒環境下除了不能保證資料一致性之外,還有可能在rehash階段引發Entry連結串列成環,導致死迴圈

  • Hashtable是執行緒安全的,能保證絕對的資料一致性,但效能是問題,併發執行緒越多,效能越差

  • ConcurrentHashMap 也是執行緒安全的,使用分離鎖和volatile等方法極大地提升了讀寫效能,同時也能保證在絕大部分情況下的資料一致性。但其不能保證絕對的資料一致性, 在一個執行緒向Map中加入Entry的操作沒有完全完成之前,其他執行緒有可能讀不到新加入的Entry

LinkedHashMap

LinkedHashMap與HashMap非常類似,唯一的不同在於前者的Entry在HashMap.Entry的基礎上增加了到前一個插入和後一個插入的Entry的引用,以實現能夠按Entry的插入順序進行遍歷。

圖片描述

圖片.png

TreeMap

TreeMap是基於紅黑樹實現的Map結構,其Entry類擁有到左/右葉子節點和父節點的引用,同時還記錄了自己的顏色:

static final class Entry implements Map.Entry {
        K key;
        V value;
        Entry left = null;
        Entry right = null;
        Entry parent;        boolean color = BLACK;
}

紅黑樹實際是一種演算法複雜但高效的平衡二叉樹,具備二叉樹的基本性質,即任何節點的值大於其左葉子節點,小於其右葉子節點,利用這種特性,TreeMap能夠實現Entry的排序和快速查詢。

關於紅黑樹的具體介紹,可以參考這篇文章,非常詳細:http://blog.csdn.net/chenssy/article/details/26668941

TreeMap的Entry是有序的,所以提供了一系列方便的功能,比如獲取以升序或降序排列的KeySet(EntrySet)、獲取在指定key(Entry)之前/之後的key(Entry)等等。適合需要對key進行有序操作的場景。

ConcurrentSkipListMap

ConcurrentSkipListMap同樣能夠提供有序的Entry排列,但其實現原理與TreeMap不同,是基於跳錶(SkipList)的:

圖片描述

圖片.png

如上圖所示,ConcurrentSkipListMap由一個多級連結串列實現,底層鏈上擁有所有元素,逐級上升的過程中每個鏈的元素數遞減。在查詢時從頂層鏈出發,按先右後下的優先順序進行查詢,從而實現快速定址。

static class Index {        final Node node;        final Index down;//下引用
        volatile Index right;//右引用}

與TreeMap不同,ConcurrentSkipListMap在進行插入、刪除等操作時,只需要修改影響到的節點的右引用,而右引用又是volatile的,所以ConcurrentSkipListMap是執行緒安全的。但ConcurrentSkipListMap與ConcurrentHashMap一樣,不能保證資料的絕對一致性,在某些情況下有可能無法讀到正在被插入的資料。

TreeMap vs ConcurrentSkipListMap

  • 二者都能夠提供有序的Entry集合

  • 二者的效能相近,查詢時間複雜度都是O(logN)

  • ConcurrentSkipListMap會佔用更多的記憶體空間

  • ConcurrentSkipListMap是執行緒安全的,TreeMap不是

Set類集合

Set 介面繼承Collection,用於儲存不含重複元素的集合。幾乎所有的Set實現都是基於同型別Map的,簡單地說,Set是閹割版的Map。每一個Set內都有一個同型別的Map例項(CopyOnWriteArraySet除外,它內建的是CopyOnWriteArrayList例項),Set把元素作為key儲存在自己的Map例項中,value則是一個空的Object。Set的常用實現也包括 HashSet、TreeSet、ConcurrentSkipListSet等,原理和對應的Map實現完全一致,此處不再贅述。

圖片描述

圖片.png

Queue/Deque類集合

圖片描述

圖片.png

Queue和Deque介面繼承Collection介面,實現FIFO(先進先出)的集合。二者的區別在於,Queue只能在隊尾入隊,隊頭出隊,而Deque介面則在隊頭和隊尾都可以執行出/入隊操作

Queue介面常用方法:

  • add(E)/offer(E):入隊,即向隊尾追加元素,二者的區別在於如果佇列是有界的,add方法在佇列已滿的情況下會丟擲IllegalStateException,而offer方法只會返回false

  • remove()/poll():出隊,即從隊頭移除1個元素,二者的區別在於如果佇列是空的,remove方法會丟擲NoSuchElementException,而poll只會返回null

  • element()/peek():檢視隊頭元素,二者的區別在於如果佇列是空的,element方法會丟擲NoSuchElementException,而peek只會返回null

Deque介面常用方法:

  • addFirst(E) / addLast(E) / offerFirst(E) / offerLast(E)

  • removeFirst() / removeLast() / pollFirst() / pollLast()

  • getFirst() / getLast() / peekFirst() / peekLast()

  • removeFirstOccurrence(Object) / removeLastOccurrence(Object)

Queue介面的常用實現類:

ConcurrentLinkedQueue

ConcurrentLinkedQueue是基於連結串列實現的佇列,佇列中每個Node擁有到下一個Node的引用:

private static class Node {
        volatile E item;        volatile Node next;
}

由於Node類的成員都是volatile的,所以ConcurrentLinkedQueue自然是執行緒安全的。能夠保證入隊和出隊操作的原子性和一致性,但在遍歷和size()操作時只能保證資料的弱一致性。

LinkedBlockingQueue

與ConcurrentLinkedQueue不同,LinkedBlocklingQueue是一種無界的阻塞佇列。所謂阻塞佇列,就是在入隊時如果佇列已滿,執行緒會被阻塞,直到佇列有空間供入隊再返回;同時在出隊時,如果佇列已空,執行緒也會被阻塞,直到佇列中有元素供出隊時再返回。LinkedBlocklingQueue同樣基於連結串列實現,其出隊和入隊操作都會使用ReentrantLock進行加鎖。所以本身是執行緒安全的,但同樣的,只能保證入隊和出隊操作的原子性和一致性,在遍歷時只能保證資料的弱一致性。

ArrayBlockingQueue

ArrayBlockingQueue是一種有界的阻塞佇列,基於陣列實現。其同步阻塞機制的實現與LinkedBlocklingQueue基本一致,區別僅在於前者的生產和消費使用同一個鎖,後者的生產和消費使用分離的兩個鎖。

ConcurrentLinkedQueue vsLinkedBlocklingQueue vs ArrayBlockingQueue

  • ConcurrentLinkedQueue是非阻塞佇列,其他兩者為阻塞佇列

  • 三者都是執行緒安全的

  • LinkedBlocklingQueue是無界的,適合實現不限長度的佇列, ArrayBlockingQueue適合實現定長的佇列

SynchronousQueue

SynchronousQueue算是JDK實現的佇列中比較奇葩的一個,它不能儲存任何元素,size永遠是0,peek()永遠返回null。向其中插入元素的執行緒會阻塞,直到有另一個執行緒將這個元素取走,反之從其中取元素的執行緒也會阻塞,直到有另一個執行緒插入元素。

這種實現機制非常適合傳遞性的場景。也就是說如果生產者執行緒需要及時確認到自己生產的任務已經被消費者執行緒取走後才能執行後續邏輯的場景下,適合使用SynchronousQueue。

PriorityQueue & PriorityBlockingQueue

這兩種Queue並不是FIFO佇列,而是根據元素的優先順序進行排序,保證最小的元素最先出隊,也可以在構造佇列時傳入Comparator例項,這樣PriorityQueue就會按照Comparator例項的要求對元素進行排序。

PriorityQueue是非阻塞佇列,也不是執行緒安全的,PriorityBlockingQueue是阻塞佇列,同時也是執行緒安全的。

Deque 的實現類包括LinkedList(前文已描述過)、ConcurrentLinkedDeque、LinkedBlockingDeque,其實現機制與前文所述的ConcurrentLinkedQueue和LinkedBlockingQueue非常類似,此處不再贅述

最後,對本文中描述的常用集合實現類做一個簡單總結:

圖片描述

圖片.png

作者:kelgon
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/855/viewspace-2809484/,如需轉載,請註明出處,否則將追究法律責任。

相關文章