由淺入深理解java集合(二)——集合 Set

上一篇文章介紹了Set集合的通用知識(shí)。Set集合中包含了三個(gè)比較重要的實(shí)現(xiàn)類:HashSet、TreeSet和EnumSet。本篇文章將重點(diǎn)介紹這三個(gè)類。

一、HashSet類

HashSet簡(jiǎn)介

HashSet是Set接口的典型實(shí)現(xiàn),實(shí)現(xiàn)了Set接口中的所有方法,并沒(méi)有添加額外的方法,大多數(shù)時(shí)候使用Set集合時(shí)就是使用這個(gè)實(shí)現(xiàn)類。HashSet按Hash算法來(lái)存儲(chǔ)集合中的元素。因此具有很好的存取和查找性能。

HashSet特點(diǎn)

1.不能保證元素的排列順序,順序可能與添加順序不同,順序也有可能發(fā)生變化。
2.HashSet不是同步的,如果多個(gè)線程同時(shí)訪問(wèn)一個(gè)HashSet,則必須通過(guò)代碼來(lái)保證其同步。
3.集合元素值可以是null。
除此之外,HashSet判斷兩個(gè)元素是否相等的標(biāo)準(zhǔn)也是其一大特點(diǎn)。HashSet集合判斷兩個(gè)元素相等的標(biāo)準(zhǔn)是兩個(gè)對(duì)象通過(guò)equals()方法比較相等,并且兩個(gè)對(duì)象的hashCode()方法返回值也相等。

寫(xiě)到這里,我們就要介紹下equals()和hashCode()方法了。

equals()和hashCode()

equals()

equals() 的作用是** 用來(lái)判斷兩個(gè)對(duì)象是否相等。**

equals() 定義在JDK的Object.java中。通過(guò)判斷兩個(gè)對(duì)象的地址是否相等(即,是否是同一個(gè)對(duì)象)來(lái)區(qū)分它們是否相等。源碼如下:

public boolean equals(Object obj) {
       return (this == obj);
   }

既然Object.java中定義了equals()方法,這就意味著所有的Java類都實(shí)現(xiàn)了equals()方法,所有的類都可以通過(guò)equals()去比較兩個(gè)對(duì)象是否相等。 但是,使用默認(rèn)的“equals()”方法,等價(jià)于“==”方法。我們也可以在Object的子類中重寫(xiě)此方法,自定義“equals()”方法,在其中定義自己的判斷邏輯,如果滿足則返回true,不滿足則返回false。下面我們自定義一個(gè)類 Person,并認(rèn)為年齡,身高相等的兩個(gè)Person對(duì)象,equals()方法比較結(jié)果相等。

public class Person {
    public int age;
    public int height;
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        if (age != other.age)
            return false;
        if (height != other.height)
            return false;
        return true;
    }
}
public class EqualTest {
  public static void main(String[] args){
      Person p1 = new Person();
      Person p2 =new Person();
      System.out.println(p1.equals(p2));
  }

}

輸出結(jié)果:

true

下面根據(jù)“類是否覆蓋equals()方法”,將它分為2類。
(01) 若某個(gè)類沒(méi)有覆蓋equals()方法,當(dāng)它的通過(guò)equals()比較兩個(gè)對(duì)象時(shí),實(shí)際上是比較兩個(gè)對(duì)象是不是同一個(gè)對(duì)象。這時(shí),等價(jià)于通過(guò)“==”去比較這兩個(gè)對(duì)象,即兩個(gè)對(duì)象的內(nèi)存地址是否相同。
(02) 我們可以覆蓋類的equals()方法,來(lái)讓equals()通過(guò)其它方式比較兩個(gè)對(duì)象是否相等。通常的做法是:若兩個(gè)對(duì)象的內(nèi)容相等,則equals()方法返回true;否則,返回fasle。

hashCode()

hashCode() 的作用是獲取哈希碼,也稱為散列碼;它實(shí)際上是返回一個(gè)int整數(shù)。這個(gè)哈希碼的作用是確定該對(duì)象在哈希表中的索引位置。

hashCode() 定義在JDK的Object.java中,這就意味著Java中的任何類都包含有hashCode() 函數(shù)。雖然,每個(gè)Java類都包含hashCode() 函數(shù)。但是,僅僅當(dāng)創(chuàng)建某個(gè)“類"的散列表時(shí),該類的hashCode() 才有用。更通俗地說(shuō)就是創(chuàng)建包含該類的HashMap,Hashtable,HashSet集合時(shí),hashCode() 才有用。因?yàn)镠ashMap,Hashtable,HashSet就是散列表集合。
在散列表中,hashCode()作用是:確定該類的每一個(gè)對(duì)象在散列表中的位置;其它情況下類的hashCode() 沒(méi)有作用。在散列表中hashCode() 的作用是獲取對(duì)象的散列碼,進(jìn)而確定該對(duì)象在散列表中的位置。

hashCode()也分兩種情況。一種是Object類中默認(rèn)的方法,另一種是在子類中重寫(xiě)的方法。
(01) 若某個(gè)類沒(méi)有覆蓋hashCode()方法,當(dāng)它的通過(guò)hashCode()比較兩個(gè)對(duì)象時(shí),實(shí)際上是比較兩個(gè)對(duì)象是不是同一個(gè)對(duì)象。這時(shí),等價(jià)于通過(guò)“==”去比較這兩個(gè)對(duì)象,即兩個(gè)對(duì)象的內(nèi)存地址是否相同。
(02) 我們可以覆蓋類的hashCode()方法,來(lái)讓hashCode()通過(guò)其它方式比較兩個(gè)對(duì)象是否相等。通常的做法是:若兩個(gè)對(duì)象的內(nèi)容相等,則hashCode()方法返回true;否則,返回fasle。

通過(guò)對(duì)以上兩個(gè)方法的了解。我們可以接下來(lái)學(xué)習(xí)HashSet集合中如何判斷兩個(gè)元素是否相等?

HashSet中判斷集合元素相等

兩個(gè)對(duì)象比較 具體分為如下四個(gè)情況:
1.如果有兩個(gè)元素通過(guò)equal()方法比較返回false,但它們的hashCode()方法返回不相等,HashSet將會(huì)把它們存儲(chǔ)在不同的位置。

2.如果有兩個(gè)元素通過(guò)equal()方法比較返回true,但它們的hashCode()方法返回不相等,HashSet將會(huì)把它們存儲(chǔ)在不同的位置。

3.如果兩個(gè)對(duì)象通過(guò)equals()方法比較不相等,hashCode()方法比較相等,HashSet將會(huì)把它們存儲(chǔ)在相同的位置,在這個(gè)位置以鏈表式結(jié)構(gòu)來(lái)保存多個(gè)對(duì)象。這是因?yàn)楫?dāng)向HashSet集合中存入一個(gè)元素時(shí),HashSet會(huì)調(diào)用對(duì)象的hashCode()方法來(lái)得到對(duì)象的hashCode值,然后根據(jù)該hashCode值來(lái)決定該對(duì)象存儲(chǔ)在HashSet中存儲(chǔ)位置。

4.如果有兩個(gè)元素通過(guò)equal()方法比較返回true,但它們的hashCode()方法返回true,HashSet將不予添加。

HashSet判斷兩個(gè)元素相等的標(biāo)準(zhǔn):兩個(gè)對(duì)象通過(guò)equals()方法比較相等,并且兩個(gè)對(duì)象的hashCode()方法返回值也相等。

注意:HashSet是根據(jù)元素的hashCode值來(lái)快速定位的,如果HashSet中兩個(gè)以上的元素具有相同的hashCode值,將會(huì)導(dǎo)致性能下降。所以如果重寫(xiě)類的equals()方法和hashCode()方法時(shí),應(yīng)盡量保證兩個(gè)對(duì)象通過(guò)hashCode()方法返回值相等時(shí),通過(guò)equals()方法比較返回true。

LinkedHashSet類

LinkedHashSet是HashSet對(duì)的子類,也是根據(jù)元素的hashCode值來(lái)決定元素的存儲(chǔ)位置,同時(shí)使用鏈表維護(hù)元素的次序,使得元素是以插入的順序來(lái)保存的。當(dāng)遍歷LinkedHashSet集合里的元素時(shí),LinkedHashSet將會(huì)按元素的添加順序來(lái)訪問(wèn)集合里的元素。但是由于要維護(hù)元素的插入順序,在性能上略低與HashSet,但在迭代訪問(wèn)Set里的全部元素時(shí)有很好的性能。
注意:LinkedHashSet依然不允許元素重復(fù),判斷重復(fù)標(biāo)準(zhǔn)與HashSet一致。

補(bǔ)充:HashSet的實(shí)質(zhì)是一個(gè)HashMap。HashSet的所有集合元素,構(gòu)成了HashMap的key,其value為一個(gè)靜態(tài)Object對(duì)象。因此HashSet的所有性質(zhì),HashMap的key所構(gòu)成的集合都具備。可以參考后續(xù)文章中HashMap的相關(guān)內(nèi)容進(jìn)行比對(duì)。

二、TreeSet類

TreeSet簡(jiǎn)介

TreeSet是SortedSet接口的實(shí)現(xiàn)類,正如SortedSet名字所暗示的,TreeSet可以確保集合元素處于排序狀態(tài)。此外,TreeSet還提供了幾個(gè)額外的方法。

TreeSet的方法

comparator():返回對(duì)此 set 中的元素進(jìn)行排序的比較器;如果此 set 使用其元素的自然順序,則返回null。
first():返回此 set 中當(dāng)前第一個(gè)(最低)元素。
last(): 返回此 set 中當(dāng)前最后一個(gè)(最高)元素。
lower(E e):返回此 set 中嚴(yán)格小于給定元素的最大元素;如果不存在這樣的元素,則返回 null。
higher(E e):返回此 set 中嚴(yán)格大于給定元素的最小元素;如果不存在這樣的元素,則返回 null。
subSet(E fromElement, E toElement):返回此 set 的部分視圖,其元素從 fromElement(包括)到 toElement(不包括)。
headSet(E toElement):返回此 set 的部分視圖,其元素小于toElement。
tailSet(E fromElement):返回此 set 的部分視圖,其元素大于等于 fromElement。

TreeSet的排序方式

TreeSet中所謂的有序,不同于之前所講的插入順序,而是通過(guò)集合中元素屬性進(jìn)行排序方式來(lái)實(shí)現(xiàn)的。
TreeSet支持兩種排序方法:自然排序和定制排序。在默認(rèn)情況下,TreeSet采用自然排序。

1.自然排序

在講自然排序之前,要先講一下Comparable接口

Java提供了一個(gè)Comparable接口,該接口里定義了一個(gè)compareTo(Object obj)方法,該方法返回一個(gè)整數(shù)值,實(shí)現(xiàn)該接口的類必須實(shí)現(xiàn)該方法,實(shí)現(xiàn)了該接口的類的對(duì)象就可以比較大小了。當(dāng)一個(gè)對(duì)象調(diào)用該方法與另一個(gè)對(duì)象比較時(shí),例如obj1.compareTo(obj2),如果該方法返回0,則表明兩個(gè)對(duì)象相等;如果該方法返回一個(gè)整數(shù),則表明obj1大于obj2;如果該方法返回一個(gè)負(fù)整數(shù),則表明oj1小于obj2。

TreeSet會(huì)調(diào)用集合中元素所屬類的compareTo(Object obj)方法來(lái)比較元素之間的大小關(guān)系,然后將集合元素按升序排列,即把通過(guò)compareTo(Object obj)方法比較后比較大的的往后排。這種方式就是自然排序。

Java的一些常用類已經(jīng)實(shí)現(xiàn)了Comparable接口,并提供了比較大小的標(biāo)準(zhǔn)。例如,String按字符串的UNICODE值進(jìn)行比較,Integer等所有數(shù)值類型對(duì)應(yīng)的包裝類按它們的數(shù)值大小進(jìn)行比較。
除了這些已經(jīng)實(shí)現(xiàn)Comparable接口類之外,如果試圖把一個(gè)對(duì)象添加到TreeSet時(shí),則該對(duì)象的類必須實(shí)現(xiàn)Comparable接口,否則就會(huì)出現(xiàn)異常。
注意:TreeSet中只能添加同一種類型的對(duì)象,否則無(wú)法比較,會(huì)出現(xiàn)異常。

TreeSet中判斷集合元素相等

對(duì)于TreeSet集合而言,判斷兩個(gè)對(duì)象是否相等的唯一標(biāo)準(zhǔn)是:兩個(gè)對(duì)象通過(guò)compareTo(Object obj)方法比較是否返回0——如果通過(guò)compareTo(Object obj)方法比較返回0,TreeSet則會(huì)認(rèn)為它們相等,不予添加入集合內(nèi);否則就認(rèn)為它們不相等,添加到集合內(nèi)。
TreeSet是根據(jù)紅黑樹(shù)結(jié)構(gòu)找到集合元素的存儲(chǔ)位置。

2.定制排序

TreeSet的自然排序是根據(jù)集合元素中compareTo(Object obj)比較的大小,以升序排列。而定制排序是通過(guò)Comparator接口的幫助。該接口包含一個(gè)int compare(T o1,T o2)方法,該方法用于比較o1,o2的大小:如果該方法返回正整數(shù),則表明o1大于o2;如果該方法返回0,則表明o1等于o2;如果該方法返回負(fù)整數(shù),則表明o1小于o2。
如果要實(shí)現(xiàn)定制排序,則需要在創(chuàng)建TreeSet時(shí),調(diào)用一個(gè)帶參構(gòu)造器,傳入Comparator對(duì)象。并有該Comparator對(duì)象負(fù)責(zé)集合元素的排序邏輯,集合元素可以不必實(shí)現(xiàn)Comparable接口。下面具體演示一下這種用法:

public static void main(String[] args){
        Person p1 = new Person();
        p1.age =20;
        Person p2 =new Person();
        p2.age = 30;
        Comparator<Person> comparator = 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 -1;
                }else{
                    return 0;
                }
                
            }
        };
        TreeSet<Person> set = new TreeSet<Person>(comparator);
        set.add(p1);
        set.add(p2);
        System.out.println(set);
    }

[Person[age=30], Person[age=20]]

總結(jié):無(wú)論使用自然排序還是定制排序,都可以通過(guò)自定義比較邏輯實(shí)現(xiàn)各種各樣的排序方式。

注意:如果向TreeSet中添加了一個(gè)可變對(duì)象后,并且后面程序修改了該可變對(duì)象的實(shí)例變量,這將導(dǎo)致它與其他對(duì)象的大小順序發(fā)生了改變,但TreeSet不會(huì)再次調(diào)整它們。下面程序演示這一現(xiàn)象:

TreeSet<Person> set = new TreeSet<Person>();
        Person p1 = new Person();
        p1.setAge(10);
        Person p2 =new Person();
        p2.setAge(30);
        Person p3 =new Person();
        p3.setAge(40);
        set.add(p1);
        set.add(p2);
        set.add(p3);
        System.out.println("初始年齡排序");
        System.out.println(set);
        //p1的年齡修改成50 最大
        p1.age = 60;
        System.out.println("修改p1年齡后集合排序");
        System.out.println(set);
        p2.age = 40;
        System.out.println("修改p2年齡后集合排序");
        System.out.println(set);
        Person p4 = new Person();

其中Person實(shí)現(xiàn)Comparable接口,將Person對(duì)象按照年齡從小到大升序排列。
輸出結(jié)果:

初始年齡排序
[Person[age=10], Person[age=30], Person[age=40]]
修改p1年齡后集合排序
[Person[age=60], Person[age=30], Person[age=40]]
修改p2年齡后集合排序
[Person[age=60], Person[age=40], Person[age=40]]

可以看到并沒(méi)有發(fā)生變化,而且如果修改后進(jìn)行元素刪除操作可能會(huì)不成功,具體比較復(fù)雜。總之,推薦不要修改放入TreeSet集合中元素的關(guān)鍵實(shí)例變量。
補(bǔ)充:TreeSet也是非線程安全的。

三、EnumSet類

EnumSet簡(jiǎn)介

EnumSet是一個(gè)專為枚舉類設(shè)計(jì)的集合類,EnumSet中的所有元素都必須是指定枚舉類型的枚舉值,該枚舉類型在創(chuàng)建EnumSet時(shí)顯示或隱式地指定。EnumSet的集合元素也是有序的,EnumSet以枚舉值在EnumSet類內(nèi)的定義順序來(lái)決定集合元素的順序。

EnumSet特點(diǎn)

1.EnumSet集合不允許加入null元素。EnumSet中的所有元素都必須是指定枚舉類型的枚舉值。
2.EnumSet類沒(méi)有暴露任何構(gòu)造器來(lái)創(chuàng)建該類的實(shí)例,程序應(yīng)該通過(guò)它提供的類方法來(lái)創(chuàng)建EnumSet對(duì)象。

EnumSet沒(méi)有其他額外增加的方法,只是增加了一些創(chuàng)建EnumSet對(duì)象的方法。

EnumSet創(chuàng)建對(duì)象的方法


補(bǔ)充:EnumSet 也是非線程安全的。

四、HashSet、TreeSet和EnumSet的性能對(duì)比

EnumSet性能>HashSet性能>LinkedHashSet>TreeSet性能

EnumSet內(nèi)部以位向量的形式存儲(chǔ),結(jié)構(gòu)緊湊、高效,且只存儲(chǔ)枚舉類的枚舉值,所以最高效。HashSet以hash算法進(jìn)行位置存儲(chǔ),特別適合用于添加、查詢操作。LinkedHashSet由于要維護(hù)鏈表,性能比HashSet差點(diǎn),但是有了鏈表,LinkedHashSet更適合于插入、刪除以及遍歷操作。而TreeSet需要額外的紅黑樹(shù)算法來(lái)維護(hù)集合的次序,性能最次。

但是具體使用要考慮具體的使用場(chǎng)景。
當(dāng)需要一個(gè)特定排序的集合時(shí),使用TreeSet集合。
當(dāng)需要保存枚舉類的枚舉值時(shí),使用EnumSet集合。
當(dāng)經(jīng)常使用添加、查詢操作時(shí),使用HashSet。
當(dāng)經(jīng)常插入排序或使用刪除、插入及遍歷操作時(shí),使用LinkedHashSet。

后續(xù)文章將對(duì)java集合中的具體實(shí)現(xiàn)類進(jìn)行深入了解。有興趣的話可以觀看后續(xù)內(nèi)容,進(jìn)一步了解java集合內(nèi)容。

由淺入深理解java集合(一)——集合框架 Collction、Map
由淺入深理解java集合(三)——集合 List
由淺入深理解java集合(四)——集合 Queue
由淺入深理解java集合(五)——集合 Map
由淺入深理解java集合(六)——集合增刪改查的細(xì)節(jié)、性能及選擇推薦(待更新)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚(yú)_t_閱讀 31,765評(píng)論 18 399
  • 本文出自 Eddy Wiki ,轉(zhuǎn)載請(qǐng)注明出處:http://eddy.wiki/interview-java.h...
    eddy_wiki閱讀 1,183評(píng)論 0 16
  • java筆記第一天 == 和 equals ==比較的比較的是兩個(gè)變量的值是否相等,對(duì)于引用型變量表示的是兩個(gè)變量...
    jmychou閱讀 1,518評(píng)論 0 3
  • 以下是《瘋狂Java講義》中的一些知識(shí),如有錯(cuò)誤,煩請(qǐng)指正。 集合概述 Java集合可以分為Set、List、Ma...
    hainingwyx閱讀 555評(píng)論 0 1
  • 書(shū)接上文,本次我們來(lái)談?wù)勗鯓痈鶕?jù)需求來(lái)做到合適的垂直居中。 1.文字居中-line-height; 在文字垂直居中...
    早安馬丁閱讀 551評(píng)論 0 0