1. 程式人生 > 實用技巧 >Java集合(二)-Set集合

Java集合(二)-Set集合

來源:流浪舟 https://www.maliaoblog.cn/2020/1003
公眾號: 菜雞幹Java

目錄
Set集合和Collection基本相同,沒有提供額外的方法,主要是行為上的不同,Set不允許包含重複元素,否則add()方法會返回false。接下來將主要介紹四種Set類,HashSetTreeSetLinkedHashSetEnumSet四種。

HashSet類

HashSet使用hash演算法來儲存集合中的元素,具有很好的查詢和存取效能,它的特點如下:

  1. 不能保證元素的順序,可能變化
  2. HashSet不是同步的,如果多個執行緒同時訪問並修改一個HashSet,則必須保證其同步。
  3. 集合的元素可以是null

當向HashSet集合中存入一個元素時,HashSet會呼叫物件的hashCode()方法來得到該物件的hashCode值,然後根據該hashCode值決定該物件在HashSet中的位置。但是如果兩個元素通過equals()方法返回true,而它們的hahCode()方法返回值不相等,HashSet將會把他們儲存在不同的位置,依然可以新增成功。也就是說,HashSet集合判斷元素相等的標準是通過equals方法比較相等,並且兩個物件的hashCode

值一樣。

Class A{
    public boolean equals(Object obj){
        return true;
    }
}
Class B{
    public int hashCode(){
        return 1;
    }
}
Class C{
    public int hashCode(){
        return 2;
    }
    public boolean equals(Object obj){
        return true;
    }
}
public Class HashSet{
    public static void main(String[] args){
        HashSet books = new HashSet();
        books.add(new A());
        books.add(new A());
        books.add(new B());
        books.add(new B());
        books.add(new C());
        books.add(new C());
    }
}//新增兩A,B,C物件
[B@1 ,B@1 ,C@2 ,A@54d ,A@8773f2]

從上面看出,即使A物件equals方法返回true,依然被當作兩個物件;即使兩個B物件hashCode方法返回值一樣,但HashSet集合儲存了兩個hash值一樣的物件。如果equals返回true,但是會放在不同的地方,不太好。如果hash值一樣,但equals返回的false也麻煩了,集合會嘗試儲存把物件儲存在同一個位置,並採用鏈式結構來儲存多個物件,這樣會導致利用雜湊值查詢的時候,效能下降。

所以總結一句:如果需要把物件儲存到HashSet集合中去,重寫類的equalshashCode方法,保證equals返回true時,hashCode返回值一樣。

HashSet中每個能儲存元素的位置通常稱為桶(bucket),如果有多個元素的雜湊值相同,但它們通過equals方法返回false,就需要在一個桶內放多個元素,然而這樣會導致效能下降。

重寫hashCode方法步驟
  1. 把物件內的每個參與equals方法比較的例項變數計算處一個int型別的雜湊值
例項變數型別 計算方式 例項變數型別 計算方式
boolean hashCode = (f ? 0 : 1) float hashCode = Float.floatToIntBits(f)
整型byte、char、short、int hashCode = (int)f double long L = Double.doubleToLongBits(f);
hashCode = (int)(L^(L>>>32));
Long hashCode = (int)(f^(f>>>32)); 引用 hashCode = f.hashCode();

2.用第一步計算出的多個雜湊值組合計算出一個雜湊值返回,例如:

return f1.hashCode() + (int)f2;//f1,f2為例項變數

為避免直接相加產生的偶然,可以為各例項變數的雜湊值乘以任意質數後再相加。但還有一點需要了解的是,這樣並不能完全保證之後就不會產生兩個相同的物件,向HashSet中新增可變物件後,後面修改了可變物件的例項變數,可能導致它和集合中其他元素相同,甚至不能正確訪問(儲存在不同位置,hash值不一樣)。

LinkedHashSet

HashSet還有一個子類LinkedHashSet,同樣也是根據元素的hashCode值來決定元素的儲存位置,不同的是它使用連結串列維護元素的次序,這樣可以以插入順序儲存元素。LinkedHashSet需要連結串列維護次序,所以效能略低於HashSet,但優勢在於遍歷集合內元素上。另外注意一點,它還是Set,所以依然不允許元素重複!

LinkedHashSet names = new LinkedHashSet();
names.add("James");
names.add("Jodan");
System.out.println(names);
//James,Jodan

TreeSet(SortedSet的實現類)

既然是實現類,那SortedSet就是TreeSet的介面了,顧名思義,TreeSet可以讓元素處於一定次序狀態。與HashSet相比,TreeSet額外提供瞭如下方法:

  • Comparator comparator():如果TreeSet採用定製排序,則返回定製排序所使用的Comparator,如果採用自然排序,則返回null
  • Object first():返回第一個元素
  • Object last():返回最後一個元素
  • Object lower(Object e):返回處於指定元素(任意元素,不需要在集合中)之前的元素
  • Object higner(Object e):返回處於指定元素之後(更大的元素)的元素
  • SortedSet subSet(Object e1,Object e2):返回從e1到e2的子集合(不包含e1,e2)
  • SortedSet headSet(Object max):返回子集合,元素都小於max
  • SortedSet tailSet(Object min):返回子集合,元素大於等於min

綜上,大概就是提供了訪問第一個、最後一個、前一個、後一個元素的方法,並提供了三個擷取子集合的方法。與此同時,TreeSet採用紅黑樹的資料結構來儲存資料,預設情況下使用自然排序。它的排序規則有以下兩種:

1.自然排序

TreeSet呼叫集合元素的compareTo(Object obj)方法來比較元素之間的大小關係,然後將集合元素按升序排列,這種就是自然排序。Java提供了Comparable介面,該介面內定義了一個compareTo(Object o)方法,方法返回一個整數。實現介面就必須實現該方法。當一個物件之間進行比較時,例如:obj1.compareTo(obj2),如果返回0,則表明這兩個物件相等;如果返回一個正整數,則obj1大於obj2;如果返回一個負整數,則obj1小於obj2。

下面提供了實現Comparable介面的類,並提供了比較大小的標準。

  • BigDecimal、BigInteger以及所有數值型別對應的包裝類:按數值大小比較
  • Character:按字元的Unicode進行比較
  • Boolean:true對應的包裝類例項大於false對應的包裝類例項
  • String:按字串中字元的Unicode值比較
  • Date、Time:後面的時間、日期比前面的日期大

所以TreeSet會自動地給集合中地大小進行排序,但前提是物件實現了Comparable介面,否則將不可行。還有一點需要說明,大部分類在實現compareTo(Object o)方法時,都需要將被比較物件強制型別轉換成相同型別,否則無法比較。然而當新增一個物件到集合中去時,集合會呼叫物件compareTo()方法與集合中的其他元素進行比較,比較要是同一類的物件。

如果向TreeSet新增物件是自定義物件,則可以向TreeSet中新增多種型別物件,但這並不表明這是最好的選擇,也不推薦這樣幹,當取出物件時,元素之間會報ClassCastException異常。

當把一個物件加入集合中時,集合呼叫物件的compareTo方法與容器的其他物件比較,然後根據紅黑樹結構找到它的儲存位置。如果兩個物件通過compareTo(Object obj)方法比較相等,新物件將無法新增到集合中。總之,如果要不報錯,TreeSet中只能新增同一種類型的物件。

對於TreeSet集合而言,判斷兩個物件相等的唯一標準是:兩個物件通過compareTo(Object obj)方法比較相等,返回0,則相等。所以它的新增元素的規則和HashSet一樣,假如通過equals方法比較相等,則它們記為同一物件,因此通過compareTo()方法返回0,只能存放一個物件,集合不會讓第二個元素進去。同樣,如果添加了可變物件,並且還修改了物件的例項變數,將導致集合中物件的順序變化,然而TreeSet不會調整順序(甚至會導致刪除物件失敗,TreeSet效能降低)。

2.定製排序

實現定製排序則可以通過Comparator介面,該介面內包含一個int compare(T o1,T o2)方法,用於比較o1、o2的大小,如果返回正整數,則o1>o2;若返回0,則o1=o2;若返回負整數,則o1<o2。但依然不可以新增不同型別的物件。

在實現定製排序之前,需要提供一個Comparator物件與TreeSet集合關聯,由該物件對集合元素進行排序。

class M{
    int age;
    public M(int age){
        this.age = age;
    }
    public String toString(){
        return "M(age:" + age +" )";
    }
}
public class Test{
    public static void main(String[] args){//Lambda表示式
        TreeSet t = new TreeSet((o1,o2)->{//o1,02這裡沒有具體型別
            M m1 = (M)o1;
            M m2 = (M)o2;
            return m1.age>m2.age ? -1:m1.age<m2.age ? 1:0;
        });
        t.add(new M(5));
        t.add(new M(-3));
        t.add(new M(9));
        System.out.println(t);
    }//Lambda表示式實現了Comparator型別的物件
}//輸出[M(age:9),M(age:5),M(age:-3)]降序排列

EnumSet類

專門為列舉類設計的集合類,EnumSet中所有元素必須是指定列舉型別的列舉值。值得一提的是EnumSet集合也是有序的,但它以列舉值在類中的定義順序來決定集合元素的順序。EnumSet在內部以位向量的形式儲存元素,因此佔用記憶體非常小,執行效率很好,特別是在批量操作時。

EnumSet不允許加入null元素,如果試圖插入將會丟擲NullPointerException異常。而如果只是判斷集合中是否有null元素或刪除它,將不會丟擲異常,刪除的話會返回false,因為沒有null元素。

EnumSet集合沒有給出任何構造器來建立物件,但還是提供了一些方法去建立的物件的。如下:

  • EnumSet allOf(Class elementType):建立一個包含指定列舉類所有列舉值的集合
  • EnumSet complementOf(EnumSet s):建立一個其元素型別與指定EnumSet裡元素型別相同的EnumSet集合,新集合包含原集合不包含的、此列舉類剩下的列舉值
  • EnumSet copyOf(Collection c):使用一個普通集合來建立EnumSet
  • EnumSet copyOf(EnumSet e):建立一個與指定EnumSet具有相同型別、相同元素的EnumSet集合
  • EnumSet noneOf(Class elementType):建立一個元素型別為指定列舉型別的空EnumSet
  • EnumSet range(E from ,E to):建立一個包含從from列舉值到to列舉值範圍內列舉值的EnumSet集合
  • EnumSet of(E first...E rest ):建立一個包含一個或多個列舉值的EnumSet集合(傳入的列舉值必須屬於同一個列舉類)
enum Colors{WHITE,YELLOW,RED,GREEN}
public class Test{
    public static void main(String[] args){
        EnumSet e1 = EnumSet.allOf(Colors.class);
        EnumSet e2 = EnumSet.noneOf(Colors.class);//這裡為[]
        e2.add(Colors.WHITE);//這裡為[WHITE]
        EnumSet e3 = EnumSet.of(Colors.RED,Colors.GREEN);
        System.out.println(e3);//輸出[RED,GREEN]
        EnumSet e4 = EnumSet.range(Colors.YELLOW,Colors.GREEN);
        System.out.println(e4);
        //[YELLOW,RED,GREEN]
        EnumSet e5 = EnumSet.complementOf(e4);
        //這裡為[WHITE]
    }
}

此外還可以複製另一個EnumSet集合中的元素來建立新集合,或者複製一個Collection集合元素來建立新的EnumSet集合。當複製Collection集合元素建立時,要求元素都來自同一個列舉類,否則丟擲ClassCastException異常。

Collection c = new HashSet();
c.clear();
c.add(Colors.RED);
c.add(Colors.YELLOW);
EnumSet e = EnumSet.copyOf(c);
System.out.println(e);
//[RED,YELLOW]

各Set實現類的效能分析

HashSet總是比TreeSet的效能好,主要體現在新增、查詢元素上,因為TreeSet採用了紅黑樹來維護集合的次序。而HashSet的子類LinkedHashSet對插入、查詢操作比父類要稍慢些,這是由於採用了連結串列維護的緣故,造成了額外開銷。但這並不影響LinkedHashSet發揮威力,然而這恰恰是它的優勢,在遍歷等批量操作上,LinkedHashSet更快。

EnumSet是所有Set類中效能最好的,缺點是隻能儲存一個列舉類的列舉值。總的說,Set的這三個實現類都是執行緒不安全的,即當有多個執行緒訪問時,如果修改了Set集合,將不能同步,必須手動保證同步。方法是在建立Set集合時,通過Collections工具類的sychronizedXxx()方法來包裝該集合(之後會提到)。