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

上一篇文章介紹了Set集合的通用知識。Set集合中包含了三個比較重要的實現類:HashSet、TreeSet和EnumSet。本篇文章將重點介紹這三個類。

一、HashSet類

HashSet簡介

HashSet是Set接口的典型實現,實現了Set接口中的所有方法,並沒有添加額外的方法,大多數時候使用Set集合時就是使用這個實現類。HashSet按Hash算法來存儲集合中的元素。因此具有很好的存取和查找性能。

HashSet特點

1.不能保證元素的排列順序,順序可能與添加順序不同,順序也有可能發生變化。
2.HashSet不是同步的,如果多個線程同時訪問一個HashSet,則必須通過代碼來保證其同步。
3.集合元素值可以是null。
除此之外,HashSet判斷兩個元素是否相等的標準也是其一大特點。HashSet集合判斷兩個元素相等的標準是兩個對象通過equals()方法比較相等,並且兩個對象的hashCode()方法返回值也相等。

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

equals()和hashCode()

equals()

equals() 的作用是用來判斷兩個對象是否相等。

equals() 定義在JDK的Object.java中。通過判斷兩個對象的地址是否相等(即,是否是同一個對象)來區分它們是否相等。源碼如下:

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

既然Object.java中定義了equals()方法,這就意味着所有的Java類都實現了equals()方法,所有的類都可以通過equals()去比較兩個對象是否相等。 但是,使用默認的“equals()”方法,等價於“==”方法。我們也可以在Object的子類中重寫此方法,自定義“equals()”方法,在其中定義自己的判斷邏輯,如果滿足則返回true,不滿足則返回false。下面我們自定義一個類 Person,並認爲年齡,身高相等的兩個Person對象,equals()方法比較結果相等。

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));
    }

}

輸出結果:

true

下面根據“類是否覆蓋equals()方法”,將它分爲2類。
(01) 若某個類沒有覆蓋equals()方法,當它的通過equals()比較兩個對象時,實際上是比較兩個對象是不是同一個對象。這時,等價於通過“==”去比較這兩個對象,即兩個對象的內存地址是否相同。
(02) 我們可以覆蓋類的equals()方法,來讓equals()通過其它方式比較兩個對象是否相等。通常的做法是:若兩個對象的內容相等,則equals()方法返回true;否則,返回fasle。

hashCode()

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

hashCode() 定義在JDK的Object.java中,這就意味着Java中的任何類都包含有hashCode() 函數。雖然,每個Java類都包含hashCode() 函數。但是,僅僅當創建某個“類”的散列表時,該類的hashCode() 纔有用。更通俗地說就是創建包含該類的HashMap,Hashtable,HashSet集合時,hashCode() 纔有用。因爲HashMap,Hashtable,HashSet就是散列表集合。
在散列表中,hashCode()作用是:確定該類的每一個對象在散列表中的位置;其它情況下類的hashCode() 沒有作用。在散列表中hashCode() 的作用是獲取對象的散列碼,進而確定該對象在散列表中的位置。

hashCode()也分兩種情況。一種是Object類中默認的方法,另一種是在子類中重寫的方法。
(01) 若某個類沒有覆蓋hashCode()方法,當它的通過hashCode()比較兩個對象時,實際上是比較兩個對象是不是同一個對象。這時,等價於通過“==”去比較這兩個對象,即兩個對象的內存地址是否相同。
(02) 我們可以覆蓋類的hashCode()方法,來讓hashCode()通過其它方式比較兩個對象是否相等。通常的做法是:若兩個對象的內容相等,則hashCode()方法返回true;否則,返回fasle。

通過對以上兩個方法的瞭解。我們可以接下來學習HashSet集合中如何判斷兩個元素是否相等?

HashSet中判斷集合元素相等

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

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

3.如果兩個對象通過equals()方法比較不相等,hashCode()方法比較相等,HashSet將會把它們存儲在相同的位置,在這個位置以鏈表式結構來保存多個對象。這是因爲當向HashSet集合中存入一個元素時,HashSet會調用對象的hashCode()方法來得到對象的hashCode值,然後根據該hashCode值來決定該對象存儲在HashSet中存儲位置。

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

HashSet判斷兩個元素相等的標準:兩個對象通過equals()方法比較相等,並且兩個對象的hashCode()方法返回值也相等。

注意: HashSet是根據元素的hashCode值來快速定位的,如果HashSet中兩個以上的元素具有相同的hashCode值,將會導致性能下降。所以如果重寫類的equals()方法和hashCode()方法時,應儘量保證兩個對象通過hashCode()方法返回值相等時,通過equals()方法比較返回true。

LinkedHashSet類

LinkedHashSet是HashSet對的子類,也是根據元素的hashCode值來決定元素的存儲位置,同時使用鏈表維護元素的次序,使得元素是以插入的順序來保存的。當遍歷LinkedHashSet集合裏的元素時,LinkedHashSet將會按元素的添加順序來訪問集合裏的元素。但是由於要維護元素的插入順序,在性能上略低與HashSet,但在迭代訪問Set裏的全部元素時有很好的性能。
注意: LinkedHashSet依然不允許元素重複,判斷重複標準與HashSet一致。

補充: HashSet的實質是一個HashMap。HashSet的所有集合元素,構成了HashMap的key,其value爲一個靜態Object對象。因此HashSet的所有性質,HashMap的key所構成的集合都具備。可以參考後續文章中HashMap的相關內容進行比對。

二、TreeSet類

TreeSet簡介

TreeSet是SortedSet接口的實現類,正如SortedSet名字所暗示的,TreeSet可以確保集合元素處於排序狀態。此外,TreeSet還提供了幾個額外的方法。

TreeSet的方法

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

TreeSet的排序方式

TreeSet中所謂的有序,不同於之前所講的插入順序,而是通過集合中元素屬性進行排序方式來實現的。
TreeSet支持兩種排序方法:自然排序和定製排序。在默認情況下,TreeSet採用自然排序。

1.自然排序

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

Java提供了一個Comparable接口,該接口裏定義了一個compareTo(Object obj)方法,該方法返回一個整數值,實現該接口的類必須實現該方法,實現了該接口的類的對象就可以比較大小了。當一個對象調用該方法與另一個對象比較時,例如obj1.compareTo(obj2),如果該方法返回0,則表明兩個對象相等;如果該方法返回一個整數,則表明obj1大於obj2;如果該方法返回一個負整數,則表明oj1小於obj2。

TreeSet會調用集合中元素所屬類的compareTo(Object obj)方法來比較元素之間的大小關係,然後將集合元素按升序排列,即把通過compareTo(Object obj)方法比較後比較大的的往後排。這種方式就是自然排序。

Java的一些常用類已經實現了Comparable接口,並提供了比較大小的標準。例如,String按字符串的UNICODE值進行比較,Integer等所有數值類型對應的包裝類按它們的數值大小進行比較。
除了這些已經實現Comparable接口類之外,如果試圖把一個對象添加到TreeSet時,則該對象的類必須實現Comparable接口,否則就會出現異常。
注意: TreeSet中只能添加同一種類型的對象,否則無法比較,會出現異常。

TreeSet中判斷集合元素相等

對於TreeSet集合而言,判斷兩個對象是否相等的唯一標準是:兩個對象通過compareTo(Object obj)方法比較是否返回0——如果通過compareTo(Object obj)方法比較返回0,TreeSet則會認爲它們相等,不予添加入集合內;否則就認爲它們不相等,添加到集合內。
TreeSet是根據紅黑樹結構找到集合元素的存儲位置。

2.定製排序

TreeSet的自然排序是根據集合元素中compareTo(Object obj)比較的大小,以升序排列。而定製排序是通過Comparator接口的幫助。該接口包含一個int compare(T o1,T o2)方法,該方法用於比較o1,o2的大小:如果該方法返回正整數,則表明o1大於o2;如果該方法返回0,則表明o1等於o2;如果該方法返回負整數,則表明o1小於o2。
如果要實現定製排序,則需要在創建TreeSet時,調用一個帶參構造器,傳入Comparator對象。並有該Comparator對象負責集合元素的排序邏輯,集合元素可以不必實現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]]

總結:無論使用自然排序還是定製排序,都可以通過自定義比較邏輯實現各種各樣的排序方式。

注意:如果向TreeSet中添加了一個可變對象後,並且後面程序修改了該可變對象的實例變量,這將導致它與其他對象的大小順序發生了改變,但TreeSet不會再次調整它們。下面程序演示這一現象:

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實現Comparable接口,將Person對象按照年齡從小到大升序排列。
輸出結果:

初始年齡排序
[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]]

可以看到並沒有發生變化,而且如果修改後進行元素刪除操作可能會不成功,具體比較複雜。總之,推薦不要修改放入TreeSet集合中元素的關鍵實例變量。
補充:TreeSet也是非線程安全的。

三、EnumSet類

EnumSet簡介

EnumSet是一個專爲枚舉類設計的集合類,EnumSet中的所有元素都必須是指定枚舉類型的枚舉值,該枚舉類型在創建EnumSet時顯示或隱式地指定。EnumSet的集合元素也是有序的,EnumSet以枚舉值在EnumSet類內的定義順序來決定集合元素的順序。

EnumSet特點

1.EnumSet集合不允許加入null元素。EnumSet中的所有元素都必須是指定枚舉類型的枚舉值。
2.EnumSet類沒有暴露任何構造器來創建該類的實例,程序應該通過它提供的類方法來創建EnumSet對象。

EnumSet沒有其他額外增加的方法,只是增加了一些創建EnumSet對象的方法。

EnumSet創建對象的方法


補充:EnumSet 也是非線程安全的。

四、HashSet、TreeSet和EnumSet的性能對比

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

EnumSet內部以位向量的形式存儲,結構緊湊、高效,且只存儲枚舉類的枚舉值,所以最高效。HashSet以hash算法進行位置存儲,特別適合用於添加、查詢操作。LinkedHashSet由於要維護鏈表,性能比HashSet差點,但是有了鏈表,LinkedHashSet更適合於插入、刪除以及遍歷操作。而TreeSet需要額外的紅黑樹算法來維護集合的次序,性能最次。

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

後續文章將對java集合中的具體實現類進行深入瞭解。有興趣的話可以觀看後續內容,進一步瞭解java集合內容。

發佈了44 篇原創文章 · 獲贊 54 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章