這 3 個 Set 集合的實現有點簡單,那來作個總結吧

Set 接口是 Java Collections Framework 中的一員,它的特色是:不能包含重複的元素,容許且最多隻有一個 null 元素。Java 中有三個經常使用的 Set 實現類:html

  • HashSet: 將元素存儲在哈希表中,性能最佳,但不能保證元素的迭代順序
  • LinkedHashSet: 維護一個鏈表貫穿全部元素,按插入順序對元素進行迭代
  • TreeSet: 將元素存儲在一個紅黑樹中,按元素大小排序的序列迭代

JDK 在實現時,這 3 個 Set 集合的核心功能其實分別委託給了: HashMap, LinkedHashMap 和 TreeMap,關於這 3 個 Map 的源碼分析可查看本站發佈的其餘文章。java

接下來對這 3 個 Set 集合的源碼簡單分析,並解決一些面試可能會遇到的問題。面試

HashSet

若是去除註釋,HashSet 源碼也就 200 行左右,除了序列化和克隆的方法,代碼以下:數組

public class HashSet<E> extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
  // 實際存儲元素的對象
  private transient HashMap<E,Object> map;
  
  // 存儲在 HashMap 中全部 key 的共享的 value 值
  private static final Object PRESENT = new Object();
  // 空構造函數
  public HashSet() {
      map = new HashMap<>(); // 0.75f 加載因子
  }
  // 使用已有集合填充並初始化
  public HashSet(Collection<? extends E> c) {
      map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
      addAll(c);
  }
  // 指定關聯 HashMap 的初始容量和加載因子
  public HashSet(int initialCapacity, float loadFactor) {
      map = new HashMap<>(initialCapacity, loadFactor);
  }
  // 只指定初始容量
  public HashSet(int initialCapacity) {
      map = new HashMap<>(initialCapacity);
  }
  // 包訪問權限的構造方法,僅用於 LinkedHashSet 初始化
  // 使用 LinkedHashMap 做爲底層存儲
  HashSet(int initialCapacity, float loadFactor, boolean dummy) {
      map = new LinkedHashMap<>(initialCapacity, loadFactor);
  }
  // HashSet 中的元素就至關於 HashMap 中的 key 
  public Iterator<E> iterator() {
      return map.keySet().iterator();
  }
  
  // 如下這些方法,都是對 Set 接口中定義的方法的實現
  public int size() {
      return map.size();
  }

  public boolean isEmpty() {
      return map.isEmpty();
  }
  
  public boolean contains(Object o) {
      return map.containsKey(o);
  }
  // 全部鍵值對的 value 值都是 PRESENT 這個 Object 對象
  public boolean add(E e) {
      return map.put(e, PRESENT)==null;
  }

  public boolean remove(Object o) {
      return map.remove(o)==PRESENT;
  }

  public void clear() {
      map.clear();
  }
  // JDK 8 提供的一種並行遍歷機制 - 可分割迭代器
  public Spliterator<E> spliterator() {
      return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
  }
}

能夠看到,底層使用 HashMap 用於實際存放數據,而 PRESENT 就是全部寫入 map 的 value 值。實現比較簡單,核心功能都委託給了 HashMap緩存

不論是 Set 仍是 Map,存儲的都是對象,在 Java 中,判斷兩個對象是否相等,都是經過 equalshashCode 兩個方法:安全

  • 兩個對象經過 equals 判斷相等,那麼它們確定返回相同的 hashCode
  • 反之,不要求必須擁有相同的 hashCode

因此,HashSet 存儲的對象,都要正確覆蓋實現 equalshashCode 兩個方法。併發

其實,HashSet 中的元素其實就是 HashMap 的 key,在插入時:ide

  1. 首先計算元素的 hashCode 值,找到底層數組存儲位置
  2. 而後和該位置上的全部元素使用 equals 方法進行比較
  3. 若是都不相等,則插入;不然不插入,本質上這裏作了一次 value 的更新,但 key 不變化。

關於迭代器,就是利用的 HashMap 中的 KeyIterator。函數

LinkedHashSet

LinkedHashSet 的代碼就更簡單了,它繼承自 HashSet,代碼以下:源碼分析

public class LinkedHashSet<E> extends HashSet<E>
  implements Set<E>, Cloneable, java.io.Serializable {
  // 調用父類特定的構造方法,初始一個 LinkedHashMap
  public LinkedHashSet(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor, true);
  }

  public LinkedHashSet(int initialCapacity) {
    super(initialCapacity, .75f, true);
  }

  public LinkedHashSet() {
    super(16, .75f, true);
  }

  public LinkedHashSet(Collection<? extends E> c) {
    super(Math.max(2*c.size(), 11), .75f, true);
    addAll(c);
  }

  @Override
  public Spliterator<E> spliterator() {
    return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
  }
}

所有代碼就這些,值得注意的是構造方法中的 super 調用的是 HashSet 中的一個默認包訪問權限的構造方法,核心功能都委託給了 LinkedHashMap。

像 HashSet 那樣,它能在常量時間內完成集合的基本操做 add, contains 和 remove。性能略低於 HashSet,由於要額外維護一個鏈表。但有一個例外,在遍歷時,LinkedHashSet 花費的時間與元素個數成比例,而 HashSet 花費時間較多,由於它與集合容量成比例。

TreeSet

TreeSet 是一個有序的 Set 集合,元素大小比較方式能夠是天然順序,也能夠指定一個 Comparator 比較器。

它是對 TreeMap 的封裝,提供了在有序集合上的遍歷 API 好比,lower、floor、ceiling 和 higher 分別返回小於、小於等於、大於等於、大於給定元素的元素。能在 log(n) 時間內完成集合的基本操做 add, contains 和 remove。

有一點能夠了解下,Set 接口定義的是使用 equals 方法比較元素是否相等,而 TreeSet 使用則是 compareTo 或者 compare 方法進行比較,這知足集合的行爲,只不過沒有遵照 Set 接口的規範。

TreeSet 源碼也比較簡單,畢竟只是對 TreeMap 封裝了一下,這裏再也不貼出。

經常使用集合面試問題總結

以前分析了一部分經常使用集合的源碼,這些集合都各有各的特色,它們的區別也常常出如今面試中,本文最後就對常見的面試題進行下總結。

ArrayList 與 LinkedList 有什麼區別?

  • 存儲結構不一樣,ArrayList 底層使用數組;LinkedList 使用雙向鏈表
  • 性能上,ArrayList 可以隨機訪問,但增長和刪除效率較慢,涉及到內存拷貝;LinkedList 只能順序或逆序訪問,佔用內存稍大,但插入刪除效率高
  • LinkedList 還能當作棧和隊列來使用
  • 二者均與容許存儲 null 也容許存儲重複元素
  • 二者都是線程不安全的,均可以使用 Collections.synchronizedList(List<E> list) 方法生成一個線程安全的 List

ArrayList 與 Vector 有什麼區別?

  • ArrayList 非線程安全,Vector 線程安全
  • 擴容時,ArrayList 增長 1.5 倍的容量 ; Vector 增長 2倍的容量

JDK 8 對 HashMap 作了哪些優化?

  • 底層結構改成單鏈表 + 數組 + 紅黑樹的存儲結構,在有大量哈希衝突時,將查詢時間複雜度從 O(n) 降爲 O(log(n))
  • 優化哈希函數,將 1.7 中的4次位運算 + 5次異或運算,下降到1次位運算 + 1次異或運算
  • 優化擴容機制,1.7 中會從新哈希計算新的位置,而 1.8 則是根據2的次冪擴展機制,不從新計算位置,只根據原散列值計算偏移量,要麼位置不變,要麼偏移舊數組容量的偏移量

HashMap 和 HashTable 的區別

  • HashMap 線程不安全 ; HashTable 線程安全
  • HashMap 容許 key 和 Vale 爲 null ; HashTable 不容許 key、value 爲 null
  • HashMap 默認容量爲 2^4 且容量必定是 2^n ; HashTable 默認容量是11(素數), 不必定是 2^n
  • HashTable 直接使用模運算計算哈希桶下標 ; HashMap 使用 & 位運算 進行優化

HashMap 和 LinkedHashMap 的區別

  • LinkedHashMap 繼承自 HashMap 它們有相同的存儲結構和擴容機制
  • LinkedHashMap 內部須要額外維護一個鏈表
  • LinkedHashMap 按插入順序對元素進行迭代 ; 而 HashMap 迭代順序不可預測
  • LinkedHashMap 可按按訪問順序遍歷元素,用於構建 LRU 緩存

什麼是 fast-fail,原理是什麼?

fast-fail,即快速失敗,在遍歷集合的過程當中,若是發現集合結構發生了變化,會拋出 ConcurrentModificationException 運行時異常。

注意,在不一樣步修改的狀況下,它不能保證會發生,它只是盡力檢測併發修改的錯誤。

原理是經過一個 modCount 字段來實現的,這個字段記錄了列表結構的修改次數,當調用 iterator() 返回迭代器時,會緩存 modCount 當前的值,若是這個值發生了不指望的變化,那麼就會在 next, remove 操做中拋出異常。

小結

本文以及以前介紹的集合都是常規的,經常使用的,非線程安全的集合實現,接下來將會介紹 Java 併發包下的線程安全的集合,以及一些有特殊用途的集合實現。

原文出處:https://www.cnblogs.com/wskwbog/p/11260056.html

相關文章
相關標籤/搜索