1. 集合類庫
通常,程序總是根據運行時才知道的某些條件去創建新對象,在此之前,不會知道所需對象的數量,甚至不知道確切的類型。為了解決這個普遍的編程問題,需要在任意時刻和任意位置創建任意數量的對象。java 實用類庫提供了一套相當完整的集合類來解決這個問題 。其中基本類型是List、Set、Queue和Map,這些對象被稱為集合類。
這里給出一個經常引用的一個類庫關系圖:
從圖中可以看出,Java集合分為Collection和Map兩大種。下面就兩大類型進行分別說明:
2. Collection
查看jdk源碼看到Collection接口定義了集合分支的基礎方法,有查詢方法,修改集合方法,批量操作方法和比較與hash方法,這些都是集合的基礎方法。其繼承接口可以分為以下幾類:
- List: 有序集合,值允許有重復
- Set:無需集合,值不允許有重復
- Queue:保持先進先出順序
2.1 List集合: ArrayList&LinkedList
ArrayList底層通過數組實現,數組因為可以通過下標訪問成為一個隨機訪問第n個數效率很高的數據結構,隨機訪問查詢的時間復雜度是O(1),而刪除/增加新的元素到某個位置時,需要一個一個的移動數組中的元素直至適合的位置,所以時間復雜度是O(n);
LinkedList底層由雙向鏈表實現,在鏈表中插入元素時,只需要改變插入位置前后結點的結點指向和添加新元素的結點指向即可,時間復雜度是O(1),在訪問元素的時候,無論是隨機方位第幾位的元素還是查詢某個定值時,鏈表的時間復雜度均為O(n)。
所以,ArrayList適用于隨機訪問的場景,而LinkedList則適用于頻繁隨機位置刪除和插入的場景。
2.2 Set集合: HashSet&TreeSet
HashSet:封裝了一個 HashMap 對象來存儲所有的集合元素,所有放入 HashSet 中的集合元素實際上由 HashMap 的 key 來保存,而 HashMap 的 value 則存儲了一個 PRESENT,它是一個靜態的 Object 對象。 HashSet 的絕大部分方法都是通過調用 HashMap 的方法來實現的,具體原理在HashMap分析。
TreeSet:TreeSet是基于TreeMap實現的。TreeSet中的元素支持2種排序方式:自然排序 或者 根據創建TreeSet 時提供的 Comparator 進行排序。這取決于使用的構造方法,具體原理在TreeMap分析。
2.3 Queue: PriorityQueue
優先級隊列,元素可以按照任意的順序插入,但總是按照排序的順序進行檢索,內部實現的數據結構是堆。堆是一個可以自我調整的二叉樹,對樹執行添加和刪除的時候,可以讓最小的元素移動到根,而不用花費時間對元素進行排序。使用的典型實例是任務調度場景。
3. Map
表示鍵值對,鍵必須是唯一的,不能對同一個鍵存放兩個值。
3.1 HashMap
HashMap底層實現上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。具體如下圖所示:
從上圖我們可以看出HashMap底層實現還是數組,只是數組的每一項都是一條鏈。其中數組和鏈表在jdk源碼中體現:
/**The table, resized as necessary.
Length MUST Always be a power of two. */
// table對象即是上圖中對應的數組
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// 而內部類Entry則是代表了鏈表上的沒給節點
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
3.1.1 如何插入一個值
HashMap插入一個值包括以下幾個步驟:
- 調用hash計算key的hash值
- 根據hash值推算出放在數組的位置
- 找到具體數組某個位置,遍歷該位置的Entry鏈表,比較key值如果相等則直接替換value,如果不同則插入鏈表的尾部。
可以看出,如果hashCode()和hash()足夠好,盡可能的減少沖突,那么對HashMap的訪問等價于對數組的隨機方位,如果不夠好有大量沖突存在,則退化為對鏈表的隨機方位。
3.1.2 再散列rehash
當哈希表的容量超過默認容量時,必須調整table的大小。當容量已經達到最大可能值時,那么該方法就將容量調整到Integer.MAX_VALUE返回,這時,需要創建一張新表,將原表的映射到新表中。
3.1.3 容量參數
可以看出整個再散列過程是比較耗時的,需要將所有老數據重新計算后放到新的散列表中。HashMap維護一個threshold變量,它始終被定義為當前數組總容量和負載因子的乘積,他表示的是HashMap的閾值,當超過該值,HashMap便會擴容。
關于負載因子定義首先看一下HashMap的constructor如下:
public HashMap(int initalCapacity);
public HashMap(int initalCapacity, float loadFactor);
其中initalCapacity 表示初始化容量,loadFactor表示負載因子一般是介于0和1之間,它決定了HashMap擴容之前,其內部數組的填充度。默認情況下,初始量為16,負載因子為0.75。
負載因子 = 元素個數/內部數組總大小
可以看出如果負載因子大于1,一定會存在沖突,元素個數大于數組的容量。
3.1.4 如何取一個值
查看jdk源碼可以看出從HashMap中獲取一個值分為兩種情況當key為null的時候單獨處理,非null的時候一套處理邏輯。這里也提醒我們HashMap可以存放key為null的鍵值對。
查看獲取非null的key的值的具體實現
分析查找一個非null的鍵流程:
- 調用hash計算key的hash值
- 根據hash值推算出放在數組的位置
- 遍歷對應位置上的鏈表找到key相同的Entry對象返回即可,找不到則返回為null
3.2 TreeMap
HashMap和LinkedHashMap底層存儲容器都是選擇了數組,內容為內部類Entry。但是TreeMap底層通過一顆紅黑樹來維護,初始化的時候有個root根節點,同時TreeMap不允許key為null。TreeMap 本質上就是一棵“紅黑樹”,而 TreeMap 的每個 Entry 就是該紅黑樹的一個節點。
3.2.1 紅黑樹
排序二叉樹要么是一棵空二叉樹,要么是具有下列性質的二叉樹:
- 若它的左子樹不空,則左子樹上所有節點的值均小于它的根節點的值;
- 若它的右子樹不空,則右子樹上所有節點的值均大于它的根節點的值;
- 它的左、右子樹也分別為排序二叉樹。
排序二叉樹雖然可以快速檢索,但在最壞的情況下:如果插入的節點集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉樹將變成鏈表。
紅黑樹在原有的排序二叉樹增加了如下幾個要求:
- 性質 1:每個節點要么是紅色,要么是黑色。
- 性質 2:根節點永遠是黑色的。
- 性質 3:所有的葉節點都是空節點(即 null),并且是黑色的。
- 性質 4:每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的路徑上不會有兩個連續的紅色節點)
- 性質 5:從任一節點到其子樹中每個葉子節點的路徑都包含相同數量的黑色節點。
上面的性質 3 中指定紅黑樹的每個葉子節點都是空節點,而且并葉子節點都是黑色。但 Java 實現的紅黑樹將使用 null 來代表空節點,因此遍歷紅黑樹時將看不到黑色的葉子節點,反而看到每個葉子節點都是紅色的。
4. 迭代器(Iterator)
迭代器是一種設計模式,在java里它是一個對象,可以遍歷并選擇序列中的對象,而開發人員不需要了解該序列的底層結構。迭代器通常被稱為“輕量級”對象,因為創建它的代價小。迭代器接口Iterable是Collection類的父接口。它只有一個方法: iterator(),方法返回一個代表當前集合對象的泛型<T>迭代器,用于之后的遍歷操作。所有的Collection集合對象都實現這個Iterable接口,允許使用foreach進行遍歷。
常用Iterator遍歷方式
List<Integer> lstint = new ArrayList<Integer>();
// Iterator遍歷一
Iterator<Integer> iterator = lstint.iterator();
while (iterator.hasNext()){
int i = iterator.next();
System.out.println(i);
}
// Iterator遍歷二
for (Iterator<Integer> it = lstint.iterator(); it.hasNext();){
int i = it.next();
System.out.println(i);
}
5. Fail-Fast機制
前面敘述的集合類均不是線程安全的,所以java集合(Collection)規定在使用迭代器的過程中有其他線程修改了同一個集合類,那么將拋出ConcurrentModificationException,這就是所謂fail-fast策略。
注意點:迭代器的快速失敗行為無法得到保證,迭代器的快速失敗行為應該僅用于檢測 bug。
例如:當某一個線程A通過iterator去遍歷某集合的過程中,若該集合的內容同時被其他線程所改變了;那么線程A訪問集合時,就會拋出ConcurrentModificationException異常,產生fail-fast事件。
5.1 實現原理
這里以HashMap為例進行說明,其他集合類實現類似,這一策略在源碼中的實現是通過modCount域,modCount顧名思義就是修改次數,對HashMap內容的修改都將增加這個值,具體定義如下:
HashMap在put函數添加元素時修改此值代碼如下:
在迭代器初始化過程中會將這個值賦給迭代器的expectedModCount,在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經有其他線程修改了Map,將會拋出ConcurrentModificationException異常,具體代碼如下:
5.2 遍歷刪除指定元素
在實際應用中,我們常會遇到一種需求是從集合中刪除符合某個特定條件的元素,對比下面幾種寫法:
/**
* 使用增強的for循環
* 在循環過程中從List中刪除非基本數據類型以后,此時modCount被修改,
* 繼續遍歷List,此時expectCount仍然是初始化時值比較不相等,
* 會報ConcurrentModificationException
*/
public void listRemove() {
List<Student> students = this.getStudents();
for (Student stu : students) {
if (stu.getId() == 2)
students.remove(stu);
}
}
改寫法如注釋中描述由于remove操作導致modCount發生變化,繼續遍歷就會觸發Fail-Fast機制。這種寫法如果確定只刪除其中一個,此時break掉程序不會拋異常。
/**
* 該寫法不使用增強的for循環意味著不會使用Iterator相關方法,
* 這樣也不會涉及到modCount比較判斷的問題,
* 但是數據不一定是正確的,這主要是因為刪除元素后,被刪除元素后
* 的元素索引發生了變化。假設被遍歷list中共有10個元素,當
* 刪除了第3個元素后,第4個元素就變成了第3個元素了,第5個就變成
* 了第4個了,但是程序下一步循環到的索引是第4個,
* 這時候取到的就是原本的第5個元素了。
*/
public void listRemove2() {
List<Student> students = this.getStudents();
for (int i=0; i<students.size(); i++) {
if (students.get(i).getId()%3 == 0) {
Student student = students.get(i);
students.remove(student);
}
}
}
該種寫法通過遍歷數組的形式,在刪除過程中導致數組元素減少位置發生變化影響了最后的結果。
/**
* 使用Iterator的方式可以順利刪除和遍歷
*/
public void iteratorRemove() {
List<Student> students = this.getStudents();
Iterator<Student> stuIter = students.iterator();
while (stuIter.hasNext()) {
Student student = stuIter.next();
if (student.getId() % 2 == 0)
//這里要使用Iterator的remove方法移除當前對象,
//使用List的remove方法,則同樣會出現 ConcurrentModificationException
stuIter.remove();
}
}
推薦寫法使用Iterator遍歷并使用迭代器對應的remove方法刪除。該刪除方法會同步size大小以及修改expectCount。
6. 總結
簡單總結上述各種類使用和實現特點,如下表所示: