搞懂 HashSet & LinkedHashSet 源碼以及集合常見面試題目

HashSet & LinkedHashSet 源碼分析以及集合常見面試題目

通過上兩篇的 HashMapLinkedHashMap 源碼分析之後,本文將繼續分析 JDK 集合之 Set 源碼,因爲有了以前的 Map 源碼分析的鋪墊,Set 源碼就簡單不少了,本文的篇幅也將比以前短不少。查看 Set 源碼的構造參數就能夠知道,Set 內部其實維護的就是一個 Map,只是單單使用了 Entry 中的 key 。那麼本文將再也不贅述內部數據結構,而是經過部分的源碼,來講明兩個 Set 集合與 Map 之間的關係。本文將從如下幾部分敘述:java

  1. Set 集合概述
  2. HashSet 源碼簡單分析
  3. LinkedHashSet 源碼簡單分析
  4. 關於面試中的集合問題總結

Set 集合概述

 

 

圖片來自互聯網侵刪面試

因爲本篇文章主要敘述 Set 容器以及和 Map 容器之間關係,咱們只須要關注上述集合圖譜中 Set 部分。能夠看出 Set 主要的實現類有 HashSetTreeSet 以及沒有畫出的 LinkedHashSet。其中 HashSet 的實現依賴於 HashMapTreeSet 的實現依賴於 TreeMapLinkedHashSet 的實現依賴於 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 集合的特色數據結構

  1. HashSet 底層是數組 + 單鏈表 + 紅黑樹的數據結構
  2. LinkedHashSet 底層是 數組 + 單鏈表 + 紅黑樹 + 雙向鏈表的數據結構
  3. Set 不容許存儲重複元素,容許存儲 null
  4. HashSet 存儲元素是無序且不等於訪問順序
  5. LinkedHashSet 存儲元素是無序的,可是因爲雙向鏈表的存在,迭代時獲取元素的順序等於元素的添加順序,注意這裏不是訪問順序

HashSet 的源碼分析

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多行不信繼續往下看。

LinkedHashSet 源碼分析

在上述分析 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 區別 ?
  1. 存儲結構上 ArrayList 底層使用數組進行元素的存儲,LinkedList 使用雙向鏈表做爲存儲結構。

  2. 二者均與容許存儲 null 也容許存儲重複元素。

  3. 在性能上 ArrayList 在存儲大量元素時候的增刪效率 平均低於 LinkedList,由於 ArrayList 在增刪的是須要拷貝元素到新的數組,而 LinkedList 只須要將節點先後指針指向改變。

  4. 在根據角標獲取元素的時間效率上ArrayList優於 LinkedList,由於數組自己有存儲連續,有 index 角標,而 LinkedList 存儲元素離散,須要遍歷鏈表。

  5. 不要使用 for 循環去遍歷 LinkedList 由於效率很低。

  6. 二者都是線程不安全的,均可以使用 Collections.synchronizedList(List<E> list) 方法生成一個線程安全的 List。

  • ArrayList 與 Vector 區別(爲何要用Arraylist取代Vector呢?)
  1. ArrayList 的擴容機制因爲 Vector , ArrayList 每次 resize 增長 1.5 倍的容量,Vector 每次增長 2倍的容量,在存儲大量元素後擴容的時候就能有很大的空間節省。
  2. Vector 添加刪除方法以及迭代器遍歷的方法都是 synchronized 修飾的方法,在線程安全的狀況下使用效率低於 ArrayList
  3. ArrayList 和 LinkedList 經過Collections.synchronizedList(List<E> list) 的線程同步的集合,迭代器並不一樣步,須要使用者去加鎖。
  • 簡述 HashMap 的工做原理 JDK 1.8後作了哪些優化

    1. JDK 1.7 HashMap 底層採用單鏈表 + 數組的存儲結構存儲元素(鍵值對)。JDK1.8以後 HashMap 在同一哈希桶中節點數量(單鏈表長度)超過 8以後會使用 紅黑樹替換單鏈表來提升效率
    2. HashMap 經過鍵值對的 key 的 hashCode 值通過擾動函數處理後肯定存儲的數組角標位置,1.7 中擾動函數使用了 4次位運算 + 5次異或運算,1.8 中下降到 1次位運算 + 1次異或運運算
    3. HashMap 擴容的時候會增長原來數組長度兩倍,並對所存儲的元素節點hash 值的從新計算,1.7中 HashMap 會從新調用 hash 函數計算新的位置,而 1.8中對此進行了優化經過 (e.hash & oldCap) == 0 來肯定節點新位置是位於擴容前的角標仍是以前的 2倍角標位置。
    4. HashMap 在多線程使用前提下,擴容的時候可能會致使循環鏈表的狀況,固然咱們不該在線程不安全的狀況下使用 HashMap
  • HashMap 和 HashTable 的區別

    1. HashMap 是線程不安全的,HashTable是線程安全的。

    2. HashMap 容許 key 和 Vale 是 null,可是隻容許一個 key 爲 null,且這個元素存放在哈希表 0 角標位置。 HashTable 不容許key、value 是 null

    3. HashMap 內部使用hash(Object key)擾動函數對 key 的 hashCode 進行擾動後做爲 hash 值。HashTable 是直接使用 key 的 hashCode() 返回值做爲 hash 值。

    4. HashMap默認容量爲 2^4 且容量必定是 2^n ; HashTable 默認容量是11,不必定是 2^n

    5. HashTable 取哈希桶下標是直接用模運算,擴容時新容量是原來的2倍+1。HashMap 在擴容的時候是原來的兩倍,且哈希桶的下標使用 &運算代替了取模。

  • HashMap 和 LinkedHashMap 的區別

  1. LinkedHashMap 擁有與 HashMap 相同的底層哈希表結構,即數組 + 單鏈表 + 紅黑樹,也擁有相同的擴容機制。

  2. LinkedHashMap 相比 HashMap 的拉鍊式存儲結構,內部額外經過 Entry 維護了一個雙向鏈表。

  3. HashMap 元素的遍歷順序不必定與元素的插入順序相同,而 LinkedHashMap 則經過遍歷雙向鏈表來獲取元素,因此遍歷順序在必定條件下等於插入順序。

  4. LinkedHashMap 能夠經過構造參數 accessOrder 來指定雙向鏈表是否在元素被訪問後改變其在雙向鏈表中的位置。

  • HashSet 如何檢查重複,與 HashMap 的關係?
  1. HashSet 內部使用 HashMap 存儲元素,對應的鍵值對的鍵爲 Set 的存儲元素,值爲一個默認的 Object 對象。
  2. HashSet 經過存儲元素的 hashCode 方法和 equals 方法來肯定元素是否重複。
  • 是否瞭解 fast-fail 規則 簡單說明一下
  1. 快速失敗(fail—fast)在用迭代器遍歷一個集合對象時,若是遍歷過程當中集合對象中的內容發生了修改(增長、刪除、修改),則會拋出ConcurrentModificationException

  2. 迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個 modCount 變量。集合在被遍歷期間若是內容發生變化,就會改變 modCount 的值。每當迭代器使用hasNext()/next() 遍歷下一個元素以前,都會檢測 modCount 變量是否爲expectedmodCount 值,是的話就返回遍歷值;不然拋出異常,終止遍歷。

  3. 場景:java.util包下的集合類都是快速失敗的,不能在多線程下發生併發修改(迭代過程當中被修改)。

  • 集合在遍歷過程當中是否能夠刪除元素,爲何迭代器就能夠安全刪除元素
  1. 集合在使用 for 循環或者高級 for 循環迭代的過程當中不容許使用,集合自己的 remove 方法刪除元素,若是進行錯誤操做將會致使 ConcurrentModificationException異常的發生

  2. Iterator 能夠刪除訪問的當前元素(current),一旦刪除的元素是Iterator 對象中 next 所正在引用的,在 Iterator 刪除元素經過 修改 modCount 與 expectedModCount 的值,可使下次在調用 remove 的方法時候二者仍然相同所以不會有異常產生。

總結

本文分析了 JDK 中 HashSetLinkedHashSet 的源碼實現,闡述了Set 與 Map 的關係,也經過最後一節的面試題總結複習了一下以前幾篇源碼分析文章的知識點。以後可能會繼續分析一下 Android 中特有的 ArrayMapSparseArray 源碼分析。

集合源碼分析文章目錄,歡迎你們查看。

  1. 搞懂 Java HashMap 源碼
  2. 搞懂 Java LinkedHashMap 源碼
  3. 搞懂 Java ArrayList 源碼
  4. 搞懂 Java LinkedList 源碼
  5. 搞懂 Java equals 和 hashCode 方法
  6. Java List 容器源碼分析的補充
相關文章
相關標籤/搜索