1. 程式人生 > 實用技巧 >JavaEE - 11集合Collection-Set

JavaEE - 11集合Collection-Set

JavaEE - 11集合Collection-Set

(5)Set集合

(5.1)Set集合概述

Set介面:儲存無序、不可重複的資料

  • HashSet: Set主要實現類;執行緒不安全;可以儲存null值
    • LinkedHashSet: HashSet的子類;遍歷內部資料時,可以按照新增的順序遍歷
  • TreeSet:按照新增物件的指定屬性進行排序
  • 無序性: 不等於隨機性。儲存的資料在底層陣列中並非按照陣列索引的順序新增,而是按照資料的雜湊值。
  • 不可重複性:保證新增的元素按照equals()判斷時,不能返回true。相同的元素只能新增一個。
  • Set介面中沒有額外定義新的方法,使用的都是Collection中宣告過的方法。
  • 向Set中新增的資料,其所在的類一定要重寫hashCode()和equals()

(5.2)HashSet類

  • HashSet 是 Set 介面的典型實現,大多數時候使用 Set 集合時就是使用這個實現類。
  • HashSet 按 Hash 演算法來儲存集合中的元素,因此具有很好的存取、查詢、刪除效能
  • HashSet 特點
    • 不能保證元素的排列順序。順序可能與新增順序不同,順序也可能發生變化。
    • HashSet 不是執行緒安全的,如果多個執行緒同時訪問一個HashSet,假設有兩個或兩個以上執行緒同時修改了HashSet 集合時,必須通過程式碼來保證其執行緒安全。
    • 集合元素可以是 null。
  • HashSet 集合判斷兩個元素相等的標準: 兩個物件 通過 equals() 方法比較相等,hashCode() 方法返回值也相等。
  • 當把一個物件放入 HashSet 中時,如果需要重寫該物件對應類的 equals() 方法,也應該重寫 hashCode() 方法。
    • 規則是: 如果兩個物件通過 equals()方法 比較返回 true,這兩個物件的 hashCode 值也應該相同。相等的物件必須具有相等的雜湊碼。
  • 如果試圖把兩個相同的元素加入到同一個Set集合中,add() 方法返回false, 且新元素不會被加入,新增操作會失敗。


(5.2.1)HashSet新增元素過程

  • 向HashSet中新增元素a,
    首先呼叫元素a所在類的hashCode()方法,計算元素a的雜湊值;
  • 使用此雜湊值通過某種演算法計算出在HashSet底層陣列中的存放位置(即:索引位置),判斷陣列此位置上是否已經存在元素。
    • 如果此位置沒有其他元素,則元素a新增成功。-->成功情況1
    • 如果此位置有其他元素b(或以連結串列形式存在多個元素),則比較元素a與元素b的hash值:
      • 如果hash值不相同,則元素a新增成功。 -->成功情況2
      • 如果hash值相同,則呼叫元素a所在類的equals()方法: 返回true,元素a新增失敗;返回false,元素b新增成功。 -->成功情況3

對於新增成功的情況2和情況3而言:元素a與已經存在於指定索引位置上的資料以連結串列的形式儲存。

  • JDK 7:元素a放到陣列中,指向原來的元素。元素a插入到連結串列的頭部。
  • JDK 8:原來的元素在陣列中,指向元素a。 元素a插入連結串列的尾部。 七上八下。

(5.2.2)HashSet底層結構

底層也是陣列,初始容量為16,當使用率超過0.75(16*0.75=12),就會擴大容量為原來的2倍,(16,32,64,128......)
HashSet底層:陣列 +連結串列的結構。

(5.2.3)hashCode()重寫,為什麼選擇31

  • 選擇係數的時候要選擇儘量大的係數。因為如果計算出來的hash地址越大,所謂的衝突就越少,查詢起來效率也會提高。(減少衝突)
  • 並且31只佔用5bits, 相乘造成資料溢位的概率較小。
  • 31可以由 i*31 == (i<<5)-1來表示,現在很多虛擬機器裡面都有做相關優化。(提高演算法效率)
  • 31是一個素數,素數作用就是用一個數字來乘以這個素數,最終的結果只能被素數本身和被乘數還有1來整除!(減少衝突)

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

(5.3)LinkedHashSet

(5.4)TreeSet

  • TreeSet 是 SortedSet 介面的實現類。TreeSet 可以確保集合元素處於排序狀態。
  • TreeSet底層使用紅黑樹結構儲存資料。有序,查詢速度比List快。
  • TreeSet 並不是根據元素的插入順序進行排序的,而是根據元素實際值的大小來進行排序的。
  • 與 HashSet 集合相比, TreeSet 還提供了幾個額外的方法
    • Comparator comparator(): 如果TreeSet採用了定製程式,則該方法返回定製排序所使用的Comparator;如果TreeSet採用了自然排序, 則返回null。
    • Object first(): 返回集合中的第一個元素。
    • Object last(): 返回集合中的最後一個元素。
    • Object lower(Object e): 返回集合中位於指定元素之前的元素。即小於指定元素的最大元素,參考元素不需要是 TreeSet 集合裡的元素。
    • Object higher(Object e): 返回集合中位於指定元素之後的元素。即大於指定元素的最小元素,參考元素不需要是 TreeSet 集合裡的元素。
    • SortedSet subSet(Object fromElement, Object toElement): 返回此 Set 的子集合,範圍從 fromElement(包含) 到 toElement(不包括)。
    • SortedSet headSet(Object toElement): 返回此Set的子集,由小於 toElement 的元素組成。
    • SortedSet tailSet(Object fromElement): 返回此Set的子集,由大於或等於 fromElement 的元素組成。
  • TreeSet 支援兩種排序方法:自然排序(實現Comparable介面) 和定製排序(Comparator。 在預設情況下, TreeSet 採用自然排序。

(5.4.1)新增元素:要求是相同型別的物件

    @Test
    public void test2(){
        TreeSet set = new TreeSet();
        set.add(123);
        set.add(456);
//        set.add("aa");  //java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
//        set.add(new Person("Sun",12));
        set.add(332);
        set.add(132);
        System.out.println(set.lower(350)); // 332
        System.out.println(set.higher(300)); //332
        System.out.println(set);  // [123, 132, 332, 456]

        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());  // 123  132 332 456
        }
    }

(5.4.2)自然排序

比較兩個物件是否相同的標準:compareTo()返回0,不再是equals()

public class Person implements Comparable {
    ......
    @Override
    public int compareTo(Object o){  // 按照姓名從小到大排列
        if(o instanceof  Person){
            Person p = (Person) o;
            return -this.name.compareTo(p.name);
        }else {
            throw new RuntimeException("輸入的型別不匹配");
        }
    }
}

    @Test
    public void test3(){
        TreeSet set = new TreeSet();
        set.add(new Person("Aaaa",12));
        set.add(new Person("Abbb",12));
        set.add(new Person("Efrd",13));
        set.add(new Person("Greg",32));
        set.add(new Person("Dfgr",52));
        //[Person{name='Greg', age=32}, Person{name='Efrd', age=13}, Person{name='Dfgr', age=52}, Person{name='Abbb', age=12}, Person{name='Aaaa', age=12}]
        System.out.println(set);
    }

(5.4.3)定製排序

比較兩個物件是否相同的標準:compare()返回0,不再是equals()。

 @Test
    public void  test4(){
        Comparator com = new Comparator() {
            // 按照年齡從小到大
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof Person && o2 instanceof Person){
                    Person p1 = (Person) o1;
                    Person p2 = (Person) o2;
                    return Integer.compare(p1.getAge(), p2.getAge());
                }else {
                    throw new RuntimeException("輸入的型別不匹配");
                }
            }
        };
        TreeSet set = new TreeSet(com);
        set.add(new Person("Aaaa",12));
        set.add(new Person("Abbb",12));
        set.add(new Person("Efrd",13));
        set.add(new Person("Greg",32));
        set.add(new Person("Dfgr",52));
        // [Person{name='Aaaa', age=12}, Person{name='Efrd', age=13}, Person{name='Greg', age=32}, Person{name='Dfgr', age=52}]
        System.out.println(set);
    }

向 books 集合 中分別添加了 兩個A 物件,兩個B 物件, 兩個 C 物件。
其中 C 類 重寫了 equals()方法總是返回 true, hashCode() 方法總是返回2 ,
HashSet 把兩個C 物件當成了一個物件。
[A@7852e922, B@1, B@1, C@2, A@6d06d69c]



如果 兩個物件 equals 相同,hashCode 不同,兩個物件儲存在 Hash 表的不同位置,
都可以新增成功,與Set集合規則衝突。
如果兩個物件 hashCode 相同,equals 不同, HashSet 試圖將兩個物件放在同一個位置,但又不行(否則只剩一個物件)
實際上會在這個位置用鏈式結構來儲存多個物件,
而 HashSet 訪問集合元素時 根據元素的 hashCode 值來快速定位的, 兩個以上元素hasCode值相同,效能下降。

* hash 演算法
hash(雜湊、雜湊)演算法的功能:
保證快速查詢被檢索的物件,hash 演算法的價值在於 速度。
當需要 查詢集合中的某個元素時, hash 演算法可以直接根據 該元素的 hashCode 值計算出該元素的儲存位置,
從而快速定位該元素。

為了理解這個概念,可以先看陣列(陣列是所有能儲存一組元素裡最快的資料結構)。
陣列可以包含多個元素,每個元素都有索引,如果需要訪問某個陣列元素,只需提供該元素的索引。
接下來即可根據該索引計算該元素在記憶體裡的儲存位置。

表面看起來, HashSet 集合裡的元素都沒有索引,
實際上當程式向 HashSet集合中 新增元素時, HashSet 會根據該元素的 hashCode 值來計算它的儲存位置,
這樣也可快速定位該元素。

因為陣列元素的索引是 連續的,而且 陣列的長度是固定的, 無法自由新增陣列的長度。
而 HashSet 不一樣,HashSet 採用每個元素的 hashCode 值來計算其儲存位置,從而可以自由增加 HashSet 長度,
並可以根據元素的 hashCode 值來訪問元素。

當從 HashSet 中訪問元素時,HashSet 先計算 該元素的 hashCode 值,然後 直接到 該 hashCode值 對應的位置
取出該元素 -- 這就是HashSet 速度快的原因。

* HashSet 中 每個能 儲存元素的 槽位 slot 通常稱為 桶 bucket。
如果多個元素的 hashCode 值相同, equals返回false,那麼在一個 桶中存放多個元素,導致效能下降。

* 重寫 hashCode()方法 的規則:
在程式執行過程中, 同一個物件 多次呼叫 hashCode() 方法 應該返回相同的值。
當兩個物件通過 equals()方法比較返回true時, hashCode() 返回值也應該相同。
物件中用作 equals()方法比較標準的例項變數,都應該用於計算 hashCode 值。

* 重寫 hashCode()方法 的一般步驟:
1. 把物件內的每個有意義的例項變數計算出一個int型別的 hashCode值。
boolean hashCode = (f?0:1);
整數型別(byte/short/char/int) hashCode = (int)f;
long hashCode = (int)(f^(f>>>32));
float hashCode = Float.floatToIntBits(f);
double long l = Double.doubleToLongBits(f); hashCode = (int)(l^(l>>>32));
引用型別 hashCode = f.hashCode();

2. 用第一步計算出來的 多個 hashCode 值組合計算出 一個 hashCode 值 返回。
return f1.hashCode() + (int)f2;

為了避免直接相加產生偶然相等,(兩個物件的 f1、f2例項變數並不相等,它們的 hashCode 和恰好相等。)
可以通過為各例項變數的 hashCode 值 乘以 任意一個質數後再相加
return f1.hashCode() * 19 + (int) f2 * 31;


* 如果向 HashSet 中新增一個可變物件後,
後面程式修改了該可變物件的例項變數,則可能導致它與集合中其他元素相同,導致HashSet 中包含兩個相同的物件。
/*
HashSet hs = new HashSet();
hs.add(new R(5));
hs.add(new R(-3));
hs.add(new R(9));
hs.add(new R(-2));
System.out.println(hs);

Iterator it = hs.iterator();
R first = (R)it.next();
first.count = -3;
System.out.println(hs);
hs.remove(new R(-3));
System.out.println(hs);
System.out.println("hs 是否包含 count 為 -3 的物件?" + hs.contains(new R(-3)));
System.out.println("hs 是否包含 count 為 -2 的物件?" + hs.contains(new R(-2)));

E:\javap\crazyit8>java HashSetTest2
[R[count:-2], R[count:-3], R[count:5], R[count:9]]
[R[count:-3], R[count:-3], R[count:5], R[count:9]]
[R[count:-3], R[count:5], R[count:9]]
hs 是否包含 count 為 -3 的物件?false
hs 是否包含 count 為 -2 的物件?false
*/

R類重寫了 equals(Object obj) 和 hashCode() 方法, 這兩個方法都是根據 R 物件的 count 例項變數來判斷的。
first 語句 改變了 Set 集合中第一個R 物件的 count 例項變數的值,導致該R 物件與集合中其他物件相同。

HashSet 集合中 第一個元素和第二個元素完全相同,表明兩個元素已經重複。此時 HashSet 會比較混亂:
當刪除 count 為 -3 的 R 物件時,HashSet 計算出 該物件的HashCode 值,從而找出物件在集合中的儲存位置。
然後把此處的物件 與count 為 -3 的 R 物件通過 equals()方法 進行比較, 如果相等則刪除該物件。
HashSet 只有第2個元素 滿足條件,(第1個元素 實際上儲存count為-2的R 物件對應的位置),所以刪除第2個元素。

第一個count 為 -3 的R 物件,它儲存在 count為 -2 的 R物件對應的位置,
使用 equals() 與 count 為 -2 的R 物件比較時 返回 false -- 導致HashSet 不可能正確訪問 該元素。

當程式把可變物件新增到HashSet 中之後,儘量不要 去修改該集合元素中參與計算 hashCode() equals()的例項變數,
否則將會導致 HashSet無法正確操作 這些集合元素。

(8.3.2) LinkedHashSet 類
* HashSet 還有一個子類 LinkedHashSet,
LinkedHashSet 集合 也是 根據元素的 hashCode 值 來決定元素的儲存位置,
但它同時使用連結串列維護元素的次序,這使得元素看起來是以插入的順序儲存的。

當遍歷 LinkedHashSet 集合裡的元素時,LinkedHashSet 將會按元素的新增順序來訪問 集合裡的元素。

LinkedHashSet 需要維護元素的插入順序,因此效能略低於 HashSet 效能。
但在迭代訪問 Set 集合裡的全部元素時 將會有很好的效能,因為它 以連結串列來維護內部順序。
/*
LinkedHashSet books = new LinkedHashSet();

books.add("瘋狂 Java 講義");
books.add("輕量級 Java EE 企業應用實戰");
System.out.println(books);

books.remove("瘋狂 Java 講義");
books.add("瘋狂 Java 講義");
System.out.println(books);

E:\javap\crazyit8>java LinkedHashSetTest
[瘋狂 Java 講義, 輕量級 Java EE 企業應用實戰]
[輕量級 Java EE 企業應用實戰, 瘋狂 Java 講義]
*/


(8.3.4) EnumSet 類
* EnumSet 是一個專為列舉類設計 的集合類,EnumSet 中所有元素都必須是指定列舉型別的列舉值。該列舉型別在建立 EnumSet 時顯式或隱式地指定。
EnumSet 的 集合元素也是有序的, EnumSet 以列舉值 在 Enum類內的定義順序來決定 集合元素的順序。

* EnumSet 在內部以位向量 的形式儲存, 這種儲存形式 非常緊湊、高效,EnumSet 物件佔用記憶體很小, 而且執行效率很好。
尤其是 進行批量操作( 呼叫 containsAll() 和 retainAll() 方法)時,如果引數也是 EnumSet 集合,則批量操作的執行速度也非常快。

* EnumSet 集合不允許 加入null 元素, 如果試圖插入 null 元素, EnumSet 將丟擲 NullPointerException 異常。
如果只是 想判斷 EnumSet 是否包含 null 元素或試圖 刪除 null 元素 都不會丟擲異常,只是刪除操作將返回 false, 因為沒有任何null 元素被刪除。

* EnumSet 類沒有 暴露任何構造器 來建立該類的例項,程式應該通過它提供的類方法 來建立 EnumSet 物件。EnumSet 類提供的類方法:
EnumSet allOf(Class elementType): 建立一個包含指定列舉類裡所有列舉值的 EnumSet 集合。
EnumSet complementOf(EnumSet s): 建立一個其元素型別與 指定 EnumSet 裡元素型別相同的 EnumSet 集合,
新 EnumSet 集合包含 原 EnumSet 集合所不包含的、此列舉類剩下的列舉值
(即 新EnumSet 集合 和原 EnumSet 集合的集合元素加起來就是 該列舉類的所有列舉值) <差集>

EnumSet copyOf(Collection c): 使用 一個普通集合來建立 EnumSet 集合。
EnumSet copyOf(EnumSet s): 建立一個與指定 EnumSet 具有相同元素型別、相同集合元素的 EnumSet 集合。
EnumSet noneOf(Class elementType): 建立一個元素型別為指定列舉型別的 空 EnumSet.
EnumSet of(E first, E... rest): 建立一個包含一個或多個列舉值的 EnumSet集合,傳入的多個列舉值 必須屬於 同一個列舉類。
EnumSet range(E from, E to): 建立一個包含從 from 列舉值 到 to 列舉值 範圍內 所有列舉值的 EnumSet 集合。

/*
EnumSet es1 = EnumSet.allOf(Season.class);
System.out.println(es1);

EnumSet es2 = EnumSet.noneOf(Season.class);
System.out.println(es2);

es2.add(Season.WINTER);
es2.add(Season.SPRING);
System.out.println(es2);

EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
System.out.println(es3);

EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
System.out.println(es4);

EnumSet es5 = EnumSet.complementOf(es4);
System.out.println(es5);

E:\javap\crazyit8>java EnumSetTest
[SPRING, SUMMER, FALL, WINTER]
[]
[SPRING, WINTER]
[SUMMER, WINTER]
[SUMMER, FALL, WINTER]
[SPRING]
*/

* 複製 另一個 EnumSet 集合中的所有元素 來建立 新的 EnumSet 集合,或
複製 另一個 Collection 集合中 的所有元素 來建立新的 EnumSet 集合。

當複製 Collection 集合中的 所有元素 來 建立新的 EnumSet 集合時, 要求 Collection 集合中的所有元素 都必須是 同一個 列舉類的列舉值。
/*
Collection c = new HashSet();
c.clear();
c.add(Season.FALL);
c.add(Season.SPRING);

EnumSet enumSet = EnumSet.copyOf(c); // 複製 Collection 集合中的所有元素 來建立 EnumSet 集合
System.out.println(enumSet);

c.add("瘋狂 Java 講義");
c.add("瘋狂 IOS 講義");
System.out.println(c);

enumSet = EnumSet.copyOf(c); // 下面程式碼出現異常, 因為 c 集合中的元素 並不是 全部都是 列舉值

E:\javap\crazyit8>java EnumSetTest2
[SPRING, FALL]
[SPRING, FALL, 瘋狂 IOS 講義, 瘋狂 Java 講義]
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Enum
at java.util.RegularEnumSet.add(Unknown Source)
at java.util.EnumSet.copyOf(Unknown Source)
at EnumSetTest2.main(EnumSetTest2.java:21)
*/

(8.3.5) 各 Set 實現類的效能分析
* HashSet 和 TreeSet 是 Set 的兩個典型實現。
HashSet 的效能總是比 TreeSet 好(特別是 最常用的新增、查詢元素等操作),因為 TreeSet 需要額外的 紅黑樹演算法來維護集合元素的次序。
只有當需要 一個保持 順序的 Set 時, 才應該使用 TreeSet, 否則都應該使用 HashSet。

HashSet 還有一個子類: LinkedHashSet,
對於普通的插入、刪除操作, LinkedHashSet 比 HashSet 要略微慢一點。這是由維護 連結串列所帶來的額外開銷造成的。
由於有了連結串列, 遍歷 LinkedHashSet 會更快。

EnumSet 是所有 Set 實現類 中效能最好的, 但它只能儲存同一個列舉類的列舉值作為集合元素。

Set 的三個實現類 HashSet TreeSet EnumSet 都是執行緒不安全的。
如果有多個執行緒同時訪問 一個Set 集合, 有超過一個執行緒修改了 Set集合,則必須手動保證該 Set 集合的同步性。

通常可以通過 Collections 工具類的 synchronizedSortedSet 方法來“包裝”該 Set 集合。
此操作最好在建立時 進行, 以防止 對Set 集合的意外非同步訪問。
SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));