通過上兩篇的 HashMap 和 LinkedHashMap 源碼分析之後,本文將繼續分析 JDK 集合之 Set 源碼,因爲有了以前的 Map 源碼分析的鋪墊,Set 源碼就簡單不少了,本文的篇幅也將比以前短不少。查看 Set 源碼的構造參數就能夠知道,Set 內部其實維護的就是一個 Map,只是單單使用了 Entry 中的 key 。那麼本文將再也不贅述內部數據結構,而是經過部分的源碼,來講明兩個 Set 集合與 Map 之間的關係。本文將從如下幾部分敘述:java
圖片來自互聯網侵刪面試
因爲本篇文章主要敘述 Set 容器以及和 Map 容器之間關係,咱們只須要關注上述集合圖譜中 Set 部分。能夠看出 Set 主要的實現類有 HashSet
和 TreeSet
以及沒有畫出的 LinkedHashSet
。其中 HashSet
的實現依賴於 HashMap
, TreeSet
的實現依賴於 TreeMap
,LinkedHashSet
的實現依賴於 LinkedHashMap
。數組
從各個實現類的聲明也能夠看出其繼承關係安全
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable 複製代碼
在看 Set 的源碼以前,咱們先歸納的說下 Set 集合的特色數據結構
HashSet 源碼只有短短的 300 行,上文也闡述了實現依賴於 HashMap,這一點充分體如今其構造方法和成員變量上。咱們來看下 HashSet 的構造方法和成員變量:多線程
// HashSet 真實的存儲元素結構 private transient HashMap<E,Object> map; // 做爲各個存儲在 HashMap 元素的鍵值對中的 Value private static final Object PRESENT = new Object(); //空參數構造方法 調用 HashMap 的空構造參數 //初始化了 HashMap 中的加載因子 loadFactor = 0.75f public HashSet() { map = new HashMap<>(); } //指按期望容量的構造方法 public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } //指按期望容量和加載因子 public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } //使用指定的集合填充Set public HashSet(Collection<? extends E> c) { //調用 new HashMap<>(initialCapacity) 其中初始指望容量爲 16 和 c 容量 / 默認 load factor 後 + 1的較大值 map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } // 該方法爲 default 訪問權限,不容許使用者直接調用,目的是爲了初始化 LinkedHashSet 時使用 HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); } 複製代碼
經過 HashSet 的構造參數咱們能夠看出每一個構造方法,都調用了對應的 HashMap 的構造方法用來初始化成員變量 map ,所以咱們能夠知道,HashSet 的初始容量也爲 1<<4
即16,加載因子默認也是 0.75f。併發
咱們都知道 Set 不容許存儲重複元素,又由構造參數得出結論底層存儲結構爲 HashMap,那麼這個不可重複的屬性必然是有 HashMap 中存儲鍵值對的 Key 來實現了。在分析 HashMap 的時候,提到過 HashMap 經過存儲鍵值對的 Key 的 hash 值(通過擾動函數hash()處理後)來決定鍵值對在哈希表中的位置,當 Key 的 hash 值相同時,再經過 equals 方法判讀是不是替換原來對應 key 的 Value 仍是存儲新的鍵值對。那麼咱們在使用 Set 方法的時候也必須保證,存儲元素的 HashCode 方法以及 equals 方法被正確覆寫。函數
HashSet 中的添加元素的方法也很簡單,咱們來看下實現:源碼分析
public boolean add(E e) { return map.put(e, PRESENT)==null; } 複製代碼
能夠看出 add 方法調用了 HashMap
的 put 方法,構造的鍵值對的 key 爲待添加的元素,而 Value 這時有全局變量 PRESENT
來充當,這個PRESENT
只是一個 Object 對象。post
除了 add 方法外 HashSet
實現了 Set 接口中的其餘方法這些方法有:
public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean contains(Object o) { return map.containsKey(o); } //調用 remove(Object key) 方法去移除對應的鍵值對 public boolean remove(Object o) { return map.remove(o)==PRESENT; } public void clear() { map.clear(); } // 返回一個 map.keySet 的 HashIterator 來做爲 Set 的迭代器 public Iterator<E> iterator() { return map.keySet().iterator(); } 複製代碼
關於迭代器咱們在講解 HashMap
中的時候沒有詳細列舉,其實 HashMap
提供了多種迭代方法,每一個方法對應了一種迭代器,這些迭代器包括下述幾種,而 HashSet
因爲只關注 Key 的內容,因此使用 HashMap
的內部類 KeySet
返回了一個 KeyIterator
,這樣在調用 next 方法的時候就能夠直接獲取下個節點的 key 了。
//HashMap 中的迭代器 final class KeyIterator extends HashIterator implements Iterator<K> { public final K next() { return nextNode().key; } } final class ValueIterator extends HashIterator implements Iterator<V> { public final V next() { return nextNode().value; } } final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> { public final Map.Entry<K,V> next() { return nextNode(); } } 複製代碼
關於 HashSet 中的源碼分析就這些,其實除了一些序列化和克隆的方法之外,咱們已經列舉了全部的 HashSet 的源碼,有沒有感受巨簡單,其實下面的 LinkedHashSet 因爲繼承自 HashSet 使得其代碼更加簡單隻有短短100多行不信繼續往下看。
在上述分析 HashSet
構造方法的時候,有一個 default 權限的構造方法沒有講,只說了其跟 LinkedHashSet 構造有關係,該構造方法內部調用的是 LinkedHashMap 的構造方法。
LinkedHashMap 較之 HashMap 內部多維護了一個雙向鏈表用來維護元素的添加順序:
// dummy 參數沒有做用這裏能夠忽略 HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); } //調用 LinkedHashMap 的構造方法,該方法初始化了初始起始容量,以及加載因子, //accessOrder = false 即迭代順序不等於訪問順序 public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } 複製代碼
LinkedHashSet的構造方法一共有四個,統一調用了父類的 HashSet(int initialCapacity, float loadFactor, boolean dummy)
構造方法。
//初始化 LinkedHashMap 的初始容量爲誒 16 加載因子爲 0.75f public LinkedHashSet() { super(16, .75f, true); } //初始化 LinkedHashMap 的初始容量爲 Math.max(2*c.size(), 11) 加載因子爲 0.75f public LinkedHashSet(Collection<? extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); addAll(c); } //初始化 LinkedHashMap 的初始容量爲參數指定值 加載因子爲 0.75f public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); } //初始化 LinkedHashMap 的初始容量,加載因子爲參數指定值 public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); } 複製代碼
完了..沒錯,LinkedHashSet 源碼就這幾行,因此能夠看出其實現依賴於 LinkedHashMap 內部的數據存儲結構。
以前分析了多篇關於 JDK 集合源碼的文章,而這些集合源碼中的知識點都是面試的時候常客,所以在本篇結尾做爲 "充數" 的一節,咱們來以面試題的形式總結一下以前所分過的源碼中的知識點,這些知識點在以前的文章中都有詳細的分析,若是有疑問能夠回顧一下以前的源碼分析文章。
存儲結構上 ArrayList 底層使用數組進行元素的存儲,LinkedList 使用雙向鏈表做爲存儲結構。
二者均與容許存儲 null 也容許存儲重複元素。
在性能上 ArrayList 在存儲大量元素時候的增刪效率 平均低於 LinkedList,由於 ArrayList 在增刪的是須要拷貝元素到新的數組,而 LinkedList 只須要將節點先後指針指向改變。
在根據角標獲取元素的時間效率上ArrayList優於 LinkedList,由於數組自己有存儲連續,有 index 角標,而 LinkedList 存儲元素離散,須要遍歷鏈表。
不要使用 for 循環去遍歷 LinkedList 由於效率很低。
二者都是線程不安全的,均可以使用 Collections.synchronizedList(List<E> list)
方法生成一個線程安全的 List。
Collections.synchronizedList(List<E> list)
的線程同步的集合,迭代器並不一樣步,須要使用者去加鎖。簡述 HashMap 的工做原理 JDK 1.8後作了哪些優化
(e.hash & oldCap) == 0
來肯定節點新位置是位於擴容前的角標仍是以前的 2倍角標位置。HashMap 和 HashTable 的區別
HashMap
是線程不安全的,HashTable是線程安全的。
HashMap
容許 key 和 Vale 是 null,可是隻容許一個 key 爲 null,且這個元素存放在哈希表 0 角標位置。 HashTable
不容許key、value 是 null
HashMap
內部使用hash(Object key)
擾動函數對 key 的 hashCode
進行擾動後做爲 hash
值。HashTable
是直接使用 key 的 hashCode()
返回值做爲 hash 值。
HashMap
默認容量爲 2^4 且容量必定是 2^n ; HashTable
默認容量是11,不必定是 2^n
HashTable
取哈希桶下標是直接用模運算,擴容時新容量是原來的2倍+1。HashMap
在擴容的時候是原來的兩倍,且哈希桶的下標使用 &運算代替了取模。
HashMap 和 LinkedHashMap 的區別
LinkedHashMap 擁有與 HashMap 相同的底層哈希表結構,即數組 + 單鏈表 + 紅黑樹,也擁有相同的擴容機制。
LinkedHashMap 相比 HashMap 的拉鍊式存儲結構,內部額外經過 Entry 維護了一個雙向鏈表。
HashMap 元素的遍歷順序不必定與元素的插入順序相同,而 LinkedHashMap 則經過遍歷雙向鏈表來獲取元素,因此遍歷順序在必定條件下等於插入順序。
LinkedHashMap 能夠經過構造參數 accessOrder 來指定雙向鏈表是否在元素被訪問後改變其在雙向鏈表中的位置。
fast-fail
規則 簡單說明一下快速失敗(fail—fast)在用迭代器遍歷一個集合對象時,若是遍歷過程當中集合對象中的內容發生了修改(增長、刪除、修改),則會拋出ConcurrentModificationException
。
迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個 modCount
變量。集合在被遍歷期間若是內容發生變化,就會改變 modCount
的值。每當迭代器使用hasNext()/next()
遍歷下一個元素以前,都會檢測 modCount
變量是否爲expectedmodCount
值,是的話就返回遍歷值;不然拋出異常,終止遍歷。
場景:java.util包下的集合類都是快速失敗的,不能在多線程下發生併發修改(迭代過程當中被修改)。
集合在使用 for 循環或者高級 for 循環迭代的過程當中不容許使用,集合自己的 remove 方法刪除元素,若是進行錯誤操做將會致使 ConcurrentModificationException
異常的發生
Iterator 能夠刪除訪問的當前元素(current),一旦刪除的元素是Iterator 對象中 next 所正在引用的,在 Iterator 刪除元素經過 修改 modCount 與 expectedModCount 的值,可使下次在調用 remove 的方法時候二者仍然相同所以不會有異常產生。
本文分析了 JDK 中 HashSet
和 LinkedHashSet
的源碼實現,闡述了Set 與 Map 的關係,也經過最後一節的面試題總結複習了一下以前幾篇源碼分析文章的知識點。以後可能會繼續分析一下 Android 中特有的 ArrayMap
和 SparseArray
源碼分析。
集合源碼分析文章目錄,歡迎你們查看。