Java中有不少集合類,例如ArrayList,LinkedList,HashMap,TreeMap等。集合的功能就是容納多個對象,它們就像容器同樣(實際上,直接稱爲容器也沒有毛病,C++就是這樣稱呼的),當須要的時候,能夠從裏面拿出來,很是方便。在Java5提供了泛型機制以後,使容器有了在編譯期作類型檢查的能力,使用起來更加安全、方便。java
Java中的集合類主要是有兩個接口派生出來的:Collection和Map,以下所示:node
Collection又主要有Set,Queue,List三大接口,在此基礎上,又有多個實現類。Map接口下一樣有總多的實現類,例如HashMap,EnumMap,HashTable等。接下來我將會挑選幾個經常使用的集合類來具體討論討論。算法
List集合能夠說是最經常使用的集合了,比HashMap還經常使用,通常咱們寫代碼的時候一旦遇到須要存儲多個元素的狀況,就優先想到使用List集合,至於使用的是ArrayList實現類仍是LinkedList實現類,那就具體狀況具體分析了。數組
ArrayList實現了List接口,繼承了AbstractList抽象類,AbstractList抽象類實現了絕大部分List接口定義的抽象方法,因此咱們在ArrayList源碼中看不到大部分List接口中定義的抽象方法的實現。ArrayList的內部使用數組來存儲對象,這也是ArrayList這個名字的由來,其各類操做,例如get,add等都是基於數組操做的,下面是add方法的源碼:安全
public void add(int index, E element) {
//先檢查index是否在一個合理的範圍內
rangeCheckForAdd(index);
//保證數組的容量足夠加入新的元素,發現不足夠的話會進行擴容操做
ensureCapacityInternal(size + 1); // Increments modCount!!
//進行一次數組拷貝,這裏的elementData就是保存對象的Object數組
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//往數組中加入元素
elementData[index] = element;
//修改size大小
size++;
}
複製代碼
解釋在註釋中給出了,get方法也很是簡單,就不浪費時間了。數據結構
LinkedList繼承了AbstractSequentialList類,AbstractSequentialList類又繼承了AbstractList類,同時LinkedList也固然有實現List接口的,並且還實現了Deque接口,這就比較有意思了,說明LinkedList不只僅是List,仍是一個Queue。下圖表示其繼承體系:併發
LinkedList的基於鏈表實現的List,這是和ArrayList最大的區別。LinkedList有一個Node內部類,用來表示節點,以下所示:app
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
複製代碼
該類有next指針和prev指針,可見是一個雙向鏈表。接下來看看LinkedList的add操做:dom
public void add(int index, E element) {
//檢查index
checkPositionIndex(index);
//若是index和size相等,說明已經到最後了,直接在last節點後插入節點接口
if (index == size)
linkLast(element);
else //不然就在index位置插入節點
linkBefore(element, node(index));
}
複製代碼
在linkLast()和linkBefore()方法裏會涉及到鏈表的操做,其中LinkLast()的實現比較簡單,linkBefore()稍微複雜一些,但只要學過數據結構的朋友,看這些源碼應該沒什麼問題,在此不貼出源碼了,比較本文定位不是源碼解析。函數
在前面的介紹中其實有說到過,在這裏總結一下:
之因此把他們倆發在一塊兒是由於它們是線程安全的列表集合。SynchronizedList是Collections工具類裏的一個內部靜態類,實現了List接口,繼承了SynchronizedCollection類,Vector是JDK早期的一個同步的List,和ArrayList的繼承體系徹底同樣,並且也是基於數組實現的,只是他的各類方法都是同步的。
SynchronizedList類是一個Collections類中包級私有的靜態內部類,咱們在編寫代碼的時候沒法直接調用這個類,只能經過Collection.synchronizedList()方法並傳入一個List來使用它,這個方法實際上就是幫咱們將原來沒有同步措施的普通List包裝成了SynchronizedList,使其擁有線程安全的特性,對其進行操做就是對原List的操做,以下所示:
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
複製代碼
Vector是JDK1.0就有的類,算是一個遠古時期的類了,在當時由於沒有比較好的同步工具,因此在併發場景下會使用到這個類,但如今隨着併發技術的進步,有了更好的同步工具類,因此Vector已經快成爲半廢棄狀態了。爲何呢?主要仍是由於同步效率過低,同步手段太粗暴了,粗暴到直接將絕大多數方法弄成同步方法(在方法上加入synchronized關鍵字),連clone方法都沒放過:
public synchronized Object clone() {
try {
@SuppressWarnings("unchecked")
Vector<E> v = (Vector<E>) super.clone();
v.elementData = Arrays.copyOf(elementData, elementCount);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
複製代碼
這樣作雖然能確保線程安全,但效率實在過低了啊,尤爲是在競爭激烈的環境下,效率可能還不如單線程。相比之下,SynchronizedList就好不少,只是在必要的地方進行加鎖而已(不過實際上效率仍是挺低的)。基於它的CRUD操做就很少說了,和ArrayList沒什麼大的區別。
他們最大區別就在同步效率,Vector的同步手段過於粗暴以致於效率過低,SynchronizedList的同步手段沒那麼粗暴,只是在有必要的地方進行同步而已,效率較Vector會好一些,但實際上也不會太好,比較同步手段比較單一,只是用內置鎖一種方案而已。
當咱們想要存儲鍵值對或者說是想要表達某種映射關係的時候,都會用到HashMap這個類,HashTable則是HashMap的同步版本,是線程安全的,但效率很低,ConcurrentHashMap是JDK1.5以後替代HashTable的類,效率較高,因此如今在併發環境下通常再也不使用HashTable,而是使用ConcurrentHashMap。
順便說一下,ConcurrentHashMap是在J.U.C包下的,該包的主要做者是Doug Lea,這位大佬幾乎一我的撐起了Java併發技術。
HashMap的內部結構是數組(稱做table)+鏈表(達到閾值會轉換成紅黑樹)的形式。數組和鏈表存儲的元素都是Node,以下所示:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
複製代碼
當向HashMap插入鍵值對的時候,會先拿key進行hash運算,獲得一個hashcode,而後根據hashcode來肯定該鍵值對(最終的形式上實際上是Node)應該放置在table的哪一個位置,這個過程當中若是有Hash衝突,即table中該位置已經有了Node節點A,那麼就會將這個新的鍵值對插入到以A節點爲頭節點的鏈表中(此尾插法,在JDK1.8中改成頭插法),若是在遍歷鏈表的途中遇到key相同的狀況,那麼就直接用新的value值替換到原來的值,這種狀況就再也不建立新的Node了,若是在途中沒有遇到的話,就在最後建立一個Node節點,並將其插入到鏈表末尾。
關於HashMap更多的內容,例如什麼併發擴容致使的問題,以及擴容因子對性能的影響等等,建議網上搜索,網上這樣的文章很是很是多,多到打開一個社區,都TM是將HashMap的文章.....
HashTable的算法實現和HashMap並無太多區別,能夠簡單把HashTable理解成HashMap的線程安全版本,HashTable實現線程安全的手段也是很是粗暴的,和Vector幾乎同樣,直接將絕大多數方法設置成同步方法,以下所示:
public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
for (int i = tab.length ; i-- > 0 ;) {
for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
複製代碼
因此,其效率能夠說是很是低的,通常不多用了,而是使用接下來要講到的ConcurrentHashMap代替。
該類位於java.util.concurrent(簡稱J.U.C)包下,是一款優秀的併發工具類。ConcurrentHashMap內部元素的存儲結構和HashMap幾乎同樣,都是數組+鏈表(達到閾值會轉換成紅黑樹)的結構。不一樣的是,CouncurrentHashMap是線程安全的,但並不像HashTable那樣粗暴的在每一個方法上加入synchronized內置鎖。而是採用一種叫作「分段鎖」的技術,將一整個table數組分紅多個段,每一個段有不一樣的鎖,每一個鎖只能影響到本身所在的段,對其餘段沒有影響,也就是說,在併發環境下,多個線程能夠同時對ConcurrentHashMap的不一樣的段進行操做。效果就是吞吐量提升了,效率也比HashTable高不少,但麻煩的是一些全局性變量不太好保證一致性,例如size。
關於ConcurrentHashMap更多的內容,仍是建議自行查找資料,網上有不少分析ConcurrentHashMap的優秀文章。
其實上面幾個小節都一直有比較,就在這裏總結一下:
Java8中除了lambda表達式,最大的特性就是Stream流了。Stream API能夠將集合看作流,集合中的元素看作一個一個的流元素。這樣的抽象能夠將對集合的操做變得很簡單、清晰,例如在之前要想合併兩個集合,就不得不作建立一個新的集合,而後遍歷兩個集合將元素放入到新的集合中,但用流API的話就很是簡單了,只須要將兩個集合看作兩個流,直接將兩個流合成一個流便可。
Stream API還提供了不少高階函數用於操做流元素,流入map,reduce,filter等,下面是一個使用Stream API的示例:
public void streamTest() {
Random random = new Random();
List<Integer> integers = IntStream.generate(() -> random.nextInt(100))
.limit(100).boxed()
.collect(Collectors.toList());
integers.stream().map(num -> num * 10)
.filter(num -> num % 2 == 0)
.forEach(System.out::println);
}
複製代碼
就這幾行代碼,實際上只能算是三行代碼,就實現了隨機生成元素放入list中,而且作了一個map操做和filter操做,還順帶遍歷了一下List。若是要用之前的方法,就不得不這樣寫:
public void originTest() {
Random random = new Random();
List<Integer> integers = new ArrayList<>();
for (int i = 0; i < 100; i++) {
integers.add(random.nextInt(100));
}
for (int i = 0; i < 100; i++) {
integers.set(i, integers.get(i) * 10); //map
}
for (int i = 0; i < 100; i++) {
if (integers.get(i) % 2 == 0) //filter
System.out.println(integers.get(i)); //foreach
}
}
複製代碼
這三個for循環看起來實在是難看。這就是Stream API的優勢,簡潔,方便,抽象程度高,但可讀性會差一些,若是對lambda和Stream不熟悉的朋友第一次看到可能會比較費勁(但實際上,這是很簡單的代碼)。
那是否是之後對集合的操做都使用Stream API呢?別那麼極端,Stream API確實簡潔,但可讀性不好,Debug難度很是高,更多的時候是靠人肉Debug,並且性能上可能會低於傳統的方法,也有可能高,因此,個人建議是:在使用以前,最後先測試一下,將兩種方案對比一下,最終根據測試結果挑選一個比較好的方案。
集合類是Java開發者必需要掌握的,經過閱讀源碼理解它們比看文章詳解來的更加深入。本文只是簡單的講了幾個經常使用的集合類,還有不少其餘的例如TreeMap,HashSet,TreeSet,Stack都沒有涉及,不是說這些集合類不重要,只是受篇幅限制,沒法一一道來,但願讀者能好好認真看看這些類的源碼,看源碼的時候不須要從頭看到尾,能夠先看幾個經常使用的方法,例如get,put等,而後一步一步跟進去,也可使用調試器單步跟蹤代碼。