(十九)Java集合類

本文目錄

1、Java集合類簡述

2、Collection接口

3、List接口

3.1 ArrayList

3.2 LinkedList

3.3 Vector

3.4 Stack

4、Set接口

4.1 EnumSet

4.2 HashSet

5、Map接口

5.1、HashMap

5.2、TreeMap

5.3、HashTable

6、Queue隊列

2 Java集合整理集中小結

2.1 List

2.1.1 ArrayList

2.1.2 LinkedList

2.1.3 CopyOnWriteArrayList

2.1.4 List缺點

2.2 Map

2.2.1 HashMap

2.2.2 LinkedHashMap

2.2.3 TreeMap

2.2.4 EnumMap

2.2.5 ConcurrentHashMap

2.2.6 ConcurrentSkipListMap

2.3 Set

2.4 Queue

2.4.1 普通隊列

2.4.2 PriorityQueue

2.4.3 線程安全的隊列

2.4.4 線程安全的阻塞隊列

2.4.5 同步隊列


1、Java集合類簡述

在Java程序開發中,最常用的除了8種基本數據類型、String對象外,還有的就是集合類了。在我們的開發程序中,集合類可以說必不可少。

(1)集合類成員如下圖所示:

java中集合類的成員實在是太豐富了,有常用的ArrayList、HashMap、HashSet,也有不常用的Stack、Queue,有線程安全的Vector、HashTable,也有線程不安全的LinkedList、TreeMap等等!

(2)集合類成員及其關係如下:

(3)集合類接口間的關係:

a)Collections和Collection

 b)Arrays和Collections

 c)Collection的子接口

d)Map的實現類

2、Collection接口

Collection接口是最基本的集合接口,它不提供直接的實現,Java SDK提供的類都是繼承自Collection的“子接口”如List和Set。Collection所代表的是一種規則,它所包含的元素都必須遵循一條或者多條規則。如有些允許重複而有些則不能重複、有些必須要按照順序插入而有些則是散列,有些支持排序但是有些則不支持。

在Java中所有實現了Collection接口的類都必須提供兩套標準的構造函數,一個是無參,用於創建一個空的Collection,一個是帶有Collection參數的有參構造函數,用於創建一個新的Collection,這個新的Collection與傳入進來的Collection具備相同的元素。

public class Collection接口 {
    class collect implements Collection {

        @Override
        public int size() {
            return 0;
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public boolean contains(Object o) {
            return false;
        }

        @Override
        public Iterator iterator() {
            return null;
        }

        @Override
        public Object[] toArray() {
            return new Object[0];
        }

        @Override
        public boolean add(Object o) {
            return false;
        }

        @Override
        public boolean remove(Object o) {
            return false;
        }

        @Override
        public boolean addAll(Collection c) {
            return false;
        }

        @Override
        public void clear() {

        }
        //省略部分代碼  

        @Override
        public Object[] toArray(Object[] a) {
            return new Object[0];
        }
    }
}

 

3、List接口

List接口爲Collection直接接口。List所代表的是有序的Collection,即它用某種特定的插入順序來維護元素順序。用戶可以對列表中每個元素的插入位置進行精確地控制,同時可以根據元素的整數索引(在列表中的位置)訪問元素,並搜索列表中的元素。實現List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。

3.1 ArrayList

ArrayList是一個動態數組,也是我們最常用的集合。它允許任何符合規則的元素插入甚至包括null。每一個ArrayList都有一個初始容量(10),該容量代表了數組的大小。隨着容器中的元素不斷增加,容器的大小也會隨着增加。在每次向容器中增加元素的同時都會進行容量檢查,當快溢出時,就會進行擴容操作。所以如果我們明確所插入元素的多少,最好指定一個初始容量值,避免過多的進行擴容操作而浪費時間、效率。

size、isEmpty、get、set、iterator 和 listIterator 操作都以固定時間運行。add 操作以分攤的固定時間運行,也就是說,添加 n 個元素需要 O(n) 時間(由於要考慮到擴容,所以這不只是添加元素會帶來分攤固定時間開銷那樣簡單)。

ArrayList擅長於隨機訪問。同時ArrayList是非同步的。

3.2 LinkedList

同樣實現List接口的LinkedList與ArrayList不同,ArrayList是一個動態數組,而LinkedList是一個雙向鏈表。所以它除了有ArrayList的基本操作方法外還額外提供了get,remove,insert方法在LinkedList的首部或尾部。

由於實現的方式不同,LinkedList不能隨機訪問,它所有的操作都是要按照雙重鏈表的需要執行。在列表中索引的操作將從開頭或結尾遍歷列表(從靠近指定索引的一端)。這樣做的好處就是可以通過較低的代價在List中進行插入和刪除操作。

與ArrayList一樣,LinkedList也是非同步的。如果多個線程同時訪問一個List,則必須自己實現訪問同步。一種解決方法是在創建List時構造一個同步的List: List list = Collections.synchronizedList(new LinkedList(...));

3.3 Vector

與ArrayList相似,但是Vector是同步的。所以說Vector是線程安全的動態數組。它的操作與ArrayList幾乎一樣。

3.4 Stack

Stack繼承自Vector,實現一個後進先出的堆棧。Stack提供5個額外的方法使得Vector得以被當作堆棧使用。基本的push和pop 方法,還有peek方法得到棧頂的元素,empty方法測試堆棧是否爲空,search方法檢測一個元素在堆棧中的位置。Stack剛創建後是空棧。。

示例如下:

public class List接口 {
    //下面是List的繼承關係,由於List接口規定了包括諸如索引查詢,迭代器的實現,所以實現List接口的類都會有這些方法。
    //所以不管是ArrayList和LinkedList底層都可以使用數組操作,但一般不提供這樣外部調用方法。
    //    public interface Iterable<T>
//    public interface Collection<E> extends Iterable<E>
//    public interface List<E> extends Collection<E>
    class MyList implements List {

        @Override
        public int size() {
            return 0;
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public boolean contains(Object o) {
            return false;
        }

        @Override
        public Iterator iterator() {
            return null;
        }

        @Override
        public Object[] toArray() {
            return new Object[0];
        }

        @Override
        public boolean add(Object o) {
            return false;
        }

        @Override
        public boolean remove(Object o) {
            return false;
        }

        @Override
        public void clear() {

        }

       //省略部分代碼
       
        @Override
        public Object get(int index) {
            return null;
        }

        @Override
        public ListIterator listIterator() {
            return null;
        }

        @Override
        public ListIterator listIterator(int index) {
            return null;
        }

        @Override
        public List subList(int fromIndex, int toIndex) {
            return null;
        }

        @Override
        public Object[] toArray(Object[] a) {
            return new Object[0];
        }
    }
}

4、Set接口

Set是一種不包括重複元素的Collection。它維持它自己的內部排序,所以隨機訪問沒有任何意義。與List一樣,它同樣運行null的存在但是僅有一個。由於Set接口的特殊性,所有傳入Set集合中的元素都必須不同,同時要注意任何可變對象,如果在對集合中元素進行操作時,導致e1.equals(e2)==true,則必定會產生某些問題。實現了Set接口的集合有:EnumSet、HashSet、TreeSet。

4.1 EnumSet

是枚舉的專用Set。所有的元素都是枚舉類型。

4.2 HashSet

HashSet堪稱查詢速度最快的集合,因爲其內部是以HashCode來實現的。它內部元素的順序是由哈希碼來決定的,所以它不保證set 的迭代順序;特別是它不保證該順序恆久不變。

示例如下:

public class Set接口 {
    // Set接口規定將set看成一個集合,並且使用和數組類似的增刪改查方式,同時提供iterator迭代器
    //    public interface Set<E> extends Collection<E>
    //    public interface Collection<E> extends Iterable<E>
    //    public interface Iterable<T>
    class MySet implements Set {

        @Override
        public int size() {
            return 0;
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public boolean contains(Object o) {
            return false;
        }

        @Override
        public Iterator iterator() {
            return null;
        }

        @Override
        public Object[] toArray() {
            return new Object[0];
        }

        @Override
        public boolean add(Object o) {
            return false;
        }

        @Override
        public boolean remove(Object o) {
            return false;
        }

        @Override
        public boolean addAll(Collection c) {
            return false;
        }

        @Override
        public void clear() {

        }

        @Override
        public boolean removeAll(Collection c) {
            return false;
        }

        @Override
        public boolean retainAll(Collection c) {
            return false;
        }

        @Override
        public boolean containsAll(Collection c) {
            return false;
        }

        @Override
        public Object[] toArray(Object[] a) {
            return new Object[0];
        }
    }
}

5、Map接口

Map與List、Set接口不同,它是由一系列鍵值對組成的集合,提供了key到Value的映射。同時它也沒有繼承Collection。在Map中它保證了key與value之間的一一對應關係。也就是說一個key對應一個value,所以它不能存在相同的key值,當然value值可以相同。實現map的有:HashMap、TreeMap、HashTable、Properties、EnumMap。

5.1、HashMap

以哈希表數據結構實現,查找對象時通過哈希函數計算其位置,它是爲快速查詢而設計的,其內部定義了一個hash表數組(Entry[] table),元素會通過哈希轉換函數將元素的哈希地址轉換成數組中存放的索引,如果有衝突,則使用散列鏈表的形式將所有相同哈希地址的元素串起來,可能通過查看HashMap.Entry的源碼它是一個單鏈表結構。

5.2、TreeMap

鍵以某種排序規則排序,內部以red-black(紅-黑)樹數據結構實現,實現了SortedMap接口。

5.3、HashTable

HashTable也是以哈希表數據結構實現的,解決衝突時與HashMap也一樣也是採用了散列鏈表的形式,不過性能比HashMap要低。

示例如下:

public class Map接口 {
    //Map接口是最上層接口,Map接口實現類必須實現put和get等哈希操作。
    //並且要提供keyset和values,以及entryset等查詢結構。
    //public interface Map<K,V>
    class MyMap implements Map {

        @Override
        public int size() {
            return 0;
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public boolean containsKey(Object key) {
            return false;
        }

        @Override
        public boolean containsValue(Object value) {
            return false;
        }

        @Override
        public Object get(Object key) {
            return null;
        }

        @Override
        public Object put(Object key, Object value) {
            return null;
        }

        @Override
        public Object remove(Object key) {
            return null;
        }

        @Override
        public void putAll(Map m) {

        }

        @Override
        public void clear() {

        }

        @Override
        public Set keySet() {
            return null;
        }

        @Override
        public Collection values() {
            return null;
        }

        @Override
        public Set<Entry> entrySet() {
            return null;
        }
    }
}

6、Queue隊列

隊列,它主要分爲兩大類,一類是阻塞式隊列,隊列滿了以後再插入元素則會拋出異常,主要包括:ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一種隊列則是雙端隊列,支持在頭、尾兩端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。

public class Queue接口 {
    //queue接口是對隊列的一個實現,需要提供隊列的進隊出隊等方法。一般使用linkedlist作爲實現類
    class MyQueue implements Queue {

        @Override
        public int size() {
            return 0;
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public boolean contains(Object o) {
            return false;
        }

        @Override
        public Iterator iterator() {
            return null;
        }

        @Override
        public Object[] toArray() {
            return new Object[0];
        }

        @Override
        public Object[] toArray(Object[] a) {
            return new Object[0];
        }

        @Override
        public boolean add(Object o) {
            return false;
        }

        @Override
        public boolean remove(Object o) {
            return false;
        }

        //省略部分代碼
        @Override
        public boolean offer(Object o) {
            return false;
        }

        @Override
        public Object remove() {
            return null;
        }

        @Override
        public Object poll() {
            return null;
        }

        @Override
        public Object element() {
            return null;
        }

        @Override
        public Object peek() {
            return null;
        }
    }
}

2 Java集合整理集中小結

2.1 List

2.1.1 ArrayList

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

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

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

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

2.1.2 LinkedList

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

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

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

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

(5)Apache Commons 有個TreeNodeList,裏面是棵二叉樹,可以快速移動指針到位。

2.1.3 CopyOnWriteArrayList

(1)併發優化的ArrayList。基於不可變對象策略,在修改時先複製出一個數組快照來修改,改好了,再讓內部指針指向新數組。

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

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

2.1.4 List缺點

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

(2)沒有按元素值排序的SortedList。

(3)除了CopyOnWriteArrayList,再沒有其他線程安全又併發優化的實現如ConcurrentLinkedList。湊合着用Set與Queue中的等價類時,會缺少一些List特有的方法如get(i)。如果更新頻率較高,或數組較大時,還是得用Collections.synchronizedList(list),對所有操作用同一把鎖來保證線程安全。

2.2 Map

2.2.1 HashMap

(1)以Entry[]數組實現的哈希桶數組,用Key的哈希值取模桶數組的大小可得到數組下標。

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

(3)JDK的做法是鏈表法,Entry用一個next屬性實現多個Entry以單向鏈表存放。查找哈希值爲17的key時,先定位到哈希桶,然後鏈表遍歷桶裏所有元素,逐個比較其Hash值然後key值。

在JDK8裏,新增默認爲8的閾值,當一個桶裏的Entry超過閥值,就不以單向鏈表而以紅黑樹來存放以加快Key的查找速度。當然,最好還是桶裏只有一個元素,不用去比較。所以默認當Entry數量達到桶數量的75%時,哈希衝突已比較嚴重,就會成倍擴容桶數組,並重新分配所有原來的Entry。擴容成本不低,所以也最好有個預估值。

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

(5)iterator()時順着哈希桶數組來遍歷,看起來是個亂序。

2.2.2 LinkedHashMap

擴展HashMap,每個Entry增加雙向鏈表,號稱是最佔內存的數據結構。

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

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

2.2.3 TreeMap

以紅黑樹實現,紅黑樹又叫自平衡二叉樹:

對於任一節點而言,其到葉節點的每一條路徑都包含相同數目的黑結點。 上面的規定,使得樹的層數不會差的太遠,使得所有操作的複雜度不超過O(lg n)),但也使得插入,修改時要複雜的左旋右旋來保持樹的平衡。

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

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

2.2.4 EnumMap

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

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

2.2.5 ConcurrentHashMap

併發優化的HashMap。

在JDK5裏的經典設計,默認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.2.6 ConcurrentSkipListMap

JDK6新增的併發優化的SortedMap,以SkipList結構實現。Concurrent包選用它是因爲它支持基於CAS的無鎖算法,而紅黑樹則沒有好的無鎖算法。

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

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

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

2.3 Set

所有Set幾乎都是內部用一個Map來實現, 因爲Map裏的KeySet就是一個Set,而value是假值,全部使用同一個Object即可。

Set的特徵也繼承了那些內部的Map實現的特徵。

(1)HashSet:內部是HashMap。

(2)LinkedHashSet:內部是LinkedHashMap。

(3)TreeSet:內部是TreeMap的SortedSet。

(4)ConcurrentSkipListSet:內部是ConcurrentSkipListMap的併發優化的SortedSet。

(5)CopyOnWriteArraySet:內部是CopyOnWriteArrayList的併發優化的Set,利用其addIfAbsent()方法實現元素去重,如前所述該方法的性能很一般。

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

2.4 Queue

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

2.4.1 普通隊列

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

ArrayDeque 以循環數組實現的雙向Queue。大小是2的倍數,默認是16。

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

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

2.4.2 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%擴容。

2.4.3 線程安全的隊列

ConcurrentLinkedQueue/Deque 無界的併發優化的Queue,基於鏈表,實現了依賴於CAS的無鎖算法。

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

2.4.4 線程安全的阻塞隊列

BlockingQueue,一來如果隊列已空不用重複的查看是否有新數據而會阻塞在那裏,二來隊列的長度受限,用以保證生產者與消費者的速度不會相差太遠。當入隊時隊列已滿,或出隊時隊列已空。

ArrayBlockingQueue 定長的併發優化的BlockingQueue,也是基於循環數組實現。有一把公共的鎖與notFull、notEmpty兩個Condition管理隊列滿或空時的阻塞狀態。

LinkedBlockingQueue/Deque 可選定長的併發優化的BlockingQueue,基於鏈表實現,所以可以把長度設爲Integer.MAX_VALUE成爲無界無等待的。

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

PriorityBlockingQueue 無界的PriorityQueue,也是基於數組存儲的二叉堆(見前)。一把公共的鎖實現線程安全。因爲無界,空間不夠時會自動擴容,所以入列時不會鎖,出列爲空時纔會鎖。

DelayQueue 內部包含一個PriorityQueue,同樣是無界的,同樣是出列時纔會鎖。一把公共的鎖實現線程安全。元素需實現Delayed接口,每次調用時需返回當前離觸發時間還有多久,小於0表示該觸發了。

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

2.4.5 同步隊列

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

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

 

附言:

本文整理來源於網絡、博客等資源,僅做個人學習筆記複習所用。

如果對你學習有用,請點贊共同學習!

如有侵權,請聯繫我刪!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章