Set集合以及其實現類


Set集合

Set集合類似於一個罐子,不記錄添加元素的添加順序,只是不允許包含重復元素(重復的判定在不同的實現類中可能有些區別。

HashSet類

HashSet具有很好的存取和查找性能。

HashSet有以下特點:

  • 不能保證元素的排列順序,順序可能與添加順序不同,順序也可能發生變化
  • HashSet 不是同步的,如果多個線程訪問同一個HashSet ,並且有兩個或兩個以上的線程同時修改HashSet,則必須通過代碼來保證同步
  • 集合元素值可以是null

Set像是一個罐子,記不住添加元素的順序,所以查找的時候只能根據元素本身的屬性去查找,因此Set不允許包含重復元素(這個重復的判定在不同的實現類中可能有細微的差別)

import java.util.HashSet;

class A{
    @Override
    public boolean equals(Object obj) {
        return true;
    }

    @Override
    public int hashCode() {
        return 1;
    }
}

class B{
    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

class C{
    @Override
    public int hashCode() {
        return 2;
    }
}

public class HashSetTest {
    public static void main(String[] args) {

        HashSet s = new HashSet();
        System.out.println(s.add(null));
        System.out.println(s.add(null));
        s.forEach(ele-> System.out.println(ele));

        HashSet h = new HashSet();
        System.out.println(h.add(new A()));
        System.out.println(h.add(new A()));
        System.out.println(h.add(new B()));
        System.out.println(h.add(new B()));
        System.out.println(h.add(new C()));
        System.out.println(h.add(new C()));
        System.out.println(h);
    }
}

輸出結果

true
false
null
true	//可以看出 HashSet集合判斷兩個元素相等的標准是:兩個對象通過equals()方法比較得到true,並且hashCode()方法的返回值也相等	
false
true	//equals()返回true,但hashCode()返回值不相等時,會把它們保存在不同位置,但這違反了Set集合的“不包含重復元素”的規則
true
true	//equals()返回false,但hashCode()返回值相等時,會把它們用鏈式結構保存在一個位置,這樣會導致在訪問時性能下降
true	//(兩個以上的元素具有相同的hashCode值)
[A@1, C@2, C@2, B@54a097cc, B@36f6e879]

基於hashCode()和equals()方法對於HashSet的重要性,對於HashSet中的對象,要遵循以下重寫這兩個方法的基本規則:

  • hashCode()每次運行的返回值應該一樣
  • 當兩個對象通過equals()方法返回true時,這兩個對象的hashCode()方法應該返回相同的值
  • 對象中用作equals()方法比較標准的實例變量,都應該用於計算hashCode值

重寫案例

import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;

class R{
    Double ooo;
    public R(double d){
        this.ooo = d;
    }

    @Override
    public String toString() {
        return "R{" +
                "ooo=" + ooo +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        R r = (R) o;
        return Objects.equals(ooo, r.ooo);
    }

    @Override
    public int hashCode() {
        long l = Double.doubleToLongBits(this.ooo);
        return (int)(1^(l>>>32));
    }
}
public class HashSetTest2 {
    public static void main(String[] args) {
        HashSet hs = new HashSet();
        hs.add(new R(1.1));
        // 整一個迭代器,把第一個元素取出來,沒有進入迭代過程所以可以修改元素
        // (這也算不上修改吧,迭代過程中也能修改元素,
        // 但除了迭代器的remove()方法外不能用其他方法添加或刪減元素)
        Iterator it = hs.iterator();
        R first = (R)it.next();
        first.ooo = 2.2;
        System.out.println(hs);
        // contains(Object o)方法:HashSet集合會先得到o的hashCode,然后到自己集合中對應的地方去找,
        // 找到后在通過equals()方法去比較,得到true后返回
        System.out.println(hs.contains(new R(1.1)));//因為equals返回了false
        System.out.println(hs.contains(new R(2.2)));//壓根就沒找到R(2.2)對應的hashCode
        /*
        	由此可見,對於已經添加進去的對象,盡量不要修改關於該元素參與計算hashCode()和equals()的實例變量,
        	否則,像上面的hs中的那一個對象,既無法通過contains(1.1)訪問到,也無法通過contains(2.2)訪問到
        	這樣會給集合操作這些元素帶來很大麻煩
        */
    }
}

輸出結果

[R{ooo=2.2}]
false
false

LinkedHashSet類

相對於HashSet的區別:

LinkedHashSet 是HashSet的子類,LinkedHashSet集合也是根據元素的hashCode值來決定元素的存儲位置,但它同時維護元素存儲的次序。
當遍歷LinkedHashSet集合里的元素時,LinkedHashSet將會按元素的添加順序來訪問集合中的元素。

import java.util.LinkedHashSet;

public class LinkedHashSetTest {
    public static void main(String[] args) {
        LinkedHashSet ls = new LinkedHashSet();
        ls.add("a");
        ls.add("b");
        ls.add("c");
        ls.add("d");
        System.out.println(ls);
        ls.remove("c");
        System.out.println(ls);
    }
}
[a, b, c, d]
[a, b, d]

此實現使客戶擺脫了HashSet提供的未指定的,通常混亂的排序,而不會導致與TreeSet相關的增加的成本。無論原始集的實現如何,都可以使用它來產生與原始集具有相同順序的集合副本:

  void foo(Set s) {
         Set copy = new LinkedHashSet(s);
         ...
     }

性能

由於需要維護元素的插入順序(其實是維護鏈式列表),LinkedHashSet相對於HashSet會增加開銷,因此執行添加、刪除等操作時性能會輕微下降,但當迭代的時候LinkedHashSet集合的迭代時間與集合的大小成正比,而HashSet集合的迭代時間與集合的容量成正比,因此LinkedHashSet的迭代性能要好一些。

當構造HashSet集合時,有兩個參數,一個是初始化容量(默認是16),一個是負載因子(默認值是0.75),因為HashSet的迭代性能和容量成正比,所以我們通常不會設置太大的容量,默認或者更低。
但當構造LinkedHashSet時,選擇初始化容量過高的值的副作用不會那么嚴重,因為其迭代性能與集合大小成正比。

TreeSet類

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet s = new TreeSet();
        s.add(1);
        s.add(2);
        s.add(3);
        s.add(4);
        System.out.println(s);
        System.out.println(s.first());  //1
        System.out.println(s.last());   //4
        System.out.println(s.headSet(3));   //1,2
        System.out.println(s.tailSet(2));   //2,3,4
        System.out.println(s.subSet(1,3)); //1,2,3
    }
}

可以看出(好像也看不太出。。沒事),TreeSet並不是根據元素的插入順序進行排序的,而是根據元素的實際值大小來排序的
與HashSet集合采用hash算法來決定元素的存儲位置不同,TreeSet采用紅黑樹的數據結構來存儲集合元素。
TreeSet支持兩種排序規則:自然排序和定制排序。在默認情況下,TreeSet采用自然排序。

TreeSet自然排序

自然排序:TreeSet會調用集合元素的compareTo(Object obj)方法來比較元素之間的大小關系,然后將集合元素按升序排列。

a.compareTo(b)方法來自Comparable接口,該方法返回一個整數:
返回0:兩個對象相等
返回1:a大於b
返回-1:a小於b
Java的一些類已經實現了Comparable接口,並提供了比較大小的標准:

  • BigDemical、BigInteger以及所有的數值型對應的包裝類:按他們的大小進行比較
  • Character:按字符的Unicode值進行比較
  • Boolean:true對應的包裝類實例大於false對應的包裝類實例
  • String:按字符串中字符的Unicode值進行比較
  • Data、Time:(后面的時間、日期) 比 (前面的時間、日期大)
    //下面程序試圖向TreeSet集合中添加兩個Err對象,添加第二個對象時,TreeSet會調用
    //該對象的compareTo(Object o)方法與集合中的其他元素進行比較——
    //如果其對應類沒有實現Comparable接口,則會引發ClassCastException異常
    TreeSet ts = new TreeSet();
    ts.add(new Error());
    ts.add(new Error());
    /**
    *而且,大部分類在實現compareTo(Object o)方法的時候,都需要將被比較的對象o強轉為相同類型,因為只有兩個相同類型才能比較大小。
    *因此,向TreeSet里添加的應該是同一個類的對象,不然容易引發ClassCastException異常
    **/

總結:如果希望TreeSet能夠正常工作,TreeSet集合中只能存同一類對象。

HashSet和TreeSet中可變元素的實例變量盡量不要修改(參與相等判斷的)

對於TreeSet而言,判斷兩個元素是否相等的唯一標准是:兩個對象通過compareTo(Object o)返回的整數是不是0。

import java.util.TreeSet;

class P implements Comparable{

    int num;

    public P(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "P{" +
                "num=" + num +
                '}';
    }

    @Override
    public int compareTo(Object o) {
        P p = (P)o;
        return this.num>p.num? 1: this.num==p.num?0:-1;
    }
}

public class TreeSet2 {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        ts.add(new P(1));
        ts.add(new P(5));
        ts.add(new P(7));
        ts.add(new P(9));
        System.out.println(ts);
        P first = (P)ts.first();
        first.num = 100;
        P last = (P)ts.last();
        last.num = 7;
        System.out.println(ts);

        System.out.println(ts.remove(new P(1)));
        System.out.println(ts.remove(new P(100)));
        System.out.println(ts);
        System.out.println(ts.remove(new P(5)));//①
        System.out.println(ts);
        /**
		 * 添加后的元素如果實例變量被修改(而且這個實例變量能夠影響compareTo()方法的判斷,TreeSet也不會重新排序
		 * 反而可能會造成出現重復的元素,導致兩個都無法被刪除,給操作帶來困難
		 * 
         * 一旦修改了TreeSet集合里可變元素的實例對象,當再試圖刪除該對象時,TreeSet也會刪除失敗
         * 甚至連集合中原有的、實例變量沒有被修改但與修改后的元素相等的元素也無法被刪除
         *
         * 說明TreeSet可以刪除沒有被修改實例變量、且不與其他修改實例變量的對象重復的對象
         * 
         * 當執行上面①行代碼時,TreeSet會對集合中的元素重新索引(不是重新排序)。接下來
         * 就可以刪除TreeSet中的其他元素了。包括那些被修改過的實例變量的元素。
         */
    }
}
[P{num=1}, P{num=5}, P{num=7}, P{num=9}]
[P{num=100}, P{num=5}, P{num=7}, P{num=7}]
false
false
[P{num=100}, P{num=5}, P{num=7}, P{num=7}]
true
[P{num=100}, P{num=7}, P{num=7}]

與HashSet類似的是,如果TreeSet中包含了可變對象,當可變對象的實例變量被修改時,TreeSet在處理這些對象將非常復雜,而且容易出錯.為了讓程序更加健壯,不要修改放入HashSet和TreeSet集合中的關鍵實例變量(與CompareTo()方法相關的).

定制排序

TreeSet如果想要實現定制排序,則可以通過Comparator接口的幫助。該接口里包含一個int compare(Object o1,Object o2)方法,o1 == o2 返回0;o1>o2 返回1;o1<o2 返回-1;
通過提供一個Comparator對象與TreeSet關聯,由該Comparator對象負責集合的排序邏輯。
由於Comparator是一個函數式接口,因此可使用Lambda表達式來代替Comparator對象。

import java.util.TreeSet;

class N{
    int age;

    public N(int age) {
        this.age = age;
    }
}

class  M{
    int age;

    public M(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "M{" +
                "age=" + age +
                '}';
    }
}

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet((o1,o2)->{
            M m1 = (M)o1;
            M m2 = (M)o2;
            //根據M對象的age屬性來決定大小,age越大,M對象反而越小
            return m1.age>m2.age? -1:m1.age<m2.age?1:0;
        });
        ts.add(new M(5));
        ts.add(new M(9));
        ts.add(new M(10));
        ts.add(new M(1));
//        ts.add(new N(90));  ClassCastException異常 所以還是不要添加不同類的對象
        System.out.println(ts);
    }
}
[M{age=10}, M{age=9}, M{age=5}, M{age=1}]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM