更多內容請關注微信公衆號【Java技術江湖】html
這是一位阿里 Java 工程師的技術小站,做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!(關注公衆號後回覆」資料「便可領取 3T 免費技術學習資源以及我我原創的程序員校招指南、Java學習指南等資源)java
本文介紹了Java集合類的基本框架,接口結構以及部分源碼分析,而且經過本身實現一些集合類來更好地剖析Java集合類的總體結構。git
本文只是對集合類框架進行一個大概的梳理,畢竟集合框架中包含的類太多了,一篇文章不可能講完,這裏先開一個頭,對總體框架有一個清晰認識以後,再去探索各個接口實現類的奧祕。程序員
後面會專門地寫幾篇關於集合類的文章,分別介紹一下List,Map,Set以及Stack等等這些接口的實現類,敬請期待。github
具體代碼在個人GitHub中能夠找到面試
https://github.com/h2pl/MyTech
喜歡的話麻煩點一下星哈謝謝。算法
文章首發於個人我的博客:後端
https://h2pl.github.io/2018/0...
更多關於Java後端學習的內容請到個人CSDN博客上查看:數組
https://blog.csdn.net/a724888
在編寫java程序中,咱們最經常使用的除了八種基本數據類型,String對象外還有一個集合類,在咱們的的程序中處處充斥着集合類的身影!安全
java中集合你們族的成員實在是太豐富了,有經常使用的ArrayList、HashMap、HashSet,也有不經常使用的Stack、Queue,有線程安全的Vector、HashTable,也有線程不安全的LinkedList、TreeMap等等!
上面的圖展現了整個集合你們族的成員以及他們之間的關係。下面就上面的各個接口、基類作一些簡單的介紹(主要介紹各個集合的特色。區別)。
下面幾張圖更清晰地介紹告終合類接口間的關係:
Collections和Collection。
Arrays和Collections。
Collection的子接口
map的實現類
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]; } } }
List接口爲Collection直接接口。List所表明的是有序的Collection,即它用某種特定的插入順序來維護元素順序。用戶能夠對列表中每一個元素的插入位置進行精確地控制,同時能夠根據元素的整數索引(在列表中的位置)訪問元素,並搜索列表中的元素。實現List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。
2.一、ArrayList
ArrayList是一個動態數組,也是咱們最經常使用的集合。它容許任何符合規則的元素插入甚至包括null。每個ArrayList都有一個初始容量(10),該容量表明瞭數組的大小。隨着容器中的元素不斷增長,容器的大小也會隨着增長。在每次向容器中增長元素的同時都會進行容量檢查,當快溢出時,就會進行擴容操做。因此若是咱們明確所插入元素的多少,最好指定一個初始容量值,避免過多的進行擴容操做而浪費時間、效率。
size、isEmpty、get、set、iterator 和 listIterator 操做都以固定時間運行。add 操做以分攤的固定時間運行,也就是說,添加 n 個元素須要 O(n) 時間(因爲要考慮到擴容,因此這不僅是添加元素會帶來分攤固定時間開銷那樣簡單)。
ArrayList擅長於隨機訪問。同時ArrayList是非同步的。
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(...));2.三、Vector
與ArrayList類似,可是Vector是同步的。因此說Vector是線程安全的動態數組。它的操做與ArrayList幾乎同樣。2.四、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]; } } }
Set是一種不包括重複元素的Collection。它維持它本身的內部排序,因此隨機訪問沒有任何意義。與List同樣,它一樣運行null的存在可是僅有一個。因爲Set接口的特殊性,全部傳入Set集合中的元素都必須不一樣,同時要注意任何可變對象,若是在對集合中元素進行操做時,致使e1.equals(e2)==true,則一定會產生某些問題。實現了Set接口的集合有:EnumSet、HashSet、TreeSet。
3.一、EnumSet
是枚舉的專用Set。全部的元素都是枚舉類型。3.二、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]; } } }
Map與List、Set接口不一樣,它是由一系列鍵值對組成的集合,提供了key到Value的映射。同時它也沒有繼承Collection。在Map中它保證了key與value之間的一一對應關係。也就是說一個key對應一個value,因此它不能存在相同的key值,固然value值能夠相同。實現map的有:HashMap、TreeMap、HashTable、Properties、EnumMap。
4.一、HashMap
以哈希表數據結構實現,查找對象時經過哈希函數計算其位置,它是爲快速查詢而設計的,其內部定義了一個hash表數組(Entry[] table),元素會經過哈希轉換函數將元素的哈希地址轉換成數組中存放的索引,若是有衝突,則使用散列鏈表的形式將全部相同哈希地址的元素串起來,可能經過查看HashMap.Entry的源碼它是一個單鏈表結構。4.二、TreeMap
鍵以某種排序規則排序,內部以red-black(紅-黑)樹數據結構實現,實現了SortedMap接口4.三、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; } } }
隊列,它主要分爲兩大類,一類是阻塞式隊列,隊列滿了之後再插入元素則會拋出異常,主要包括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; } } }
這部份內容轉自我偶像 江南白衣 的博客:http://calvin1978.blogcn.com/...
在儘量短的篇幅裏,將全部集合與併發集合的特徵、實現方式、性能捋一遍。適合全部"精通Java",其實還不那麼自信的人閱讀。
指望能不止用於面試時,平時選擇數據結構,也能考慮一下其成本與效率,不要看着API合適就用了。
1.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.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。
在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.6 ConcurrentSkipListMap
JDK6新增的併發優化的SortedMap,以SkipList結構實現。Concurrent包選用它是由於它支持基於CAS的無鎖算法,而紅黑樹則沒有好的無鎖算法。
原理上,能夠想象爲多個鏈表組成的N層樓,其中的元素從稀疏到密集,每一個元素有往右與往下的指針。從第一層樓開始遍歷,若是右端的值比指望的大,那就往下走一層,繼續往前走。
典型的空間換時間。每次插入,都要決定在哪幾層插入,同時,要決定要不要多蓋一層樓。
它的size()一樣不能隨便調,會遍從來統計。
全部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是在兩端出入的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[24+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.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.3.3 PriorityBlockingQueue
無界的PriorityQueue,也是基於數組存儲的二叉堆(見前)。一把公共的鎖實現線程安全。由於無界,空間不夠時會自動擴容,因此入列時不會鎖,出列爲空時纔會鎖。
4.3.4 DelayQueue
內部包含一個PriorityQueue,一樣是無界的,一樣是出列時纔會鎖。一把公共的鎖實現線程安全。元素需實現Delayed接口,每次調用時需返回當前離觸發時間還有多久,小於0表示該觸發了。
pull()時會用peek()查看隊頭的元素,檢查是否到達觸發時間。ScheduledThreadPoolExecutor用了相似的結構。
4.4 同步隊列
SynchronousQueue同步隊列自己無容量,放入元素時,好比等待元素被另外一條線程的消費者取走再返回。JDK線程池裏用它。
JDK7還有個LinkedTransferQueue,在普通線程安全的BlockingQueue的基礎上,增長一個transfer(e) 函數,效果與SynchronousQueue同樣。