Java Connection集合分析之Set

Java Connection集合家庭分析

Java集合大致可以分爲Set、List、Queue和Map四種體系,其中Set代表無序、不可重複的集合;List代表有序、重複的集合;而Map則代表具有映射關係的集合,Java 5 又增加了Queue體系集合,代表一種隊列集合實現。

Java集合類之間的繼承關係

Java的集合類主要由兩個接口派生而出:Collection和Map,Collection和Map是Java集合框架的根接口。

Collection家族:

 

Set集合

Set集合時無序、不可重複的集合

Set集合中包含了三個比較重要的實現類:HashSet、TreeSet和EnumSet。本篇文章將重點介紹這三個類。

 

1.HashSet是Set接口的典型實現,實現了Set接口中的所有方法,並沒有添加額外的方法,大多數時候使用Set集合時就是使用這個實現類。HashSet按Hash算法來存儲集合中的元素。因此具有很好的存取和查找性能。HashSet不能保證元素的排列順序,順序可能與添加順序不同,順序也有可能發生變化。HashSet不是同步的,如果多個線程同時訪問一個HashSet,則必須通過代碼來保證其同步。同HashTable不同集合元素值可以是null。HashSet集合判斷兩個元素相等的標準是兩個對象通過equals()方法比較相等,並且兩個對象的hashCode()方法返回值也相等。

 

2.兩個對象比較 具體分爲如下四個情況:

.如果有兩個元素通過equal()方法比較返回false,但它們的hashCode()方法返回不相等,HashSet將會把它們存儲在不同的位置。

.如果有兩個元素通過equal()方法比較返回true,但它們的hashCode()方法返回不相等,HashSet將會把它們存儲在不同的位置。

.如果兩個對象通過equals()方法比較不相等,hashCode()方法比較相等,HashSet將會把它們存儲在相同的位置,在這個位置以鏈表式結構來保存多個對象。這是因爲當向HashSet集合中存入一個元素時,HashSet會調用對象的hashCode()方法來得到對象的hashCode值,然後根據該hashCode值來決定該對象存儲在HashSet中存儲位置。

.如果有兩個元素通過equal()方法比較返回true,但它們的hashCode()方法返回true,HashSet將不予添加。

HashSet判斷兩個元素相等的標準:兩個對象通過equals()方法比較相等,並且兩個對象的hashCode()方法返回值也相等。

注意:HashSet是根據元素的hashCode值來快速定位的,如果HashSet中兩個以上的元素具有相同的hashCode值,將會導致性能下降。所以如果重寫類的equals()方法和hashCode()方法時,應儘量保證兩個對象通過hashCode()方法返回值相等時,通過equals()方法比較返回true。

 

3.HashSet的實質是一個HashMap。HashSet的所有集合元素,構成了HashMap的key,其value爲一個靜態Object對象。因此HashSet的所有性質,HashMap的key所構成的集合都具備。

 

從源碼中我們可以看到,HashSet的add方法其實就是將值存入HashMap的Key,而Value存儲了一個靜態Object進行填充。

 

通過源碼我們看到,HashSet其實底層應用的HashMap進行存儲,因此HashSet如果Hash碰撞頻繁,查詢效率也會降低。

 

4.LinkedHashMap源碼

繼承與HashSet

 

默認初始化方式

 

可以看到默認的初始化方式調用了HashSet的初始化

 

最終我們得出結論,LinkedHashMap底層由LinkedHashMap進行存儲的,從初始化源碼中可以看出來,並且繼承於HashSet,因此其添加元素的原理同HashSet,key存儲了Set的值,value存儲了一個靜態的站位Object對象。

並且,由於底層是LinkedHashMap,因此它是有序的,並且元素不能重複。當遍歷LinkedHashSet集合裏的元素時,LinkedHashSet將會按元素的添加順序來訪問集合裏的元素。但是由於要維護元素的插入順序,在性能上略低與HashSet,但在迭代訪問Set裏的全部元素時有很好的性能。

 

5.TreeSet源碼

通過上面源碼的截圖,可以看出其底層是通過TreeMap進行存儲,並且套路和HashSet一樣TreeMap的Key用來存TreeSet的值,TreeMap的Value用一個靜態Object填充。

TreeSet針對元素的排序,可以根據元素的compareTo方法進行比較,還可以在初始化TreeSet時指定Comparator來對元素進行排序。

注意:如果向TreeSet中添加了一個可變對象後,並且後面程序修改了該可變對象的實例變量,這將導致它與其他對象的大小順序發生了改變,但TreeSet不會再次調整它們。我們通過下面的程序來進行演示:

 

其中Person實現Comparable接口,將Person對象按照年齡從小到大升序排列。

輸出結果:

 

初始年齡排序

[Person[age=10], Person[age=30], Person[age=40]]

修改p1年齡後集合排序

[Person[age=60], Person[age=30], Person[age=40]]

修改p2年齡後集合排序

[Person[age=60], Person[age=40], Person[age=40]]

可以看到並沒有發生變化,而且如果修改後進行元素刪除操作可能會不成功,具體比較複雜。總之,推薦不要修改放入TreeSet集合中元素的關鍵實例變量。

補充:TreeSet也是非線程安全的。

 

 

6.EnumSet——專門存放枚舉類型元素的Set:

    1) EnumSet只能存放一種枚舉類型的元素,具體存放什麼枚舉類型的元素可以通過兩種方法指定,一種是顯式,一種是隱式;

    2) 一旦元素的枚舉類型確定那麼集合就確定了(即只能存放該種枚舉類型的元素,不能同時存放多種枚舉類型的元素!!否則就會拋出異常);

    3) 枚舉集合EnumSet底層並不是直接存放枚舉對象的,而是用二進制位向量來存放的,因此存儲非常緊湊,並且高效(比如枚舉類型A裏有4個枚舉值a、b、c、d,並且這4個值都是對象,現在一個枚舉集合存放a、c、d三個值,由於枚舉類型的所有值的個數是有限的,因此可以用二進制序列來唯一表示,這裏就用4位二進制數表示,現在只有a、c、d這三個值,因此可以表示爲1011,1表示該枚舉值在集合中,0表示不在集合種;

!!枚舉值在枚舉類型中是有序號的,就是按照其定義順序排列的;

!!而從枚舉集合中取出一個枚舉值是是根據二進制位的位置還原原本的枚舉值的,比如取出第二個二進制位置的枚舉值,那麼根據枚舉類型中枚舉值定義的順序,第二個位置是b,那麼取出的就是b,即位置和值是一一對應的;

!!由於是二進制位操作,因此向containsAll、retainAll等方法會非常高效;

    4) EnumSet不允許包含null元素,強行添加會拋出異常!但是判空和刪除null元素的方法可以正常調用,只不過永遠返回null而已(因爲不存在null元素,因此也沒辦法刪除);

    5) 由於使用二進制位來保存的,重複就更加不用擔心的,每個二進位的位置就代表一個枚舉值,因此一定不會重複;

 

7.構造EnumSet:

    1) EnumSet並沒有提供公開的構造器來構造對象,而是提供了很多靜態工具方法來構造EnumSet對象;

    2) 下面介紹的都是EnumSet的靜態工具方法,用來構造EnumSet對象:共有顯式和隱式兩種

****顯式:顯式(手工)指出存放元素所屬的枚舉類型

         i. static EnumSet allOf(Class elementType);  // 將elementType所代表的枚舉類型的所有枚舉值加入到集合中,例如EnumSet es = EnumSet.allOf(Season.class);

!!將Season枚舉類的所有枚舉值SPRING、SUMMER、FALL、WINNTER加入到集合中

         ii. static EnumSet noneOf(Class elementType);  // 創建一個空的、只用來存放elementType枚舉類型值的集合

****隱式:不直接在參數中指定枚舉類型,而是通過參數的類型自動推斷枚舉類型

         i. 用其它枚舉集合來構造:

            a. static EnumSet complementOf(EnumSet s);   // 用s之外的其它枚舉值來構造一個集合(比如枚舉類型有a、b、c、d 4個值,現在s有a和c,那麼構造出來集合只包含b和d,即補集);

            b. static EnumSet copyOf(EnumSet s);  // s的深複製(不是引用複製)

         ii. 直接用枚舉值來構造:

            a. static EnumSet<E> of(E first, E... rest);  // 直接用若干枚舉值構建一個枚舉集合,例如EnumSet es = EnumSet.of(Season.SPRING, Season.WINNTER);

            b. static EnumSet<E> range(E from, E to); // 直接用枚舉值區間構建一個集合,包括[from, to]的所有枚舉值

注意!

        *1. 直接用枚舉值構造時枚舉值必須都屬於同一個類型,否則會報錯!

        *2. [from, to]是閉區間!而不是左閉右開了,因爲枚舉類型無法表示最後一個值的後一個值!因此只有這裏比較特殊,採用了右邊閉合的區間;

        *3. 枚舉值的順序就是枚舉類型中枚舉值定義的順序;

    3) 構造完之後就可以正常調用add等集合操作方法對枚舉集合進行操作了;

 

8. 特殊的構造方式——用另一個集合來構造EnumSet:

    1) 原型:static EnumSet copyOf(Collection c);

    2) 如果c就是一個EnumSet那該方法就跟static EnumSet copyOf(EnumSet s);沒有區別;

    3) 如果是c是普通的集合(Set、List等),那就要求c裏面存放的必須是同一類型的枚舉類型對象,因爲它會將c中的全部元素加入到新集合中,如果類型不一致肯定是不行的;

!!注意兩個關鍵點:

        i. c中元素的類型必須是枚舉類型,否則和EnumSet的本質相違背肯定是會異常的!

        ii. c中的元素必須屬於同一枚舉類型,否則也會違反EnumSet類型一致的規定而異常的!

4) 如果c是一個List,而裏面的元素有重複會怎麼樣呢?沒關係,重複元素就相當於add了多次,add的時候會自動判斷是否重複的,如果重複則拒絕添加,因此最終產生的EnumSet是絕對不會重複(添加的時候自動去重了);

 

9. Set各實現類的性能以及該如何選擇使用哪種實現類:

    1) 就效率而言EnumSet毋庸置疑是最高的,畢竟是用二進制向量保存的,其次HashSet性能好於LinkedHashSet(僅僅多了一個維護插入順序的鏈表),而TreeSet性能排最後,因爲需要時刻維護紅黑樹的結構已達到有序狀態(大小順序);

    2) 至於碰到一個問題該選擇何種Set的實現類就很簡單了,關鍵看你是什麼需求,枚舉就用EnumSet,僅僅是一個無需集合就用HashSet,需要維護插入順序就用LinkedHashSet,需要維護大小順序的就只能用TreeSet了!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章