-
Set接口
前面已經簡紹過Set集合,它類似於一個罐子,一旦把對象'丟進'Set集合,集合里多個對象之間沒有明顯的順序。Set集合於Collection基本上完全一樣,它沒有提供任何額外的方法。
Set集合不容許包含相同的元素,如果試圖把兩個相同元素加入到同一個Set集合中,則添加操作失敗,add方法返回false,且新元素不會被加入。
Set判斷兩個對象是否相同不是使用==運算符,而是根據equals方法。也就是說,只要兩個對象用equals方法比較返回true,Set就不會接受這兩個對象,反之,只要兩個對象用equals方法比較返回false,Set就會接受這兩個對象(甚至這兩個對象是同一個對象,Set也可把他們當成倆個對象處理),下面是Set的使用案例。
public class Test { public static void main(String[] args){ Set set = new HashSet(); boolean a = set.add(new String("語文")); boolean b = set.add(new String("語文")); //打印結果為true System.out.println(a); //打印結果為false System.out.println(b); /* * 打印結果為[語文]; * 因為兩個字符串通過equals方法比較返回為true(String類默認重寫了Object中equals方法),所以第二次添加失敗 */ System.out.println(set); } }
從上面程序中可以看出,books集合兩次添加的字符串對象明顯不是同一個對象(因為兩次都調用了new關鍵字來創造字符串對象),這兩個字符串對象使用==運算符判斷肯定返回false,但它們通過equals方法比較將返回true,所以添加失敗。最后輸出set集合時,將看到輸出結果只有一個元素。
上面介紹的是Set集合的通用知識,完全適合HashSet、TreeSet和EnumSet三個實現類。
-
HashSet類
HashSet具有以下特點:
-
- HashSet具有很好的對象檢索性能,當從HashSet中查找某個對象時,Java系統首先調用對象的hasCode方法獲得該對象的哈希碼,然后根據哈希碼找到對應的存儲區域,最后取出該存儲區域的每個元素與該對象進行equals方法的比較,這樣不用遍歷集合中的所有元素就可以得到結論。
- HashSet存儲對象的效率相對要低些,因為向HashSet集合中添加對象的時候,首先要計算出來對象的哈希碼和根據這個哈希碼來確定對象在集合中的存放位置。
- 不能保證排列的順序,順序有可能發生改變。
- HashSet不是同步的,如果多個線程同時訪問一個Set集合,如果多個線程同時訪問一個HashSet集合,如果有2條或者2條以上線程同時修改了HashSet集合時,必須通過代碼來保證其同步。
- HashSet集合元素可以是null。
HashSet還有一個子類LinkedHashSet,LinkedHashSet集合也是根據元素hashCode值來決定元素存儲位置,但它同時使用鏈表維護元素的次序,這樣使的元素看起來是以插入的順序保存的。也就是說當遍歷LinkedHashSet集合里的元素時,HashSet將會按元素的添加順序來訪問集合里的元素。
LinkedHashSet需要維護元素的插入順序,因此性能略低於HashSet的性能,但是在迭代訪問Set里的全部元素時,將有很好的性能,因為它以列表來維護內部順序。
public class Test { public static void main(String[] args){ LinkedHashSet books = new LinkedHashSet(); books.add("語文"); books.add("數學"); books.add("英語"); //刪除語文 books.remove("語文"); //重新添加 books.add("語文"); //打印結果為[數學, 英語, 語文] System.out.println(books); } }
上面的集合里,元素的順序正好與添加順序一致。
-
TreeSet類
TreeSet是SortedSet接口的唯一實現(SortedSet接口繼承Set接口),正如SortedSet名字所暗示的,TreeSet可以確保集合元素處於排序狀態。與前面的HashSet集合相比,TreeSet還提供了如下幾個額外方法:
-
- Comparator comparator(); //返回當前Set使用的Comparator,或者返回null,表示以自然方式排序。
- Object first(); //第一個;返回集合中的第一個元素。
- Object last(); //最后一個;返回集合中的最后一個元素。
- Object lower(Object o); //前一個;返回集合中位於指定元素之前的元素(即小於指定元素的最大元素,參考元素不需要是TreeSet的元素)。
- Object higher(Object o); //后一個;返回集合中位於指定元素之后的元素(即大於指定元素的最小元素,參考元素不需要是TreeSet的元素)。
- SortedSet subSet(fromElement, toElement); //返回此Set的子集合,范圍從fromElement(包含)到toElement(不包含)。
- SortedSet headSet(toElement); //返回此set的子集,由小於toElement的元素組成。
- SortedSet tailSet(fromElement); //返回此set的子集,由大於或等於fromElement的元素組成。
public class Test { public static void main(String[] args){ TreeSet<Integer> nums = new TreeSet<Integer>(); nums.add(3); nums.add(1); nums.add(5); nums.add(-9); //1.返回第一個元素 Integer first = nums.first(); //打印結果為-9 System.out.println(first); //2.返回最后一個元素 Integer last = nums.last(); //打印結果為5 System.out.println(last); //3.返回上一個 Integer lower = nums.lower(2); //打印結果為1 System.out.println(lower); //4.返回下一個 Integer higher = nums.higher(2); //打印結果為3 System.out.println(higher); //5.返回小於3的子集,不包含3 SortedSet<Integer> headSet = nums.headSet(3); //打印結果[-9, 1] System.out.println(headSet); //6.返回大於等於3的子集,包含3 SortedSet<Integer> tailSet = nums.tailSet(3); //打印結果[3, 5] System.out.println(tailSet); //7.打印整個集合結果為[-9, 1, 3, 5] System.out.println(nums); } }
根據上面程序的運行結果可看出,TreeSet並不是根據元素的插入順序進行排序,而是根據元素實際值來進行排序的。
與HashSet集合采用的hash算法來決定元素的存儲位置不同,TreeSet采用紅黑樹的數據結構對元素進行排序。那么TreeSet進行排序是怎么樣的呢?TreeSet支持兩種排序方法:自然排序和定制排序。默認情況下,TreeSet采用自然排序。
-
自然排序
Java提供了一個Comparable接口,該接口里定義了一個compareTo(Object obj)方法,該方法返回一個整數值,實現該接口的類必須實現該方法,實現了該接口的類的對象就可以比較大小了。當一個對象調用該方法與另一個對象進行比較,例如obj1.compareTo(obj2); 如果該方法返回0,則表明這兩個對象相等;如果該方法返回一個正整數,則表明obj1大於obj2;如果該方法返回一個負整數,則表明obj1小於obj2。
Java的一些常用類已經實現了Comparable接口,並提供了比較大小的標准, 下面是實現了Comparable接口的常用類:
-
- BigDecimal、BigInteger以及所有數值型對應包裝類:按它們對象的數值大小進行比較。
- Character :按字符的Unicode值進行比較。
- Boolean : true對應的包裝類實例大於false對應的包裝類實例。
- String : 按字符串中字符的Unicode值進行比較。
- Date、Time : 后面的時間、日期比前面的日期時間大。
如圖所示:Integer類實現了Comparable接口:
由於上邊的Integer類實現了Comparable接口,故TreeSet會調用集合元素的compareTo(Object o)方法來比較元素之間的大小關系,然后將集合元素按升序排列,這種方式就是自然排序。如果試圖把一個對象添加進TreeSet時,則該對象的類必須實現Comparable接口,否則程序將會拋出ClassCastException異常。代碼如下:
class Person{ } public class Test { public static void main(String[] args){ TreeSet<Person> persons = new TreeSet<Person>(); persons.add(new Person()); System.out.println(persons); } }
以上代碼將會拋出:
-
定制排序
TreeSet的自然排序是根據集合元素的大小,TreeSet將它們以升序排列。如果需要完成定制排序,例如以降序排列,則可以使用Comparator接口的幫助。該接口里包含了一個int compare(T o1, T o2)方法,該方法用於比較o1、o2的大小:如果該方法返回正整數,則表明o1大於o2;如果該方法返回0,則表明o1等於o2;如果該方法返回負整數,則表明o1小於o2。
如下所示:如果需要實現定制排序(我們這實現倒序),則需要在創建TreeSet集合對象時,並提供一個Comparator對象與該TreeSet集合關聯,由該Comparator對象負責集合元素的排序邏輯。
class Person{ Integer age; public Person(int age){ this.age = age; } @Override public String toString() { return "Person [age=" + age + "]"; } } public class Test { public static void main(String[] args){ TreeSet<Person> persons = new TreeSet<Person>(new Comparator<Person>(){ @Override public int compare(Person o1, Person o2) { if(o1.age > o2.age){ return -1; }else if(o1.age == o2.age){ return 0; }else{ return 1; } } }); persons.add(new Person(2)); persons.add(new Person(5)); persons.add(new Person(6)); //打印結果為[Person [age=6], Person [age=5], Person [age=2]]倒序 System.out.println(persons); } }
上面程序創建了一個Compartor接口的匿名內部類對象,該對象負責persons集合的排序。所以當我們把Person對象添加到persons集合中時,無須Person類實現Comparable接口,因為此時TreeSet無須通過Person對象來比較大小,而是由與TreeSet關聯的Compartor對象來負責集合元素的排序。
-
EnumSet類
EnumSet是一個專為枚舉設計的集合類,EnumSet中所有值都必須是指定枚舉類型的枚舉值,該枚舉類型在創建EnumSet時顯式或隱性的指定。EnumSet的集合元素也是有序的,EnumSet以枚舉值在Enum類內的定義順序來決定集合元素的排序。
EnumSet在內部以位向量的形式存儲,這種存儲形式非常緊湊、高效,因此EnumSet對象占用內存很小,而且運行效率很好。尤其是當進行批量操作(如調用containsAll和retainAll方法)時,如其參數也是EnumSet集合,則該批量操作的執行速度也非常快。
EnumSet集合不容許加入null元素。如果試圖插入null元素,EnumSet將會拋出NullPointerException異常。
EnumSet類沒有暴露任何構造器來創建該類的實例,程序應該通過它提供的static方法來創建EnumSet對象。它提供了如下常用static方法來創建EnumSet對象:
-
- static EnumSet allOf(Class elementType); 創建一個包含指定枚舉類里所有枚舉值的EnumSet集合。
- static EnumSet complementOf(EnumSet s); 創建一個其元素類型與指定EnumSet里元素類型相同的EnumSet,新EnumSet集合包含原EnumSet所不包含的、此枚舉類剩下的枚舉值(有點繞,看下面的例子,一看就懂)。
- static EnumSet copyOf(Collection c); 使用一個普通集合來創建EnumSet集合。
- static EnumSet copyOf(EnumSet s); 創建一個與指定EnumSet具有相同元素集合類型、相同集合元素的EnumSet。
- static EnumSet noneOf(Class elementType); 創建一個集合類型為指定枚舉類型的空EnumSet。
- static EnumSet of(E first, E...rest); 創建一個包含一個或多個枚舉值的EnumSet,傳入的多個枚舉值必須屬於同一個枚舉類。
- static EnumSet range(E first, E to); 創建包含從from枚舉值,到to枚舉值范圍內所有枚舉值的EnumSet集合。
enum Season{ SPRING,SUMMER,AUTUMN,WINTER } public class Test { public static void main(String[] args){ //1.0創建一個EnumSet集合,集合元素就是Season枚舉類的全部枚舉值 EnumSet<Season> es = EnumSet.allOf(Season.class); System.out.println(es);//輸出[SPRING, SUMMER, AUTUMN, WINTER] //2.0創建一個EnumSet空集合,指定其集合元素時Season類的枚舉值。 EnumSet<Season> es2 = EnumSet.noneOf(Season.class); System.out.println(es2);//輸出[] //2.1手動添加兩個元素 es2.add(Season.AUTUMN); es2.add(Season.WINTER); System.out.println(es2);//輸出[AUTUMN, WINTER] //3.0以指定枚舉值創建EnumSet集合 EnumSet<Season> es3 = EnumSet.of(Season.SPRING, Season.SUMMER); System.out.println(es3);//輸出[SPRING, SUMMER] //4.0創建包含從Season.SPRING枚舉值,到Season.AUTUMN枚舉值范圍內所有枚舉值的EnumSet集合。 EnumSet<Season> es4 = EnumSet.range(Season.SPRING, Season.AUTUMN); System.out.println(es4); //輸出[SPRING, SUMMER, AUTUMN] //5.0新創建的EnumSet集合元素和es4集合的元素有相同類型,es5的集合元素 + es4的集合元素 = Season 的所有枚舉值 EnumSet<Season> es5 = EnumSet.complementOf(es4); System.out.println(es5); //輸出[WINNER] //6.0復制Collection集合中所有元素來創建EnumSet集合。 Collection<Season> c = new HashSet<Season>(); c.add(Season.AUTUMN); c.add(Season.WINTER); EnumSet<Season> es6 = EnumSet.copyOf(c); System.out.println(es6); //輸出[AUTUMN, WINTER] } }
-
總結
- HashSet和TreeSet是Set的兩個典型實現,HashSet的性能總是比TreeSet好(特別是比較常用的添加、查詢元素等操作),因為TreeSet需要額外的紅黑樹算法來維護集合元素的次序。只有當需要一個保持排序的Set時,才應該使用TreeSet,否則都應該使用HashSet。
- HashSet還有一個子類:LinkedHashSet,對於普通插入、刪除操作,LinkedHashSet比HashSet要略微慢一點;這是由維護鏈表所帶來的額外開銷所趙成的,不過,因為有了鏈表,遍歷LinkedHashSet會更快。
- EnumSet是所有Set實現類中性能最好的,但它只能保存同一個枚舉類的枚舉值做為集合元素。
- Set的三個實現類HashSet(包括LinkedHashSet)、TreeSet和EnumSet都是線程不安全的。如果有多個線程同時訪問一個Set集合,並且有超過一條線程修改了該Set集合,則必須手動保證該Set集合的同步性。通常可以通過Collections工具類的synchronizedSet方法來"包裝"該Set集合。此操作最好在創建時進行,以防止對Set集合的意外非同步訪問。例如:Set hs = Collections.synchronizedSet(new HashSet());