天天看點

Java中的集合HashSet、LinkedHashSet、TreeSet和EnumSet

原檔案連結點這裡

HashSet是一個對象容器類.HastSet<Integer>的意思就是在HashSet内的資料都是Integer類型的資料.這是為了防止程式員自己裝入錯誤的資料,而是在編譯時自己幫助程式員進行檢測.

ps:凡是對象容器類的,都可以用Xxxxx<Object> 格式來聲明.(當然,也可以不那麼聲明,隻是編譯時會有警告的)

  • 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具有以下特點:

    1. HashSet具有很好的對象檢索性能,當從HashSet中查找某個對象時,Java系統首先調用對象的hasCode方法獲得該對象的哈希碼,然後根據哈希碼找到對應的存儲區域,最後取出該存儲區域的每個元素與該對象進行equals方法的比較,這樣不用周遊集合中的所有元素就可以得到結論。
    2. HashSet存儲對象的效率相對要低些,因為向HashSet集合中添加對象的時候,首先要計算出來對象的哈希碼和根據這個哈希碼來确定對象在集合中的存放位置。
    3. 不能保證排列的順序,順序有可能發生改變。
    4. HashSet不是同步的,如果多個線程同時通路一個Set集合,如果多個線程同時通路一個HashSet集合,如果有2條或者2條以上線程同時修改了HashSet集合時,必須通過代碼來保證其同步。
    5. 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還提供了如下幾個額外方法:

    1. Comparator comparator(); //傳回目前Set使用的Comparator,或者傳回null,表示以自然方式排序。
    2. Object first();   //第一個;傳回集合中的第一個元素。
    3. Object last();   //最後一個;傳回集合中的最後一個元素。
    4. Object lower(Object o);    //前一個;傳回集合中位于指定元素之前的元素(即小于指定元素的最大元素,參考元素不需要是TreeSet的元素)。 
    5. Object higher(Object o);  //後一個;傳回集合中位于指定元素之後的元素(即大于指定元素的最小元素,參考元素不需要是TreeSet的元素)。
    6. SortedSet subSet(fromElement, toElement);    //傳回此Set的子集合,範圍從fromElement(包含)到toElement(不包含)。
    7. SortedSet headSet(toElement);  //傳回此set的子集,由小于toElement的元素組成。
    8. 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接口的常用類:

    1. BigDecimal、BigInteger以及所有數值型對應包裝類:按它們對象的數值大小進行比較。
    2. Character :按字元的Unicode值進行比較。
    3. Boolean : true對應的包裝類執行個體大于false對應的包裝類執行個體。
    4. String : 按字元串中字元的Unicode值進行比較。
    5. Date、Time : 後面的時間、日期比前面的日期時間大。

如圖所示:Integer類實作了Comparable接口:

Java中的集合HashSet、LinkedHashSet、TreeSet和EnumSet

  由于上邊的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);    
    }
}      

 以上代碼将會抛出:

Java中的集合HashSet、LinkedHashSet、TreeSet和EnumSet
  •  定制排序

  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對象:

    1. static EnumSet allOf(Class elementType);   建立一個包含指定枚舉類裡所有枚舉值的EnumSet集合。
    2. static EnumSet complementOf(EnumSet s); 建立一個其元素類型與指定EnumSet裡元素類型相同的EnumSet,新EnumSet集合包含原EnumSet所不包含的、此枚舉類剩下的枚舉值(有點繞,看下面的例子,一看就懂)。
    3. static EnumSet copyOf(Collection c);  使用一個普通集合來建立EnumSet集合。
    4. static EnumSet copyOf(EnumSet s);   建立一個與指定EnumSet具有相同元素集合類型、相同集合元素的EnumSet。
    5. static EnumSet noneOf(Class elementType); 建立一個集合類型為指定枚舉類型的空EnumSet。
    6. static EnumSet of(E first, E...rest);  建立一個包含一個或多個枚舉值的EnumSet,傳入的多個枚舉值必須屬于同一個枚舉類。
    7. 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]
    }
}      
  • 總結

  1. HashSet和TreeSet是Set的兩個典型實作,HashSet的性能總是比TreeSet好(特别是比較常用的添加、查詢元素等操作),因為TreeSet需要額外的紅黑樹算法來維護集合元素的次序。隻有當需要一個保持排序的Set時,才應該使用TreeSet,否則都應該使用HashSet。
  2. HashSet還有一個子類:LinkedHashSet,對于普通插入、删除操作,LinkedHashSet比HashSet要略微慢一點;這是由維護連結清單所帶來的額外開銷所趙成的,不過,因為有了連結清單,周遊LinkedHashSet會更快。
  3. EnumSet是所有Set實作類中性能最好的,但它隻能儲存同一個枚舉類的枚舉值做為集合元素。
  4. Set的三個實作類HashSet(包括LinkedHashSet)、TreeSet和EnumSet都是線程不安全的。如果有多個線程同時通路一個Set集合,并且有超過一條線程修改了該Set集合,則必須手動保證該Set集合的同步性。通常可以通過Collections工具類的synchronizedSet方法來"包裝"該Set集合。此操作最好在建立時進行,以防止對Set集合的意外非同步通路。例如:Set hs = Collections.synchronizedSet(new HashSet());