Java集合歸納-<二>Set

Set概要

上一篇說過,Set是Collection的實現之一,在這裏我可以說其實Set和Collection基本相同,只是需要注意Set不得存放重複元素即可。下面詳細說一下Set在日常開發中用的最多的兩種實現,HashSet和TreeSet。

HashSet

特徵

HashSet正如其名,因爲是按照Hash算法來存儲集合中的元素,所以HashSet具有良好的存取和查找性能。還有一點要說的是,感覺很多人認爲查詢效率和集合是有序還是無序有直接關係,其實不然,關鍵還是看集合的數據結構和實現算法。HashSet是無序的並不影響他查詢性能出色,畢竟Hash算法的誕生就是爲了用於快速查找和加密。

HashSet主要有以下三個特點:

  • 無序
  • 非線程同步
  • 集合元素值可以爲null(有且僅允許有一個)

等價條件-equals()和hashCode()

這是經典的關於採用hash算法實現的集合逃不開的問題。首先說明HashSet判斷兩個元素相等的標準是兩個對象通過equals()方法比較相等,並且兩個對象的hashCode()方法也相等,即:

equals && hashCode

所以我們應該注意,當把一個對象放入HashSet中時,如果需要重寫equals()方法,重新設定其返回true的條件的話,那麼我們也應該重寫hashCode()方法,以保證當equals()爲true時hashCode()也爲true。

那麼爲什麼要遵循這個規則呢?

  1. 當hashCode()爲true,equals()爲false。
    此時,由於hashCode()爲true,HashSet會認爲兩個元素存放的地址相同,這就產生了“hash衝突”。我們知道hash表裏存儲元素的位置被稱爲bucket(桶),理想情況下,每個桶中存放一個元素擁有最好的性能。現在產生了hash值上的衝突,那麼這個桶中就會存放兩(多)個元素,桶就會產生鏈式結構來存放這多個hash衝突的元素,會大大降低性能。

  2. 當hashCode()爲false,equals()爲true。
    這種情況就不僅僅是性能下降的問題了,因爲hash值不一樣,HashSet會把這兩個對象存放在不同的位置,從而使兩個對象都添加成功,但是,這就從根本特性上違背了Set的不可重複性。

重寫hashCode()的規則

通過前面的講解,我們知道了hashCode()方法的重要性,他是HashSet存放和查找元素時決定其物理地址的唯一標準,下面說一說針對集合存放的對象重寫其hashCode() 方法時需要注意的幾個點:

  • equals()方法返回true時,兩個對象的hashCode()方法也應該返回true
  • HashSet中,同一對象多次調用hashCode()方法應該返回相同的結果
  • HashSet中,多個對象間用作equals()方法比較的實例變量,都應該用於計算hashCode值

LinkedHashSet

LinkedHashSet是HashSet的一個子類實現,具有HashSet的所有特性,不同的是,它可以使用鏈表來維護元素的插入順序。當我們遍歷LinkedHashSet時,LinkedHashSet會按照元素的插入順序來訪問集合中的所有元素。同時,由於需要鏈表結構來維護順序,LinkedHashSet的性能是低於HashSet的,除非工作中對集合元素插入的順序有特殊需求,在兩者的取捨中我們都應該優先考慮使用HashSet。

TreeSet

特徵

TreeSet是SortedSet接口的實現類,所以可以保證集合元素的排序狀態,與HashSet相比,具有以下幾個能夠體現集合特徵的方法:

  • Comparator comparator():TreeSet默認採用自然排序(可以近似的認爲是有小到大),那麼該方法返回null,如果TreeSet採用自己定製的排序規則comparator,則返回該comparator。
  • Object first():返回集合中排序後的第一個元素。
  • Object last():返回集合中排序後的最後一個元素。
  • Object lower(Object e):返回集合中位於制定元素之前的元素,即小於指定元素的最大元素,參考元素不要求一定是當前TreeSet集合中存在的元素。(比如一個TreeSet爲[0,2,5,9],可以返回lower(3)->2,3並不是該集合中的元素,higher方法同理)。
  • Object higher(Object e):返回集合中位於制定元素之後的元素,即大於指定元素的最小元素,參考元素不要求一定是當前TreeSet集合中存在的元素。
  • SortedSet subSet(Object fromElement, Object toElement):返回此Set的子集合。
  • SortedSet headSet(Object toElement):返回此Set中由小於toElement元素組成的子集。
  • SortedSet tailSet(Object fromElement):返回此Set中由大於等於fromElement元素組成的子集。

記憶這些方法的訣竅很簡單,就是返回TreeSet中的第一個,前一個,後一個,最後一個元素的方法,和截取TreeSet子集的方法。

實現

與HashSet採用hash算法來覺得元素的存儲地址不同,TreeSet底層採用紅黑樹實現,關於紅黑樹的詳細知識可以參考這篇文章:紅黑樹(一)之 原理和算法詳細介紹。這篇文章講得很細緻,有興趣的同學可以花時間好好看看,對於數據結構的學習也是很有幫助的。

Comparable接口

說到TreeSet的排序功能,自然要提到比較,TreeSet在插入元素時,會調用元素的compareTo(Object e)方法來比較元素之間的大小關係。compareTo()方法是Java提供的Comparable接口中的方法,要實現這個接口就必須實現compareTo()方法
在默認的自然排序下,obj1.compareTo(obj2)有三種結果:

  • 0,obj1等於obj2
  • 正整數,obj1大於obj2

當然,爲了方便,Java中很多常用類都已經實現了Comparable接口,並且提供了排序標準,下面是實現了Comparable接口的常用類:

  • BigDecimal、BigInteger以及所有的數值型對應包裝類,按照他們的數值大小進行比較。
  • Character:按照字符的UNICODE值進行比較。
  • Boolean:true的包裝類實例大於false的包裝類實例。
  • String:按照字符串中字符的UNICODE值進行比較。
  • Date、Time:越新的時間越大。

當我們試圖將一個對象插入到TreeSet中時,該對象就必須實現Comparable接口,否則會拋出ClassCastException異常。同時,TreeSet默認情況下只允許存儲統一類型的對象,否則會產生一些不正常的運行表現。這點很好理解,只有統一類型才能保證比較邏輯上的一致性。如果插入元素時,compareTo方法返回0,說明集合中已經存在了一樣的元素,那麼該集合就不能被插入,體現了Set元素的不可重複性。

總結

性能上,HashSet總比TreeSet搖號,尤其是添加和查詢操作,由於hash算法的存在,HashSet都會快得多,因爲TreeSet需要額外的紅黑樹算法來維護集合元素的排序。理論上,只有當需要一個能保持排序的Set時,我們纔會考慮使用TreeSet,否則都該首選HashSet。
LinkedHashSet由於要使用鏈表來維護集合元素的插入順序,也會帶來額外的開銷,但是因爲鏈表的實現方式,它的遍歷操作相較於HashSet會更快捷。
還有一點就是,HashSet和TreeSet都是線程不安全的。爲了防止多線程訪問同一個Set而導致的不同步問題,我們必須手動保證Set的線程安全,通常可以使用Collections工具類的SynchronizedSortedSet裝飾符在創建Set時來包裝Set集合,如下所示:

SortedSet sortedSet = Collections synchronizedSortedSet(new TreeSet(...));
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章